Browse Source

Fix exception trap. Re-Add Mime

mscherer 3 years ago
parent
commit
f38eb8a6d1
42 changed files with 1423 additions and 431 deletions
  1. 1 1
      composer.json
  2. 10 12
      docs/Error/ErrorHandler.md
  3. 1 0
      phpstan.neon
  4. 64 0
      src/Auth/AbstractPasswordHasher.php
  5. 66 0
      src/Auth/DefaultPasswordHasher.php
  6. 0 90
      src/Auth/MultiColumnAuthenticate.php
  7. 45 0
      src/Auth/PasswordHasherFactory.php
  8. 3 1
      src/Error/ErrorLogger.php
  9. 4 4
      src/Error/ErrorHandler.php
  10. 7 7
      src/Error/Middleware/ErrorHandlerMiddleware.php
  11. 5 6
      src/Model/Behavior/BitmaskedBehavior.php
  12. 3 3
      src/Model/Behavior/PasswordableBehavior.php
  13. 11 11
      src/Model/Table/Table.php
  14. 6 6
      src/Model/Table/TokensTable.php
  15. 6 9
      src/Utility/DateTime.php
  16. 1 0
      src/Utility/FileLog.php
  17. 833 0
      src/Utility/Mime.php
  18. 3 3
      src/View/Helper/CommonHelper.php
  19. 1 1
      src/View/Helper/IconHelper.php
  20. 1 1
      templates/Admin/Format/icons.php
  21. 0 119
      tests/TestCase/Auth/MultiColumnAuthenticateTest.php
  22. 7 0
      tests/TestCase/Controller/Admin/FormatControllerTest.php
  23. 2 2
      tests/TestCase/Controller/Component/MobileComponentTest.php
  24. 7 7
      tests/TestCase/Controller/Component/UrlComponentTest.php
  25. 2 2
      tests/TestCase/Controller/ControllerTest.php
  26. 1 1
      tests/TestCase/ErrorHandler/ErrorLoggerTest.php
  27. 6 6
      tests/TestCase/ErrorHandler/ErrorHandlerTest.php
  28. 1 1
      tests/TestCase/Mailer/MessageTest.php
  29. 16 16
      tests/TestCase/Model/Behavior/ConfirmableBehaviorTest.php
  30. 7 6
      tests/TestCase/Model/Behavior/PasswordableBehaviorTest.php
  31. 3 3
      tests/TestCase/Model/Behavior/SluggedBehaviorTest.php
  32. 4 9
      tests/TestCase/Model/Behavior/TypeMapBehaviorTest.php
  33. 54 53
      tests/TestCase/Model/Table/TableTest.php
  34. 1 1
      tests/TestCase/Utility/DateTimeTest.php
  35. 173 0
      tests/TestCase/Utility/MimeTest.php
  36. 16 28
      tests/TestCase/View/Helper/CommonHelperTest.php
  37. 21 12
      tests/TestCase/View/Helper/FormatHelperTest.php
  38. 2 1
      tests/TestCase/View/Helper/HtmlHelperTest.php
  39. 9 6
      tests/TestCase/View/Helper/UrlHelperTest.php
  40. 2 2
      tests/bootstrap.php
  41. 1 1
      tests/test_app/Model/Table/ToolsUsersTable.php
  42. 17 0
      tests/test_app/Utility/TestMime.php

+ 1 - 1
composer.json

@@ -27,7 +27,7 @@
 		"dereuromark/cakephp-shim": "dev-cake5"
 	},
 	"require-dev": {
-		"mobiledetect/mobiledetectlib": "^2.8",
+		"mobiledetect/mobiledetectlib": "^3.74",
 		"fig-r/psr2r-sniffer": "dev-next",
 		"yangqi/htmldom": "^1.0",
 		"phpunit/phpunit": "^9.5"

+ 10 - 12
docs/Error/ErrorHandler.md

@@ -1,4 +1,4 @@
-# Improved version of ErrorHandler
+# Improved version of ExceptionTrap
 
 The main goal of the error.log is to notify about internal errors of the system.
 By default there would also be a lot of noise in there.
@@ -14,7 +14,7 @@ Log::config('404', [
 ]);
 ```
 
-Make sure your other log configs are scope-deactivated then to prevent them being 
+Make sure your other log configs are scope-deactivated then to prevent them being
 logged twice (`config/app.php`):
 ```php
     'Log' => [
@@ -29,17 +29,15 @@ logged twice (`config/app.php`):
     ],
 ```
 
-In your `config/bootstrap.php`, the following snippet just needs to include the 
+In your `config/bootstrap.php`, the following snippet just needs to include the
 ErrorHandler of this plugin:
+
 ```php
-// Switch Cake\Error\ErrorHandler to
-use Tools\Error\ErrorHandler;
-
-if ($isCli) {
-    (new ConsoleErrorHandler(Configure::read('Error')))->register();
-} else {
-    (new ErrorHandler(Configure::read('Error')))->register();
-}
+// Switch Cake\Error\ExceptionTrap to
+use Tools\Error\ExceptionTrap;
+
+(new ErrorTrap(Configure::read('Error')))->register();
+(new ExceptionTrap(Configure::read('Error')))->register();
 ```
 
 Also, make sure to switch out the middleware:
@@ -71,7 +69,7 @@ So those are considered actual errors here.
 
 ### Adding more exceptions
 
-In case you need custom 404 mappings for some additional custom exceptions, 
+In case you need custom 404 mappings for some additional custom exceptions,
 make use of `log404` option in your `app.php`.
 It will overwrite the current defaults completely.
 ```php

+ 1 - 0
phpstan.neon

@@ -4,6 +4,7 @@ parameters:
 		- src/
 	checkMissingIterableValueType: false
 	checkGenericClassInNonGenericObjectType: false
+	treatPhpDocTypesAsCertain: false
 	bootstrapFiles:
 		- %rootDir%/../../../tests/bootstrap.php
 	excludePaths:

+ 64 - 0
src/Auth/AbstractPasswordHasher.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace Tools\Auth;
+
+use Cake\Core\InstanceConfigTrait;
+
+/**
+ * Abstract password hashing class
+ */
+abstract class AbstractPasswordHasher {
+
+	use InstanceConfigTrait;
+
+	/**
+	 * Default config
+	 *
+	 * These are merged with user-provided config when the object is used.
+	 *
+	 * @var array<string, mixed>
+	 */
+	protected array $_defaultConfig = [];
+
+	/**
+	 * Constructor
+	 *
+	 * @param array<string, mixed> $config Array of config.
+	 */
+	public function __construct(array $config = []) {
+		$this->setConfig($config);
+	}
+
+	/**
+	 * Generates password hash.
+	 *
+	 * @param string $password Plain text password to hash.
+	 * @return string The password hash
+	 */
+	abstract public function hash(string $password): string;
+
+	/**
+	 * Check hash. Generate hash from user provided password string or data array
+	 * and check against existing hash.
+	 *
+	 * @param string $password Plain text password to hash.
+	 * @param string $hashedPassword Existing hashed password.
+	 * @return bool True if hashes match else false.
+	 */
+	abstract public function check(string $password, string $hashedPassword): bool;
+
+	/**
+	 * Returns true if the password need to be rehashed, due to the password being
+	 * created with anything else than the passwords generated by this class.
+	 *
+	 * Returns true by default since the only implementation users should rely
+	 * on is the one provided by default in php 5.5+ or any compatible library
+	 *
+	 * @param string $password The password to verify
+	 * @return bool
+	 */
+	public function needsRehash(string $password): bool {
+		return password_needs_rehash($password, PASSWORD_DEFAULT);
+	}
+
+}

+ 66 - 0
src/Auth/DefaultPasswordHasher.php

@@ -0,0 +1,66 @@
+<?php
+declare(strict_types=1);
+
+namespace Tools\Auth;
+
+/**
+ * Default password hashing class.
+ */
+class DefaultPasswordHasher extends AbstractPasswordHasher {
+
+	/**
+	 * Default config for this object.
+	 *
+	 * ### Options
+	 *
+	 * - `hashType` - Hashing algo to use. Valid values are those supported by `$algo`
+	 *   argument of `password_hash()`. Defaults to `PASSWORD_DEFAULT`
+	 * - `hashOptions` - Associative array of options. Check the PHP manual for
+	 *   supported options for each hash type. Defaults to empty array.
+	 *
+	 * @var array<string, mixed>
+	 */
+	protected array $_defaultConfig = [
+		'hashType' => PASSWORD_DEFAULT,
+		'hashOptions' => [],
+	];
+
+	/**
+	 * Generates password hash.
+	 *
+	 * @psalm-suppress InvalidNullableReturnType
+	 * @link https://book.cakephp.org/4/en/controllers/components/authentication.html#hashing-passwords
+	 * @param string $password Plain text password to hash.
+	 * @return string Password hash or false on failure
+	 */
+	public function hash(string $password): string {
+		return password_hash(
+			$password,
+			$this->_config['hashType'],
+			$this->_config['hashOptions'],
+		);
+	}
+
+	/**
+	 * Check hash. Generate hash for user provided password and check against existing hash.
+	 *
+	 * @param string $password Plain text password to hash.
+	 * @param string $hashedPassword Existing hashed password.
+	 * @return bool True if hashes match else false.
+	 */
+	public function check(string $password, string $hashedPassword): bool {
+		return password_verify($password, $hashedPassword);
+	}
+
+	/**
+	 * Returns true if the password need to be rehashed, due to the password being
+	 * created with anything else than the passwords generated by this class.
+	 *
+	 * @param string $password The password to verify
+	 * @return bool
+	 */
+	public function needsRehash(string $password): bool {
+		return password_needs_rehash($password, $this->_config['hashType'], $this->_config['hashOptions']);
+	}
+
+}

+ 0 - 90
src/Auth/MultiColumnAuthenticate.php

@@ -1,90 +0,0 @@
-<?php
-
-namespace Tools\Auth;
-
-use Cake\Auth\FormAuthenticate;
-use Cake\Controller\ComponentRegistry;
-use Cake\ORM\Query;
-use Cake\ORM\TableRegistry;
-
-/**
- * An authentication adapter for AuthComponent
- *
- * Provides the ability to authenticate using POST data. The username form input
- * can be checked against multiple table columns, for instance username and email
- *
- * ```
- *    $this->Auth->setConfig('authenticate', [
- *        'Tools.MultiColumn' => [
- *            'fields' => [
- *                'username' => 'login',
- *                'password' => 'password'
- *             ],
- *            'columns' => ['username', 'email'],
- *        ]
- *    ]);
- * ```
- *
- * Licensed under The MIT License
- * Copied from discontinued FriendsOfCake/Authenticate
- */
-class MultiColumnAuthenticate extends FormAuthenticate {
-
-	/**
-	 * Besides the keys specified in BaseAuthenticate::$_defaultConfig,
-	 * MultiColumnAuthenticate uses the following extra keys:
-	 *
-	 * - 'columns' Array of columns to check username form input against
-	 *
-	 * @param \Cake\Controller\ComponentRegistry $registry The Component registry
-	 *   used on this request.
-	 * @param array $config Array of config to use.
-	 */
-	public function __construct(ComponentRegistry $registry, $config) {
-		$this->setConfig([
-			'columns' => [],
-		]);
-
-		parent::__construct($registry, $config);
-	}
-
-	/**
-	 * Get query object for fetching user from database.
-	 *
-	 * @param string $username The username/identifier.
-	 * @return \Cake\ORM\Query
-	 */
-	protected function _query(string $username): Query {
-		$table = TableRegistry::get($this->_config['userModel']);
-
-		$columns = [];
-		foreach ($this->_config['columns'] as $column) {
-			$columns[] = [$table->aliasField($column) => $username];
-		}
-		$conditions = ['OR' => $columns];
-
-		$options = [
-			'conditions' => $conditions,
-		];
-
-		if (!empty($this->_config['scope'])) {
-			$options['conditions'] = array_merge($options['conditions'], $this->_config['scope']);
-		}
-		if (!empty($this->_config['contain'])) {
-			$options['contain'] = $this->_config['contain'];
-		}
-
-		$finder = $this->_config['finder'];
-		if (is_array($finder)) {
-			$options += current($finder);
-			$finder = key($finder);
-		}
-
-		if (!isset($options['username'])) {
-			$options['username'] = $username;
-		}
-
-		return $table->find($finder, $options);
-	}
-
-}

+ 45 - 0
src/Auth/PasswordHasherFactory.php

@@ -0,0 +1,45 @@
+<?php
+
+namespace Tools\Auth;
+
+use Cake\Core\App;
+use RuntimeException;
+
+/**
+ * Builds password hashing objects
+ */
+class PasswordHasherFactory {
+
+	/**
+	 * Returns password hasher object out of a hasher name or a configuration array
+	 *
+	 * @param array<string, mixed>|string $passwordHasher Name of the password hasher or an array with
+	 * at least the key `className` set to the name of the class to use
+	 * @throws \RuntimeException If password hasher class not found or
+	 *   it does not extend {@link \Tools\Auth\AbstractPasswordHasher}
+	 * @return \Tools\Auth\AbstractPasswordHasher Password hasher instance
+	 */
+	public static function build($passwordHasher): AbstractPasswordHasher {
+		$config = [];
+		if (is_string($passwordHasher)) {
+			$class = $passwordHasher;
+		} else {
+			$class = $passwordHasher['className'];
+			$config = $passwordHasher;
+			unset($config['className']);
+		}
+
+		$className = App::className('Tools.' . $class, 'Auth', 'PasswordHasher');
+		if ($className === null) {
+			throw new RuntimeException(sprintf('Password hasher class "%s" was not found.', $class));
+		}
+
+		$hasher = new $className($config);
+		if (!($hasher instanceof AbstractPasswordHasher)) {
+			throw new RuntimeException('Password hasher must extend AbstractPasswordHasher class.');
+		}
+
+		return $hasher;
+	}
+
+}

+ 3 - 1
src/Error/ErrorLogger.php

@@ -39,7 +39,9 @@ class ErrorLogger extends CoreErrorLogger {
 			return Log::write($level, $message, ['404']);
 		}
 
-		return parent::log($exception, $request);
+		parent::logException($exception, $request);
+
+		return true;
 	}
 
 }

+ 4 - 4
src/Error/ErrorHandler.php

@@ -3,13 +3,13 @@
 namespace Tools\Error;
 
 use Cake\Core\Configure;
-use Cake\Error\ErrorHandler as CoreErrorHandler;
+use Cake\Error\ExceptionTrap as CoreExceptionTrap;
 
 /**
  * Custom ErrorHandler to not mix the 404 exceptions with the rest of "real" errors in the error.log file.
  *
  * All you need to do is:
- * - switch `use Cake\Error\ErrorHandler;` with `use Tools\Error\ErrorHandler;` in your bootstrap
+ * - switch `use Cake\Error\ExceptionTrap;` with `use Tools\Error\ExceptionTrap;` in your bootstrap
  * - Make sure you got the 404 log defined either in your app.php or as Log::config() call.
  *
  * Example config as scoped one:
@@ -25,7 +25,7 @@ use Cake\Error\ErrorHandler as CoreErrorHandler;
  * In case you need custom 404 mappings for some additional custom exceptions, make use of `log404` option.
  * It will overwrite the current defaults completely.
  */
-class ErrorHandler extends CoreErrorHandler {
+class ExceptionTrap extends CoreExceptionTrap {
 
 	/**
 	 * Constructor
@@ -35,7 +35,7 @@ class ErrorHandler extends CoreErrorHandler {
 	public function __construct(array $config = []) {
 		$config += (array)Configure::read('Error');
 		$config += [
-			'errorLogger' => ErrorLogger::class,
+			'logger' => ErrorLogger::class,
 		];
 
 		parent::__construct($config);

+ 7 - 7
src/Error/Middleware/ErrorHandlerMiddleware.php

@@ -4,7 +4,7 @@ namespace Tools\Error\Middleware;
 
 use Cake\Core\Configure;
 use Cake\Error\Middleware\ErrorHandlerMiddleware as CoreErrorHandlerMiddleware;
-use Tools\Error\ErrorHandler;
+use Tools\Error\ExceptionTrap;
 
 /**
  * Custom ErrorHandler to not mix the 404 exceptions with the rest of "real" errors in the error.log file.
@@ -12,16 +12,16 @@ use Tools\Error\ErrorHandler;
 class ErrorHandlerMiddleware extends CoreErrorHandlerMiddleware {
 
 	/**
-	 * @param \Cake\Error\ErrorHandler|array $errorHandler The error handler instance
+	 * @param \Cake\Error\ExceptionTrap|array $exceptionTrap The error handler instance
 	 *  or config array.
 	 */
-	public function __construct($errorHandler = []) {
-		if (is_array($errorHandler)) {
-			$errorHandler += (array)Configure::read('Error');
+	public function __construct($exceptionTrap = []) {
+		if (is_array($exceptionTrap)) {
+			$exceptionTrap += (array)Configure::read('Error');
 		}
-		parent::__construct($errorHandler);
+		parent::__construct($exceptionTrap);
 
-		$this->errorHandler = new ErrorHandler($this->getConfig());
+		$this->exceptionTrap = new ExceptionTrap($this->getConfig());
 	}
 
 }

+ 5 - 6
src/Model/Behavior/BitmaskedBehavior.php

@@ -7,7 +7,6 @@ use Cake\Database\Expression\ComparisonExpression;
 use Cake\Datasource\EntityInterface;
 use Cake\Event\EventInterface;
 use Cake\ORM\Behavior;
-use Cake\ORM\Query;
 use Cake\ORM\Query\SelectQuery;
 use Cake\Utility\Inflector;
 use InvalidArgumentException;
@@ -51,12 +50,12 @@ class BitmaskedBehavior extends Behavior {
 	];
 
 	/**
-	 * @param \Cake\ORM\Query $query
+	 * @param \Cake\ORM\Query\SelectQuery $query
 	 * @param array<string, mixed> $options
 	 * @throws \InvalidArgumentException If the 'slug' key is missing in options
-	 * @return \Cake\ORM\Query
+	 * @return \Cake\ORM\Query\SelectQuery
 	 */
-	public function findBitmasked(Query $query, array $options) {
+	public function findBitmasked(SelectQuery $query, array $options) {
 		if (!isset($options['bits'])) {
 			throw new InvalidArgumentException("The 'bits' key is required for find('bits')");
 		}
@@ -269,10 +268,10 @@ class BitmaskedBehavior extends Behavior {
 	}
 
 	/**
-	 * @param \Cake\ORM\Query $query
+	 * @param \Cake\ORM\Query\SelectQuery $query
 	 * @return void
 	 */
-	public function encodeBitmaskConditions(Query $query) {
+	public function encodeBitmaskConditions(SelectQuery $query) {
 		$field = $this->_config['field'];
 		$mappedField = $this->_config['mappedField'];
 		if (!$mappedField) {

+ 3 - 3
src/Model/Behavior/PasswordableBehavior.php

@@ -3,13 +3,13 @@
 namespace Tools\Model\Behavior;
 
 use ArrayObject;
-use Cake\Auth\PasswordHasherFactory;
 use Cake\Core\Configure;
 use Cake\Datasource\EntityInterface;
 use Cake\Event\EventInterface;
 use Cake\ORM\Behavior;
 use Cake\ORM\Table;
 use RuntimeException;
+use Tools\Auth\PasswordHasherFactory;
 
 if (!defined('PWD_MIN_LENGTH')) {
 	define('PWD_MIN_LENGTH', 6);
@@ -95,7 +95,7 @@ class PasswordableBehavior extends Behavior {
 	/**
 	 * Password hasher instance.
 	 *
-	 * @var \Cake\Auth\AbstractPasswordHasher|null
+	 * @var \Tools\Auth\AbstractPasswordHasher|null
 	 */
 	protected $_passwordHasher;
 
@@ -480,7 +480,7 @@ class PasswordableBehavior extends Behavior {
 	/**
 	 * @param array<string, mixed>|string $hasher Name or options array.
 	 * @param array<string, mixed> $options
-	 * @return \Cake\Auth\AbstractPasswordHasher
+	 * @return \Tools\Auth\AbstractPasswordHasher
 	 */
 	protected function _getPasswordHasher($hasher, array $options = []) {
 		if ($this->_passwordHasher) {

+ 11 - 11
src/Model/Table/Table.php

@@ -97,7 +97,7 @@ class Table extends ShimTable {
 	 * @param string|null $groupField Field to group by
 	 * @param string $type Find type
 	 * @param array<string, mixed> $options
-	 * @return \Cake\ORM\Query
+	 * @return \Cake\ORM\Query\SelectQuery
 	 */
 	public function getRelatedInUse($tableName, $groupField = null, $type = 'all', array $options = []) {
 		if ($groupField === null) {
@@ -134,7 +134,7 @@ class Table extends ShimTable {
 	 * @param string $groupField Field to group by
 	 * @param string $type Find type
 	 * @param array<string, mixed> $options
-	 * @return \Cake\ORM\Query
+	 * @return \Cake\ORM\Query\SelectQuery
 	 */
 	public function getFieldInUse($groupField, $type = 'all', array $options = []) {
 		/** @var string $displayField */
@@ -308,7 +308,7 @@ class Table extends ShimTable {
 		}
 		$format = !empty($options['dateFormat']) ? $options['dateFormat'] : 'ymd';
 
-		/** @var \Cake\Chronos\ChronosInterface $time */
+		/** @var \Cake\Chronos\ChronosDate $time */
 		$time = $value;
 		if (!is_object($value)) {
 			$time = new DateTime($value);
@@ -351,24 +351,24 @@ class Table extends ShimTable {
 
 			if (!empty($options['after'])) {
 				$compare = $compareValue->subSeconds($seconds);
-				if ($options['after']->gt($compare)) {
+				if ($options['after']->greaterThan($compare)) {
 					return false;
 				}
 				if (!empty($options['max'])) {
 					$after = $options['after']->addSeconds($options['max']);
-					if ($time->gt($after)) {
+					if ($time->greaterThan($after)) {
 						return false;
 					}
 				}
 			}
 			if (!empty($options['before'])) {
 				$compare = $compareValue->addSeconds($seconds);
-				if ($options['before']->lt($compare)) {
+				if ($options['before']->lessThan($compare)) {
 					return false;
 				}
 				if (!empty($options['max'])) {
 					$after = $options['before']->subSeconds($options['max']);
-					if ($time->lt($after)) {
+					if ($time->lessThan($after)) {
 						return false;
 					}
 				}
@@ -415,23 +415,23 @@ class Table extends ShimTable {
 			$days = !empty($options['min']) ? $options['min'] : 0;
 			if (!empty($options['after']) && isset($context['data'][$options['after']])) {
 				$compare = $dateTime->subDays($days);
-				/** @var \Cake\I18n\Time $after */
+				/** @var \Cake\I18n\DateTime $after */
 				$after = $context['data'][$options['after']];
 				if (!is_object($after)) {
 					$after = new DateTime($after);
 				}
-				if ($after->gt($compare)) {
+				if ($after->greaterThan($compare)) {
 					return false;
 				}
 			}
 			if (!empty($options['before']) && isset($context['data'][$options['before']])) {
 				$compare = $dateTime->addDays($days);
-				/** @var \Cake\I18n\Time $before */
+				/** @var \Cake\I18n\DateTime $before */
 				$before = $context['data'][$options['before']];
 				if (!is_object($before)) {
 					$before = new DateTime($before);
 				}
-				if ($before->lt($compare)) {
+				if ($before->lessThan($compare)) {
 					return false;
 				}
 			}

+ 6 - 6
src/Model/Table/TokensTable.php

@@ -25,27 +25,27 @@ class TokensTable extends Table {
 	/**
 	 * @var string
 	 */
-	public $displayField = 'token_key';
+	public string $displayField = 'token_key';
 
 	/**
-	 * @var array<mixed, mixed>|string|null
+	 * @var array<int|string, mixed>
 	 */
-	public $order = ['created' => 'DESC'];
+	public array $order = ['created' => 'DESC'];
 
 	/**
 	 * @var int
 	 */
-	public $defaultLength = 30;
+	public int $defaultLength = 30;
 
 	/**
 	 * @var int
 	 */
-	public $validity = WEEK;
+	public int $validity = WEEK;
 
 	/**
 	 * @var array
 	 */
-	public $validate = [
+	public array $validate = [
 		'type' => [
 			'notBlank' => [
 				'rule' => 'notBlank',

+ 6 - 9
src/Utility/DateTime.php

@@ -4,9 +4,6 @@ namespace Tools\Utility;
 
 use Cake\Core\Configure;
 use Cake\I18n\Date as CakeDate;
-
-use Cake\Core\Configure;
-use Cake\I18n\Date as CakeDate;
 use Cake\I18n\DateTime as CakeDateTime;
 use DateInterval;
 use DateTime as NativeDateTime;
@@ -527,7 +524,7 @@ class DateTime extends CakeDateTime {
 
 		if (!is_object($dateString)) {
 			if (is_string($dateString) && strlen($dateString) === 10) {
-				$date = new Date($dateString);
+				$date = new CakeDateTime($dateString);
 			} else {
 				$date = new static($dateString);
 			}
@@ -536,7 +533,7 @@ class DateTime extends CakeDateTime {
 		}
 
 		if ($format === null) {
-			if ($date instanceof Date) {
+			if ($date instanceof CakeDateTime) {
 				$format = FORMAT_NICE_YMD;
 			} else {
 				$format = FORMAT_NICE_YMDHM;
@@ -986,7 +983,7 @@ class DateTime extends CakeDateTime {
 	/**
 	 * Returns true if given datetime string was day before yesterday.
 	 *
-	 * @param \Cake\Chronos\ChronosInterface $date Datetime
+	 * @param \Cake\Chronos\Chronos $date Datetime
 	 * @return bool True if datetime string was day before yesterday
 	 */
 	public static function wasDayBeforeYesterday($date) {
@@ -996,7 +993,7 @@ class DateTime extends CakeDateTime {
 	/**
 	 * Returns true if given datetime string is the day after tomorrow.
 	 *
-	 * @param \Cake\Chronos\ChronosInterface $date Datetime
+	 * @param \Cake\Chronos\Chronos $date Datetime
 	 * @return bool True if datetime string is day after tomorrow
 	 */
 	public static function isDayAfterTomorrow($date) {
@@ -1297,8 +1294,8 @@ class DateTime extends CakeDateTime {
 	 */
 	public static function duration($duration, $format = '%h:%I:%S') {
 		if (!$duration instanceof DateInterval) {
-			$d1 = new CakeDateTime();
-			$d2 = new CakeDateTime();
+			$d1 = new NativeDateTime();
+			$d2 = new NativeDateTime();
 			$d2 = $d2->add(new DateInterval('PT' . $duration . 'S'));
 
 			$duration = $d2->diff($d1);

+ 1 - 0
src/Utility/FileLog.php

@@ -29,6 +29,7 @@ class FileLog {
 			$filename = 'custom_log';
 		}
 
+		CoreLog::drop('custom');
 		CoreLog::setConfig('custom', [
 			'className' => 'File',
 			'path' => LOGS,

+ 833 - 0
src/Utility/Mime.php

@@ -0,0 +1,833 @@
+<?php
+
+namespace Tools\Utility;
+
+use Cake\Http\Response;
+
+/**
+ * Wrapper to be able to read cake core's mime types as well as fix for missing ones
+ *
+ * @version 1.0
+ * @license MIT
+ * @author Mark Scherer
+ */
+class Mime extends Response {
+
+	/**
+	 * @var array
+	 */
+	protected array $_mimeTypesExt = [
+		'3dm' => 'x-world/x-3dmf',
+		'3dmf' => 'x-world/x-3dmf',
+		'a' => 'application/octet-stream',
+		'aab' => 'application/x-authorware-bin',
+		'aam' => 'application/x-authorware-map',
+		'aas' => 'application/x-authorware-seg',
+		'abc' => 'text/vnd.abc',
+		'acgi' => 'text/html',
+		'afl' => 'video/animaflex',
+		'aim' => 'application/x-aim',
+		'aip' => 'text/x-audiosoft-intra',
+		'ani' => 'application/x-navi-animation',
+		'aos' => 'application/x-nokia-9000-communicator-add-on-software',
+		'aps' => 'application/mime',
+		'arc' => 'application/octet-stream',
+		'arj' => 'application/arj',
+		'art' => 'image/x-jg',
+		'asf' => 'video/x-ms-asf',
+		'asm' => 'text/x-asm',
+		'asp' => 'text/asp',
+		'asx' => 'application/x-mplayer2',
+		'asx' => 'video/x-ms-asf',
+		'asx' => 'video/x-ms-asf-plugin',
+		'au' => 'audio/basic',
+		'au' => 'audio/x-au',
+		'avi' => 'application/x-troff-msvideo',
+		'avi' => 'video/avi',
+		'avi' => 'video/msvideo',
+		'avi' => 'video/x-msvideo',
+		'avs' => 'video/avs-video',
+		'bcpio' => 'application/x-bcpio',
+		'bin' => 'application/mac-binary',
+		'bin' => 'application/macbinary',
+		'bin' => 'application/octet-stream',
+		'bin' => 'application/x-binary',
+		'bin' => 'application/x-macbinary',
+		'bm' => 'image/bmp',
+		'bmp' => 'image/bmp',
+		'bmp' => 'image/x-windows-bmp',
+		'boo' => 'application/book',
+		'book' => 'application/book',
+		'boz' => 'application/x-bzip2',
+		'bsh' => 'application/x-bsh',
+		'bz' => 'application/x-bzip',
+		'bz2' => 'application/x-bzip2',
+		'c' => 'text/plain',
+		'c' => 'text/x-c',
+		'c++' => 'text/plain',
+		'cat' => 'application/vnd.ms-pki.seccat',
+		'cc' => 'text/plain',
+		'cc' => 'text/x-c',
+		'ccad' => 'application/clariscad',
+		'cco' => 'application/x-cocoa',
+		'cdf' => 'application/cdf',
+		'cdf' => 'application/x-cdf',
+		'cdf' => 'application/x-netcdf',
+		'cer' => 'application/pkix-cert',
+		'cer' => 'application/x-x509-ca-cert',
+		'cha' => 'application/x-chat',
+		'chat' => 'application/x-chat',
+		'class' => 'application/java',
+		'class' => 'application/java-byte-code',
+		'class' => 'application/x-java-class',
+		'com' => 'application/octet-stream',
+		'com' => 'text/plain',
+		'conf' => 'text/plain',
+		'cpio' => 'application/x-cpio',
+		'cpp' => 'text/x-c',
+		'cpt' => 'application/mac-compactpro',
+		'cpt' => 'application/x-compactpro',
+		'cpt' => 'application/x-cpt',
+		'crl' => 'application/pkcs-crl',
+		'crl' => 'application/pkix-crl',
+		'crt' => 'application/pkix-cert',
+		'crt' => 'application/x-x509-ca-cert',
+		'crt' => 'application/x-x509-user-cert',
+		'csh' => 'application/x-csh',
+		'csh' => 'text/x-script.csh',
+		'css' => 'text/css',
+		'cxx' => 'text/plain',
+		'dcr' => 'application/x-director',
+		'deepv' => 'application/x-deepv',
+		'def' => 'text/plain',
+		'der' => 'application/x-x509-ca-cert',
+		'dif' => 'video/x-dv',
+		'dir' => 'application/x-director',
+		'dl' => 'video/dl',
+		'doc' => 'application/msword',
+		'dot' => 'application/msword',
+		'dp' => 'application/commonground',
+		'drw' => 'application/drafting',
+		'dump' => 'application/octet-stream',
+		'dv' => 'video/x-dv',
+		'dvi' => 'application/x-dvi',
+		'dwf' => 'model/vnd.dwf',
+		'dwg' => 'application/acad',
+		'dwg' => 'image/vnd.dwg',
+		'dxf' => 'application/dxf',
+		'dxf' => 'image/vnd.dwg',
+		'dxr' => 'application/x-director',
+		'el' => 'text/x-script.elisp',
+		'elc' => 'application/x-bytecode.elisp',
+		'elc' => 'application/x-elc',
+		'env' => 'application/x-envoy',
+		'eps' => 'application/postscript',
+		'es' => 'application/x-esrehber',
+		'etx' => 'text/x-setext',
+		'evy' => 'application/envoy',
+		'evy' => 'application/x-envoy',
+		'exe' => 'application/octet-stream',
+		'f' => 'text/plain',
+		'f' => 'text/x-fortran',
+		'f77' => 'text/x-fortran',
+		'f90' => 'text/plain',
+		'f90' => 'text/x-fortran',
+		'fdf' => 'application/vnd.fdf',
+		'fif' => 'application/fractals',
+		'fif' => 'image/fif',
+		'fli' => 'video/fli',
+		'fli' => 'video/x-fli',
+		'flo' => 'image/florian',
+		'flx' => 'text/vnd.fmi.flexstor',
+		'fmf' => 'video/x-atomic3d-feature',
+		'for' => 'text/plain',
+		'fpx' => 'image/vnd.fpx',
+		'frl' => 'application/freeloader',
+		'funk' => 'audio/make',
+		'g' => 'text/plain',
+		'g3' => 'image/g3fax',
+		'gl' => 'video/gl',
+		'gsd' => 'audio/x-gsm',
+		'gsm' => 'audio/x-gsm',
+		'gsp' => 'application/x-gsp',
+		'gss' => 'application/x-gss',
+		'gtar' => 'application/x-gtar',
+		'gz' => 'application/x-compressed',
+		'gzip' => 'application/x-gzip',
+		'h' => 'text/plain',
+		'hdf' => 'application/x-hdf',
+		'help' => 'application/x-helpfile',
+		'hgl' => 'application/vnd.hp-hpgl',
+		'hh' => 'text/plain',
+		'hlb' => 'text/x-script',
+		'hlp' => 'application/hlp',
+		'hpg' => 'application/vnd.hp-hpgl',
+		'hpgl' => 'application/vnd.hp-hpgl',
+		'hqx' => 'application/binhex',
+		'hta' => 'application/hta',
+		'htc' => 'text/x-component',
+		'htmls' => 'text/html',
+		'htt' => 'text/webviewhtml',
+		'htx' => 'text/html',
+		'ico' => 'image/x-icon',
+		'ics' => 'application/ics', // important for ipad to properly display ics files
+		'ical' => 'text/calendar',
+		'idc' => 'text/plain',
+		'ief' => 'image/ief',
+		'iefs' => 'image/ief',
+		'ifb' => 'text/calendar',
+		'iges' => 'application/iges',
+		'igs' => 'application/iges',
+		'ima' => 'application/x-ima',
+		'imap' => 'application/x-httpd-imap',
+		'inf' => 'application/inf',
+		'ins' => 'application/x-internett-signup',
+		'ip' => 'application/x-ip2',
+		'isu' => 'video/x-isvideo',
+		'it' => 'audio/it',
+		'iv' => 'application/x-inventor',
+		'ivr' => 'i-world/i-vrml',
+		'ivy' => 'application/x-livescreen',
+		'jam' => 'audio/x-jam',
+		'jav' => 'text/plain',
+		'java' => 'text/plain',
+		'jcm' => 'application/x-java-commerce',
+		'jfif' => 'image/jpeg',
+		'jfif-tbnl' => 'image/jpeg',
+		'jpe' => 'image/jpeg',
+		'jpeg' => 'image/jpeg',
+		'jpg' => 'image/jpeg',
+		'jps' => 'image/x-jps',
+		'jut' => 'image/jutvision',
+		'kar' => 'audio/midi',
+		'kar' => 'music/x-karaoke',
+		'ksh' => 'application/x-ksh',
+		'ksh' => 'text/x-script.ksh',
+		'la' => 'audio/nspaudio',
+		'la' => 'audio/x-nspaudio',
+		'lam' => 'audio/x-liveaudio',
+		'latex' => 'application/x-latex',
+		'lha' => 'application/lha',
+		'lha' => 'application/octet-stream',
+		'lha' => 'application/x-lha',
+		'lhx' => 'application/octet-stream',
+		'list' => 'text/plain',
+		'lma' => 'audio/nspaudio',
+		'lma' => 'audio/x-nspaudio',
+		'log' => 'text/plain',
+		'lsp' => 'application/x-lisp',
+		'lst' => 'text/plain',
+		'lsx' => 'text/x-la-asf',
+		'ltx' => 'application/x-latex',
+		'lzh' => 'application/octet-stream',
+		'lzx' => 'application/lzx',
+		'm' => 'text/plain',
+		'm1v' => 'video/mpeg',
+		'm2a' => 'audio/mpeg',
+		'm2v' => 'video/mpeg',
+		'm3u' => 'audio/x-mpequrl',
+		'man' => 'application/x-troff-man',
+		'map' => 'application/x-navimap',
+		'mar' => 'text/plain',
+		'mbd' => 'application/mbedlet',
+		'mc$' => 'application/x-magic-cap-package-1.0',
+		'mcd' => 'application/mcad',
+		'mcf' => 'image/vasa',
+		'mcp' => 'application/netmc',
+		'me' => 'application/x-troff-me',
+		'mht' => 'message/rfc822',
+		'mhtml' => 'message/rfc822',
+		'mid' => 'audio/midi',
+		'midi' => 'audio/midi',
+		'mif' => 'application/x-frame',
+		'mime' => 'message/rfc822',
+		'mjf' => 'audio/x-vnd.audioexplosion.mjuicemediafile',
+		'mjpg' => 'video/x-motion-jpeg',
+		'mm' => 'application/base64',
+		'mm' => 'application/x-meme',
+		'mme' => 'application/base64',
+		'mod' => 'audio/mod',
+		'mod' => 'audio/x-mod',
+		'moov' => 'video/quicktime',
+		'mov' => 'video/quicktime',
+		'movie' => 'video/x-sgi-movie',
+		'mp2' => 'audio/mpeg',
+		'mp2' => 'audio/x-mpeg',
+		'mp2' => 'video/mpeg',
+		'mp2' => 'video/x-mpeg',
+		'mp2' => 'video/x-mpeq2a',
+		'mp3' => 'audio/mpeg3',
+		'mp3' => 'audio/x-mpeg-3',
+		'mp3' => 'video/mpeg',
+		'mp3' => 'video/x-mpeg',
+		'mpa' => 'audio/mpeg',
+		'mpa' => 'video/mpeg',
+		'mpc' => 'application/x-project',
+		'mpe' => 'video/mpeg',
+		'mpeg' => 'video/mpeg',
+		'mpg' => 'audio/mpeg',
+		'mpg' => 'video/mpeg',
+		'mpga' => 'audio/mpeg',
+		'mpp' => 'application/vnd.ms-project',
+		'mpt' => 'application/x-project',
+		'mpv' => 'application/x-project',
+		'mpx' => 'application/x-project',
+		'mrc' => 'application/marc',
+		'ms' => 'application/x-troff-ms',
+		'mv' => 'video/x-sgi-movie',
+		'my' => 'audio/make',
+		'mzz' => 'application/x-vnd.audioexplosion.mzz',
+		'nap' => 'image/naplps',
+		'naplps' => 'image/naplps',
+		'nc' => 'application/x-netcdf',
+		'ncm' => 'application/vnd.nokia.configuration-message',
+		'nif' => 'image/x-niff',
+		'niff' => 'image/x-niff',
+		'nix' => 'application/x-mix-transfer',
+		'nsc' => 'application/x-conference',
+		'nvd' => 'application/x-navidoc',
+		'o' => 'application/octet-stream',
+		'oda' => 'application/oda',
+		'omc' => 'application/x-omc',
+		'omcd' => 'application/x-omcdatamaker',
+		'omcr' => 'application/x-omcregerator',
+		'p' => 'text/x-pascal',
+		'p10' => 'application/pkcs10',
+		'p10' => 'application/x-pkcs10',
+		'p12' => 'application/pkcs-12',
+		'p12' => 'application/x-pkcs12',
+		'p7a' => 'application/x-pkcs7-signature',
+		'p7c' => 'application/pkcs7-mime',
+		'p7c' => 'application/x-pkcs7-mime',
+		'p7m' => 'application/pkcs7-mime',
+		'p7m' => 'application/x-pkcs7-mime',
+		'p7r' => 'application/x-pkcs7-certreqresp',
+		'p7s' => 'application/pkcs7-signature',
+		'part' => 'application/pro_eng',
+		'pas' => 'text/pascal',
+		'pbm' => 'image/x-portable-bitmap',
+		'pcl' => 'application/vnd.hp-pcl',
+		'pcl' => 'application/x-pcl',
+		'pct' => 'image/x-pict',
+		'pcx' => 'image/x-pcx',
+		'pdb' => 'chemical/x-pdb',
+		'pdf' => 'application/pdf',
+		'pfunk' => 'audio/make',
+		'pfunk' => 'audio/make.my.funk',
+		'pgm' => 'image/x-portable-graymap',
+		'pgm' => 'image/x-portable-greymap',
+		'pic' => 'image/pict',
+		'pict' => 'image/pict',
+		'pkg' => 'application/x-newton-compatible-pkg',
+		'pko' => 'application/vnd.ms-pki.pko',
+		'pl' => 'text/plain',
+		'pl' => 'text/x-script.perl',
+		'plx' => 'application/x-pixclscript',
+		'pm' => 'image/x-xpixmap',
+		'pm' => 'text/x-script.perl-module',
+		'pm4' => 'application/x-pagemaker',
+		'pm5' => 'application/x-pagemaker',
+		'png' => 'image/png',
+		'pnm' => 'application/x-portable-anymap',
+		'pnm' => 'image/x-portable-anymap',
+		'pot' => 'application/mspowerpoint',
+		'pot' => 'application/vnd.ms-powerpoint',
+		'pov' => 'model/x-pov',
+		'ppa' => 'application/vnd.ms-powerpoint',
+		'ppm' => 'image/x-portable-pixmap',
+		'pps' => 'application/mspowerpoint',
+		'pps' => 'application/vnd.ms-powerpoint',
+		'ppt' => 'application/mspowerpoint',
+		'ppt' => 'application/powerpoint',
+		'ppt' => 'application/vnd.ms-powerpoint',
+		'ppt' => 'application/x-mspowerpoint',
+		'ppz' => 'application/mspowerpoint',
+		'pre' => 'application/x-freelance',
+		'prt' => 'application/pro_eng',
+		'ps' => 'application/postscript',
+		'psd' => 'application/octet-stream',
+		'pvu' => 'paleovu/x-pv',
+		'pwz' => 'application/vnd.ms-powerpoint',
+		'py' => 'text/x-script.phyton',
+		'pyc' => 'applicaiton/x-bytecode.python',
+		'qcp' => 'audio/vnd.qcelp',
+		'qd3' => 'x-world/x-3dmf',
+		'qd3d' => 'x-world/x-3dmf',
+		'qif' => 'image/x-quicktime',
+		'qt' => 'video/quicktime',
+		'qtc' => 'video/x-qtc',
+		'qti' => 'image/x-quicktime',
+		'qtif' => 'image/x-quicktime',
+		'ra' => 'audio/x-pn-realaudio',
+		'ra' => 'audio/x-pn-realaudio-plugin',
+		'ra' => 'audio/x-realaudio',
+		'ram' => 'audio/x-pn-realaudio',
+		'ras' => 'application/x-cmu-raster',
+		'ras' => 'image/cmu-raster',
+		'ras' => 'image/x-cmu-raster',
+		'rast' => 'image/cmu-raster',
+		'rexx' => 'text/x-script.rexx',
+		'rf' => 'image/vnd.rn-realflash',
+		'rgb' => 'image/x-rgb',
+		'rm' => 'application/vnd.rn-realmedia',
+		'rm' => 'audio/x-pn-realaudio',
+		'rmi' => 'audio/mid',
+		'rmm' => 'audio/x-pn-realaudio',
+		'rmp' => 'audio/x-pn-realaudio',
+		'rmp' => 'audio/x-pn-realaudio-plugin',
+		'rng' => 'application/ringing-tones',
+		'rng' => 'application/vnd.nokia.ringing-tone',
+		'rnx' => 'application/vnd.rn-realplayer',
+		'roff' => 'application/x-troff',
+		'rp' => 'image/vnd.rn-realpix',
+		'rpm' => 'audio/x-pn-realaudio-plugin',
+		'rt' => 'text/richtext',
+		'rt' => 'text/vnd.rn-realtext',
+		'rtf' => 'application/rtf',
+		'rtf' => 'application/x-rtf',
+		'rtf' => 'text/richtext',
+		'rtx' => 'application/rtf',
+		'rtx' => 'text/richtext',
+		'rv' => 'video/vnd.rn-realvideo',
+		's' => 'text/x-asm',
+		's3m' => 'audio/s3m',
+		'saveme' => 'application/octet-stream',
+		'sbk' => 'application/x-tbook',
+		'scm' => 'application/x-lotusscreencam',
+		'scm' => 'text/x-script.guile',
+		'scm' => 'text/x-script.scheme',
+		'scm' => 'video/x-scm',
+		'sdml' => 'text/plain',
+		'sdp' => 'application/sdp',
+		'sdp' => 'application/x-sdp',
+		'sdr' => 'application/sounder',
+		'sea' => 'application/sea',
+		'sea' => 'application/x-sea',
+		'set' => 'application/set',
+		'sgm' => 'text/sgml',
+		'sgm' => 'text/x-sgml',
+		'sgml' => 'text/sgml',
+		'sgml' => 'text/x-sgml',
+		'sh' => 'application/x-bsh',
+		'sh' => 'application/x-sh',
+		'sh' => 'application/x-shar',
+		'sh' => 'text/x-script.sh',
+		'shar' => 'application/x-bsh',
+		'shar' => 'application/x-shar',
+		'shtml' => 'text/html',
+		'sid' => 'audio/x-psid',
+		'sit' => 'application/x-sit',
+		'sit' => 'application/x-stuffit',
+		'skd' => 'application/x-koan',
+		'skm' => 'application/x-koan',
+		'skp' => 'application/x-koan',
+		'skt' => 'application/x-koan',
+		'sl' => 'application/x-seelogo',
+		'smi' => 'application/smil',
+		'smil' => 'application/smil',
+		'snd' => 'audio/basic',
+		'snd' => 'audio/x-adpcm',
+		'sol' => 'application/solids',
+		'spc' => 'application/x-pkcs7-certificates',
+		'spc' => 'text/x-speech',
+		'spl' => 'application/futuresplash',
+		'spr' => 'application/x-sprite',
+		'sprite' => 'application/x-sprite',
+		'src' => 'application/x-wais-source',
+		'ssi' => 'text/x-server-parsed-html',
+		'ssm' => 'application/streamingmedia',
+		'sst' => 'application/vnd.ms-pki.certstore',
+		'step' => 'application/step',
+		'stl' => 'application/sla',
+		'stl' => 'application/vnd.ms-pki.stl',
+		'stl' => 'application/x-navistyle',
+		'stp' => 'application/step',
+		'sv4cpio' => 'application/x-sv4cpio',
+		'sv4crc' => 'application/x-sv4crc',
+		'svf' => 'image/vnd.dwg',
+		'svf' => 'image/x-dwg',
+		'svr' => 'application/x-world',
+		'svr' => 'x-world/x-svr',
+		'swf' => 'application/x-shockwave-flash',
+		't' => 'application/x-troff',
+		'talk' => 'text/x-speech',
+		'tar' => 'application/x-tar',
+		'tbk' => 'application/toolbook',
+		'tbk' => 'application/x-tbook',
+		'tcl' => 'application/x-tcl',
+		'tcl' => 'text/x-script.tcl',
+		'tcsh' => 'text/x-script.tcsh',
+		'tex' => 'application/x-tex',
+		'texi' => 'application/x-texinfo',
+		'texinfo' => 'application/x-texinfo',
+		'text' => 'application/plain',
+		'text' => 'text/plain',
+		'tgz' => 'application/gnutar',
+		'tgz' => 'application/x-compressed',
+		'tif' => 'image/tiff',
+		'tif' => 'image/x-tiff',
+		'tiff' => 'image/tiff',
+		'tiff' => 'image/x-tiff',
+		'tr' => 'application/x-troff',
+		'tsi' => 'audio/tsp-audio',
+		'tsp' => 'application/dsptype',
+		'tsp' => 'audio/tsplayer',
+		'tsv' => 'text/tab-separated-values',
+		'turbot' => 'image/florian',
+		'txt' => 'text/plain',
+		'uil' => 'text/x-uil',
+		'uni' => 'text/uri-list',
+		'unis' => 'text/uri-list',
+		'unv' => 'application/i-deas',
+		'uri' => 'text/uri-list',
+		'uris' => 'text/uri-list',
+		'ustar' => 'application/x-ustar',
+		'ustar' => 'multipart/x-ustar',
+		'uu' => 'application/octet-stream',
+		'uu' => 'text/x-uuencode',
+		'uue' => 'text/x-uuencode',
+		'vcd' => 'application/x-cdlink',
+		'vcs' => 'text/x-vcalendar',
+		'vda' => 'application/vda',
+		'vdo' => 'video/vdo',
+		'vew' => 'application/groupwise',
+		'viv' => 'video/vivo',
+		'viv' => 'video/vnd.vivo',
+		'vivo' => 'video/vivo',
+		'vivo' => 'video/vnd.vivo',
+		'vmd' => 'application/vocaltec-media-desc',
+		'vmf' => 'application/vocaltec-media-file',
+		'voc' => 'audio/voc',
+		'voc' => 'audio/x-voc',
+		'vos' => 'video/vosaic',
+		'vox' => 'audio/voxware',
+		'vqe' => 'audio/x-twinvq-plugin',
+		'vqf' => 'audio/x-twinvq',
+		'vql' => 'audio/x-twinvq-plugin',
+		'vrml' => 'application/x-vrml',
+		'vrml' => 'model/vrml',
+		'vrml' => 'x-world/x-vrml',
+		'vrt' => 'x-world/x-vrt',
+		'vsd' => 'application/x-visio',
+		'vst' => 'application/x-visio',
+		'vsw' => 'application/x-visio',
+		'w60' => 'application/wordperfect6.0',
+		'w61' => 'application/wordperfect6.1',
+		'w6w' => 'application/msword',
+		'wav' => 'audio/wav',
+		'wav' => 'audio/x-wav',
+		'wb1' => 'application/x-qpro',
+		'wbmp' => 'image/vnd.wap.wbmp',
+		'web' => 'application/vnd.xara',
+		'wiz' => 'application/msword',
+		'wk1' => 'application/x-123',
+		'wmf' => 'windows/metafile',
+		'wml' => 'text/vnd.wap.wml',
+		'wmlc' => 'application/vnd.wap.wmlc',
+		'wmls' => 'text/vnd.wap.wmlscript',
+		'wmlsc' => 'application/vnd.wap.wmlscriptc',
+		'word' => 'application/msword',
+		'wp' => 'application/wordperfect',
+		'wp5' => 'application/wordperfect',
+		'wp5' => 'application/wordperfect6.0',
+		'wp6' => 'application/wordperfect',
+		'wpd' => 'application/wordperfect',
+		'wpd' => 'application/x-wpwin',
+		'wq1' => 'application/x-lotus',
+		'wri' => 'application/mswrite',
+		'wri' => 'application/x-wri',
+		'wrl' => 'application/x-world',
+		'wrl' => 'model/vrml',
+		'wrl' => 'x-world/x-vrml',
+		'wrz' => 'model/vrml',
+		'wrz' => 'x-world/x-vrml',
+		'wsc' => 'text/scriplet',
+		'wsrc' => 'application/x-wais-source',
+		'wtk' => 'application/x-wintalk',
+		'xbm' => 'image/x-xbitmap',
+		'xbm' => 'image/x-xbm',
+		'xbm' => 'image/xbm',
+		'xdr' => 'video/x-amt-demorun',
+		'xgz' => 'xgl/drawing',
+		'xif' => 'image/vnd.xiff',
+		'xl' => 'application/excel',
+		'xla' => 'application/excel',
+		'xla' => 'application/x-excel',
+		'xla' => 'application/x-msexcel',
+		'xlb' => 'application/excel',
+		'xlb' => 'application/vnd.ms-excel',
+		'xlb' => 'application/x-excel',
+		'xlc' => 'application/excel',
+		'xlc' => 'application/vnd.ms-excel',
+		'xlc' => 'application/x-excel',
+		'xld' => 'application/excel',
+		'xld' => 'application/x-excel',
+		'xlk' => 'application/excel',
+		'xlk' => 'application/x-excel',
+		'xll' => 'application/excel',
+		'xll' => 'application/vnd.ms-excel',
+		'xll' => 'application/x-excel',
+		'xlm' => 'application/excel',
+		'xlm' => 'application/vnd.ms-excel',
+		'xlm' => 'application/x-excel',
+		'xls' => 'application/excel',
+		'xls' => 'application/vnd.ms-excel',
+		'xls' => 'application/x-excel',
+		'xls' => 'application/x-msexcel',
+		'xlt' => 'application/excel',
+		'xlt' => 'application/x-excel',
+		'xlv' => 'application/excel',
+		'xlv' => 'application/x-excel',
+		'xlw' => 'application/excel',
+		'xlw' => 'application/vnd.ms-excel',
+		'xlw' => 'application/x-excel',
+		'xlw' => 'application/x-msexcel',
+		'xm' => 'audio/xm',
+		'xml' => 'application/xml',
+		'xml' => 'text/xml',
+		'xmz' => 'xgl/movie',
+		'xpix' => 'application/x-vnd.ls-xpix',
+		'xpm' => 'image/x-xpixmap',
+		'xpm' => 'image/xpm',
+		'x-png' => 'image/png',
+		'xsr' => 'video/x-amt-showrun',
+		'xwd' => 'image/x-xwd',
+		'xwd' => 'image/x-xwindowdump',
+		'xyz' => 'chemical/x-pdb',
+		'z' => ['application/x-compress', 'application/x-compressed'],
+		'zip' => 'application/x-compressed',
+		'zip' => 'application/x-zip-compressed',
+		'zip' => 'application/zip',
+		'zip' => 'multipart/x-zip',
+		'zoo' => 'application/octet-stream',
+		'zsh' => 'text/x-script.zsh',
+		'txt' => 'text/plain',
+		'php' => 'application/x-httpd-php',
+		'phps' => 'application/x-httpd-phps',
+		'css' => 'text/css',
+		'js' => 'application/javascript',
+		'json' => 'application/json',
+		'xml' => 'application/xml',
+		'flv' => 'video/x-flv',
+		'asc' => 'text/plain',
+		'atom' => 'application/atom+xml',
+		'bcpio' => 'application/x-bcpio',
+		'png' => 'image/png',
+		'jpe' => 'image/jpeg',
+		'jpeg' => 'image/jpeg',
+		'jpg' => 'image/jpeg',
+		'gif' => 'image/gif',
+		'bmp' => 'image/bmp',
+		'ico' => 'image/vnd.microsoft.icon',
+		'tiff' => 'image/tiff',
+		'tif' => 'image/tiff',
+		'svg' => 'image/svg+xml',
+		'svgz' => 'image/svg+xml',
+		'zip' => 'application/zip',
+		'rar' => 'application/x-rar-compressed',
+		'exe' => 'application/x-msdownload',
+		'msi' => 'application/x-msdownload',
+		'cab' => 'application/vnd.ms-cab-compressed',
+		'mp3' => 'audio/mpeg',
+		'qt' => 'video/quicktime',
+		'mov' => 'video/quicktime',
+		'au' => 'audio/basic',
+		'avi' => 'video/x-msvideo',
+		'pdf' => 'application/pdf',
+		'psd' => 'image/vnd.adobe.photoshop',
+		'ai' => 'application/postscript',
+		'eps' => 'application/postscript',
+		'ps' => 'application/postscript',
+		'aif' => 'audio/x-aiff',
+		'aifc' => 'audio/x-aiff',
+		'aiff' => 'audio/x-aiff',
+		'doc' => 'application/msword',
+		'rtf' => 'application/rtf',
+		'xls' => 'application/vnd.ms-excel',
+		'ppt' => 'application/vnd.ms-powerpoint',
+		'odt' => 'application/vnd.oasis.opendocument.text',
+		'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+		'swf' => ['application/x-shockwave-flash', 'application/x-shockwave-flash2-preview', 'application/futuresplash', 'image/vnd.rn-realflash'],
+	];
+
+	protected ?array $_mimeTypesTmp = null;
+
+	/**
+	 * Override constructor
+	 *
+	 * @param array<string, mixed> $options
+	 */
+	public function __construct(array $options = []) {
+	}
+
+	/**
+	 * Get all mime types that are supported right now
+	 *
+	 * @param bool $coreHasPrecedence
+	 * @return array
+	 */
+	public function mimeTypes($coreHasPrecedence = false) {
+		if ($coreHasPrecedence) {
+			return $this->_mimeTypes += $this->_mimeTypesExt;
+		}
+
+		return $this->_mimeTypesExt += $this->_mimeTypes;
+	}
+
+	/**
+	 * Returns the primary mime type definition for an alias/extension.
+	 *
+	 * e.g `getMimeType('pdf'); // returns 'application/pdf'`
+	 *
+	 * @param string $alias the content type alias to map
+	 * @param bool $primaryOnly
+	 * @param bool $coreHasPrecedence
+	 * @return array|string|null Mapped mime type or null if $alias is not mapped
+	 */
+	public function getMimeTypeByAlias(
+		string $alias,
+		bool $primaryOnly = true,
+		bool $coreHasPrecedence = false,
+	) {
+		if (empty($this->_mimeTypeTmp)) {
+			$this->_mimeTypesTmp = $this->mimeTypes($coreHasPrecedence);
+		}
+		if (!isset($this->_mimeTypesTmp[$alias])) {
+			return null;
+		}
+		$mimeType = $this->_mimeTypesTmp[$alias];
+		if ($primaryOnly && is_array($mimeType)) {
+			$mimeType = array_shift($mimeType);
+		}
+
+		return $mimeType;
+	}
+
+	/**
+	 * Maps a content-type back to an alias
+	 *
+	 * e.g `mapType('application/pdf'); // returns 'pdf'`
+	 *
+	 * @param array|string $ctype Either a string content type to map, or an array of types.
+	 * @return array|string|null Aliases for the types provided.
+	 */
+	public function mapType(array|string $ctype): array|string|null {
+		return parent::mapType($ctype);
+	}
+
+	/**
+	 * Retrieve the corresponding MIME type, if one exists
+	 *
+	 * @param string|null $file File Name (relative location such as "image_test.jpg" or full "http://site.com/path/to/image_test.jpg")
+	 * @return string MIMEType - The type of the file passed in the argument
+	 */
+	public function detectMimeType($file = null) {
+		// Attempts to retrieve file info from FINFO
+		// If FINFO functions are not available then try to retrieve MIME type from pre-defined MIMEs
+		// If MIME type doesn't exist, then try (as a last resort) to use the (deprecated) mime_content_type function
+		// If all else fails, just return application/octet-stream
+		if (!function_exists('finfo_open')) {
+			if (function_exists('mime_content_type')) {
+				$type = mime_content_type($file);
+				if (!empty($type)) {
+					return $type;
+				}
+			}
+			$extension = $this->_getExtension($file);
+			$mimeType = $this->getMimeTypeByAlias($extension);
+			if ($mimeType) {
+				return $mimeType;
+			}
+
+			return 'application/octet-stream';
+		}
+
+		return $this->_detectMimeType($file);
+	}
+
+	/**
+	 * Utility::getMimeType()
+	 *
+	 * @param string $file File
+	 * @return string Mime type
+	 */
+	public static function _detectMimeType($file) {
+		if (!function_exists('finfo_open')) {
+			//throw new InternalErrorException('finfo_open() required - please enable');
+		}
+
+		// Treat non local files differently
+		$pattern = '~^https?://~i';
+		if (preg_match($pattern, $file)) {
+			// @codingStandardsIgnoreStart
+			$headers = @get_headers($file);
+			// @codingStandardsIgnoreEnd
+			if (!$headers || !preg_match("|\b200\b|", $headers[0])) {
+				return '';
+			}
+			foreach ($headers as $header) {
+				if (strpos($header, 'Content-Type:') === 0) {
+					return trim(substr($header, 13));
+				}
+			}
+
+			return '';
+		}
+
+		if (!is_file($file)) {
+			return '';
+		}
+
+		$finfo = finfo_open(FILEINFO_MIME);
+		$mimetype = finfo_file($finfo, $file);
+		$pos = strpos($mimetype, ';');
+		if ($pos !== false) {
+			$mimetype = substr($mimetype, 0, $pos);
+		}
+		if ($mimetype) {
+			return $mimetype;
+		}
+		$extension = static::_getExtension($file);
+		$mimeType = static::getMimeTypeByAlias($extension);
+		if ($mimeType) {
+			return $mimeType;
+		}
+
+		return 'application/octet-stream';
+	}
+
+	/**
+	 * Get encoding.
+	 *
+	 * @param string|null $file
+	 * @param string $default
+	 * @return string
+	 */
+	public static function getEncoding(?string $file = null, string $default = 'utf-8'): string {
+		if (!function_exists('finfo_open')) {
+			return $default;
+		}
+		$finfo = finfo_open(FILEINFO_MIME_ENCODING);
+		$encoding = finfo_file($finfo, $file);
+		finfo_close($finfo);
+		if ($encoding !== false) {
+			return $encoding;
+		}
+
+		return $default;
+	}
+
+	/**
+	 * Gets the file extension from a string
+	 *
+	 * @param string $file The full file name
+	 * @return string The file extension
+	 */
+	protected static function _getExtension(string $file): string {
+		$pieces = explode('.', $file);
+		$ext = strtolower(array_pop($pieces));
+
+		return $ext;
+	}
+
+}

+ 3 - 3
src/View/Helper/CommonHelper.php

@@ -84,7 +84,7 @@ class CommonHelper extends Helper {
 		}
 
 		$content = (array)$content;
-		$return = '<meta name="' . $name . '" content="' . implode(', ', $content) . '" />';
+		$return = '<meta name="' . $name . '" content="' . implode(', ', $content) . '">';
 
 		return $return;
 	}
@@ -191,7 +191,7 @@ class CommonHelper extends Helper {
 	 */
 	public function metaRss($url, ?string $title = null): string {
 		$tags = [
-			'meta' => '<link rel="alternate" type="application/rss+xml" title="%s" href="%s"/>',
+			'meta' => '<link rel="alternate" type="application/rss+xml" title="%s" href="%s">',
 		];
 		if (!$title) {
 			$title = __d('tools', 'Subscribe to this feed');
@@ -212,7 +212,7 @@ class CommonHelper extends Helper {
 	 */
 	public function metaEquiv(string $type, string $value, bool $escape = true): string {
 		$tags = [
-			'meta' => '<meta http-equiv="%s"%s/>',
+			'meta' => '<meta http-equiv="%s"%s>',
 		];
 		if ($escape) {
 			$value = h($value);

+ 1 - 1
src/View/Helper/IconHelper.php

@@ -26,7 +26,7 @@ class IconHelper extends Helper {
 	/**
 	 * @var \Tools\View\Icon\IconCollection
 	 */
-	protected $collection;
+	protected IconCollection $collection;
 
 	/**
 	 * @var array

+ 1 - 1
templates/Admin/Format/icons.php

@@ -14,7 +14,7 @@
 		?>
 		<ul>
 		<?php foreach ($icons as $icon => $class) { ?>
-			<li><?php echo $this->Format->icon($icon); ?> - <?php echo h($icon)?> (<?php echo h($class)?>)</li>
+			<li><?php echo $this->Icon->render($icon); ?> - <?php echo h($icon)?> (<?php echo h($class)?>)</li>
 		<?php } ?>
 		</ul>
 

+ 0 - 119
tests/TestCase/Auth/MultiColumnAuthenticateTest.php

@@ -1,119 +0,0 @@
-<?php
-
-namespace Tools\Test\TestCase\Auth;
-
-use Cake\Http\ServerRequest;
-use Cake\I18n\Time;
-use Cake\TestSuite\TestCase;
-use Tools\Auth\MultiColumnAuthenticate;
-
-class MultiColumnAuthenticateTest extends TestCase {
-
-	/**
-	 * @var array
-	 */
-	protected array $fixtures = [
-		'plugin.Tools.MultiColumnUsers',
-	];
-
-	/**
-	 * @var \Tools\Auth\MultiColumnAuthenticate
-	 */
-	protected $auth;
-
-	/**
-	 * @var \Cake\Http\Response
-	 */
-	protected $response;
-
-	/**
-	 * @var \Cake\Controller\ComponentRegistry
-	 */
-	protected $registry;
-
-	/**
-	 * @return void
-	 */
-	public function setUp(): void {
-		parent::setUp();
-
-		$this->registry = $this->getMockBuilder('Cake\Controller\ComponentRegistry')->disableOriginalConstructor()->getMock();
-		$this->auth = new MultiColumnAuthenticate($this->registry, [
-			'fields' => ['username' => 'user_name', 'password' => 'password'],
-			'userModel' => 'MultiColumnUsers',
-			'columns' => ['user_name', 'email'],
-		]);
-
-		$password = password_hash('password', PASSWORD_DEFAULT);
-		$MultiColumnUsers = $this->getTableLocator()->get('MultiColumnUsers');
-		$MultiColumnUsers->updateAll(['password' => $password], []);
-
-		$this->response = $this->getMockBuilder('Cake\Http\Response')->getMock();
-	}
-
-	/**
-	 * @return void
-	 */
-	public function testAuthenticateEmailOrUsername() {
-		$request = new ServerRequest(['url' => 'posts/index']);
-		$expected = [
-			'id' => 1,
-			'user_name' => 'mariano',
-			'email' => 'mariano@example.com',
-			'token' => '12345',
-			'created' => new Time('2007-03-17 01:16:23'),
-			'updated' => new Time('2007-03-17 01:18:31'),
-		];
-
-		$request = $request->withData('user_name', 'mariano')->withData('password', 'password');
-		$result = $this->auth->authenticate($request, $this->response);
-		$this->assertEquals($expected, $result);
-
-		$request = $request->withData('user_name', 'mariano@example.com')->withData('password', 'password');
-		$result = $this->auth->authenticate($request, $this->response);
-		$this->assertEquals($expected, $result);
-	}
-	/**
-	 * @return void
-	 */
-	public function testAuthenticateNoUsername() {
-		$request = new ServerRequest(['url' => 'posts/index']);
-		$request = $request->withData('password', 'foobar');
-		$this->assertFalse($this->auth->authenticate($request, $this->response));
-	}
-
-	/**
-	 * @return void
-	 */
-	public function testAuthenticateNoPassword() {
-		$request = new ServerRequest(['url' => 'posts/index']);
-		$request = $request->withData('user_name', 'mariano');
-		$this->assertFalse($this->auth->authenticate($request, $this->response));
-
-		$request = $request->withData('user_name', 'mariano@example.com');
-		$this->assertFalse($this->auth->authenticate($request, $this->response));
-	}
-
-	/**
-	 * @return void
-	 */
-	public function testAuthenticateInjection() {
-		$request = new ServerRequest(['url' => 'posts/index']);
-		$request = $request->withData('user_name', '> 1')->withData('password', "' OR 1 = 1");
-		$this->assertFalse($this->auth->authenticate($request, $this->response));
-	}
-
-	/**
-	 * test scope failure.
-	 *
-	 * @return void
-	 */
-	public function testAuthenticateScopeFail() {
-		$this->auth->setConfig('scope', ['user_name' => 'nate']);
-		$request = new ServerRequest(['url' => 'posts/index']);
-		$request = $request->withData('user_name', 'mariano')->withData('password', 'password');
-
-		$this->assertFalse($this->auth->authenticate($request, $this->response));
-	}
-
-}

+ 7 - 0
tests/TestCase/Controller/Admin/FormatControllerTest.php

@@ -2,8 +2,10 @@
 
 namespace Tools\Test\TestCase\Controller\Admin;
 
+use Cake\Core\Configure;
 use Cake\TestSuite\IntegrationTestTrait;
 use Shim\TestSuite\TestCase;
+use Tools\View\Icon\BootstrapIcon;
 
 /**
  * @uses \Tools\Controller\Admin\FormatController
@@ -18,6 +20,11 @@ class FormatControllerTest extends TestCase {
 	public function testIcons() {
 		$this->disableErrorHandlerMiddleware();
 
+		Configure::write('Icon', [
+			'sets' => [
+				'bs' => BootstrapIcon::class,
+			],
+		]);
 		$this->get(['prefix' => 'Admin', 'plugin' => 'Tools', 'controller' => 'Format', 'action' => 'icons']);
 
 		$this->assertResponseCode(200);

+ 2 - 2
tests/TestCase/Controller/Component/MobileComponentTest.php

@@ -24,12 +24,12 @@ class MobileComponentTest extends TestCase {
 	/**
 	 * @var \Cake\Event\Event
 	 */
-	protected $event;
+	protected Event $event;
 
 	/**
 	 * @var \TestApp\Controller\MobileComponentTestController
 	 */
-	protected $Controller;
+	protected MobileComponentTestController $Controller;
 
 	/**
 	 * SetUp method

+ 7 - 7
tests/TestCase/Controller/Component/UrlComponentTest.php

@@ -16,12 +16,12 @@ class UrlComponentTest extends TestCase {
 	/**
 	 * @var \Cake\Event\Event
 	 */
-	protected $event;
+	protected Event $event;
 
 	/**
 	 * @var \TestApp\Controller\UrlComponentTestController
 	 */
-	protected $Controller;
+	protected UrlComponentTestController $Controller;
 
 	/**
 	 * @return void
@@ -114,13 +114,13 @@ class UrlComponentTest extends TestCase {
 			->withParam('plugin', 'Foo');
 		$this->Controller->setRequest($request);
 
-		Router::reload();
-		Router::defaultRouteClass(DashedRoute::class);
-		Router::connect('/:controller/:action/*');
-		Router::plugin('Foo', function (RouteBuilder $routes): void {
+		$builder = Router::createRouteBuilder('/');
+		$builder->setRouteClass(DashedRoute::class);
+		$builder->connect('/:controller/:action/*');
+		$builder->plugin('Foo', function (RouteBuilder $routes): void {
 			$routes->fallbacks(DashedRoute::class);
 		});
-		Router::prefix('Admin', function (RouteBuilder $routes): void {
+		$builder->prefix('Admin', function (RouteBuilder $routes): void {
 			$routes->plugin('Foo', function (RouteBuilder $routes): void {
 				$routes->fallbacks(DashedRoute::class);
 			});

+ 2 - 2
tests/TestCase/Controller/ControllerTest.php

@@ -51,8 +51,8 @@ class ControllerTest extends TestCase {
 		$count = $ToolsUser->find()->count();
 		$this->assertTrue($count > 3);
 
-		$this->Controller->loadModel('ToolsUsers');
-		$result = $this->Controller->paginate('ToolsUsers');
+		$toolsUsers = $this->Controller->fetchTable('ToolsUsers');
+		$result = $this->Controller->paginate($toolsUsers);
 		$this->assertSame(2, count($result->toArray()));
 	}
 

+ 1 - 1
tests/TestCase/ErrorHandler/ErrorLoggerTest.php

@@ -13,7 +13,7 @@ class ErrorLoggerTest extends TestCase {
 	use TestTrait;
 
 	/**
-	 * @var \Tools\Error\ErrorHandler
+	 * @var \Tools\Error\ExceptionTrap
 	 */
 	protected $errorLogger;
 

+ 6 - 6
tests/TestCase/ErrorHandler/ErrorHandlerTest.php

@@ -3,15 +3,15 @@
 namespace Tools\Test\TestCase\ErrorHandler;
 
 use Shim\TestSuite\TestCase;
-use Tools\Error\ErrorHandler;
 use Tools\Error\ErrorLogger;
+use Tools\Error\ExceptionTrap;
 
-class ErrorHandlerTest extends TestCase {
+class ExceptionTrapTest extends TestCase {
 
 	/**
-	 * @var \Tools\Error\ErrorHandler
+	 * @var \Tools\Error\ExceptionTrap
 	 */
-	protected $errorHandler;
+	protected ExceptionTrap $exceptionTrap;
 
 	/**
 	 * @return void
@@ -19,14 +19,14 @@ class ErrorHandlerTest extends TestCase {
 	public function setUp(): void {
 		parent::setUp();
 
-		$this->errorHandler = new ErrorHandler();
+		$this->exceptionTrap = new ExceptionTrap();
 	}
 
 	/**
 	 * @return void
 	 */
 	public function testLogger(): void {
-		$result = $this->errorHandler->getConfig('errorLogger');
+		$result = $this->exceptionTrap->getConfig('logger');
 		$this->assertSame(ErrorLogger::class, $result);
 	}
 

+ 1 - 1
tests/TestCase/Mailer/MessageTest.php

@@ -12,7 +12,7 @@ class MessageTest extends TestCase {
 	/**
 	 * @var \Tools\Mailer\Message
 	 */
-	protected $message;
+	protected MailerMessage $message;
 
 	/**
 	 * setUp

+ 16 - 16
tests/TestCase/Model/Behavior/ConfirmableBehaviorTest.php

@@ -31,16 +31,16 @@ class ConfirmableBehaviorTest extends TestCase {
 	 * @return void
 	 */
 	public function testBasicValidation() {
-		$this->Articles = $this->getTableLocator()->get('SluggedArticles');
-		$this->Articles->addBehavior('Tools.Confirmable');
+		$Articles = $this->getTableLocator()->get('SluggedArticles');
+		$Articles->addBehavior('Tools.Confirmable');
 
-		$animal = $this->Articles->newEmptyEntity();
+		$animal = $Articles->newEmptyEntity();
 
 		$data = [
 			'name' => 'FooBar',
 			'confirm' => '0',
 		];
-		$animal = $this->Articles->patchEntity($animal, $data);
+		$animal = $Articles->patchEntity($animal, $data);
 		$this->assertNotEmpty($animal->getErrors());
 		$this->assertSame(['confirm' => ['notBlank' => __d('tools', 'Please confirm the checkbox')]], $animal->getErrors());
 
@@ -48,7 +48,7 @@ class ConfirmableBehaviorTest extends TestCase {
 			'name' => 'FooBar',
 			'confirm' => '1',
 		];
-		$animal = $this->Articles->patchEntity($animal, $data);
+		$animal = $Articles->patchEntity($animal, $data);
 		$this->assertEmpty($animal->getErrors());
 	}
 
@@ -56,9 +56,9 @@ class ConfirmableBehaviorTest extends TestCase {
 	 * @return void
 	 */
 	public function testValidationThatHasBeenModifiedBefore() {
-		$this->Articles = $this->getTableLocator()->get('SluggedArticles');
+		$Articles = $this->getTableLocator()->get('SluggedArticles');
 		/*
-		$this->Articles->validator()->add('confirm', 'notBlank', [
+		$Articles->validator()->add('confirm', 'notBlank', [
 				'rule' => function ($value, $context) {
 					return !empty($value);
 				},
@@ -67,18 +67,18 @@ class ConfirmableBehaviorTest extends TestCase {
 				'allowEmpty' => false,
 				'last' => true,
 			]);
-		$this->Articles->validator()->remove('confirm');
+		$Articles->validator()->remove('confirm');
 		*/
 
-		$this->Articles->addBehavior('Tools.Confirmable');
+		$Articles->addBehavior('Tools.Confirmable');
 
-		$animal = $this->Articles->newEmptyEntity();
+		$animal = $Articles->newEmptyEntity();
 
 		$data = [
 			'name' => 'FooBar',
 			'confirm' => '0',
 		];
-		$animal = $this->Articles->patchEntity($animal, $data);
+		$animal = $Articles->patchEntity($animal, $data);
 		$this->assertNotEmpty($animal->getErrors());
 
 		$this->assertSame(['confirm' => ['notBlank' => __d('tools', 'Please confirm the checkbox')]], $animal->getErrors());
@@ -87,7 +87,7 @@ class ConfirmableBehaviorTest extends TestCase {
 			'name' => 'FooBar',
 			'confirm' => '1',
 		];
-		$animal = $this->Articles->patchEntity($animal, $data);
+		$animal = $Articles->patchEntity($animal, $data);
 		$this->assertEmpty($animal->getErrors());
 	}
 
@@ -95,14 +95,14 @@ class ConfirmableBehaviorTest extends TestCase {
 	 * @return void
 	 */
 	public function testValidationFieldMissing() {
-		$this->Articles = $this->getTableLocator()->get('SluggedArticles');
-		$this->Articles->addBehavior('Tools.Confirmable');
+		$Articles = $this->getTableLocator()->get('SluggedArticles');
+		$Articles->addBehavior('Tools.Confirmable');
 
-		$animal = $this->Articles->newEmptyEntity();
+		$animal = $Articles->newEmptyEntity();
 		$data = [
 			'name' => 'FooBar',
 		];
-		$animal = $this->Articles->patchEntity($animal, $data);
+		$animal = $Articles->patchEntity($animal, $data);
 		$this->assertSame(['confirm' => ['_required' => 'This field is required']], $animal->getErrors());
 	}
 

+ 7 - 6
tests/TestCase/Model/Behavior/PasswordableBehaviorTest.php

@@ -2,9 +2,12 @@
 
 namespace Tools\Test\TestCase\Model\Behavior;
 
-use Cake\Auth\PasswordHasherFactory;
 use Cake\Core\Configure;
+use Cake\ORM\Table;
 use Shim\TestSuite\TestCase;
+use TestApp\Model\Table\ToolsUsersTable;
+use Tools\Auth\AbstractPasswordHasher;
+use Tools\Auth\PasswordHasherFactory;
 
 class PasswordableBehaviorTest extends TestCase {
 
@@ -19,12 +22,12 @@ class PasswordableBehaviorTest extends TestCase {
 	/**
 	 * @var \TestApp\Model\Table\ToolsUsersTable
 	 */
-	protected $Users;
+	protected Table|ToolsUsersTable $Users;
 
 	/**
-	 * @var \Cake\Auth\AbstractPasswordHasher
+	 * @var \Tools\Auth\AbstractPasswordHasher
 	 */
-	protected $hasher;
+	protected AbstractPasswordHasher $hasher;
 
 	/**
 	 * SetUp method
@@ -503,7 +506,6 @@ class PasswordableBehaviorTest extends TestCase {
 			'formField' => 'pwd',
 			'formFieldRepeat' => 'pwd_repeat',
 			'current' => false,
-			'passwordHasher' => 'Complex',
 		]);
 		$user = $this->Users->newEmptyEntity();
 		$data = [
@@ -525,7 +527,6 @@ class PasswordableBehaviorTest extends TestCase {
 			'formField' => 'pwd',
 			'formFieldRepeat' => 'pwd_repeat',
 			'current' => false,
-			'passwordHasher' => 'Complex',
 			'forceFieldList' => true,
 		]);
 		$user = $this->Users->newEmptyEntity();

+ 3 - 3
tests/TestCase/Model/Behavior/SluggedBehaviorTest.php

@@ -288,7 +288,7 @@ class SluggedBehaviorTest extends TestCase {
 			'conditions' => ['title LIKE' => 'Andy Daw%'],
 			'fields' => ['title', 'slug'],
 			'order' => 'title',
-		])->combine('title', 'slug')->toArray();
+		])->all()->combine('title', 'slug')->toArray();
 		$expected = [
 			'Andy Dawsom' => 'bar',
 			'Andy Dawson' => 'foo',
@@ -303,7 +303,7 @@ class SluggedBehaviorTest extends TestCase {
 			'conditions' => ['title LIKE' => 'Andy Daw%'],
 			'fields' => ['title', 'slug'],
 			'order' => 'title',
-		])->combine('title', 'slug')->toArray();
+		])->all()->combine('title', 'slug')->toArray();
 		$expected = [
 			'Andy Dawsom' => 'Andy-Dawsom',
 			'Andy Dawson' => 'Andy-Dawson',
@@ -348,7 +348,7 @@ class SluggedBehaviorTest extends TestCase {
 			'conditions' => ['title LIKE' => 'Andy Daw%'],
 			'fields' => ['title', 'slug'],
 			'order' => 'title',
-		])->combine('title', 'slug')->toArray();
+		])->all()->combine('title', 'slug')->toArray();
 		$expected = [
 			'Andy Dawson' => 'Andy-Dawso',
 			'Andy Dawsom' => 'Andy-Daw-1',

+ 4 - 9
tests/TestCase/Model/Behavior/TypeMapBehaviorTest.php

@@ -3,6 +3,8 @@
 namespace Tools\Test\TestCase\Model\Behavior;
 
 use Shim\TestSuite\TestCase;
+use Tools\Model\Behavior\TypeMapBehavior;
+use Tools\Model\Table\Table;
 
 class TypeMapBehaviorTest extends TestCase {
 
@@ -16,19 +18,12 @@ class TypeMapBehaviorTest extends TestCase {
 	/**
 	 * @var \Tools\Model\Behavior\TypeMapBehavior
 	 */
-	protected $TypeMapBehavior;
+	protected TypeMapBehavior $TypeMapBehavior;
 
 	/**
 	 * @var \Tools\Model\Table\Table
 	 */
-	protected $Table;
-
-	/**
-	 * @return void
-	 */
-	public function setUp(): void {
-		parent::setUp();
-	}
+	protected Table $Table;
 
 	/**
 	 * Tests that we can disable array conversion for edit forms if we need to modify the JSON directly.

+ 54 - 53
tests/TestCase/Model/Table/TableTest.php

@@ -3,6 +3,7 @@
 namespace Tools\Test\TestCase\Model\Table;
 
 use Cake\Utility\Hash;
+use DateTime as NativeDateTime;
 use Shim\TestSuite\TestCase;
 use Tools\Utility\DateTime;
 
@@ -67,9 +68,9 @@ class TableTest extends TestCase {
 	 * @return void
 	 */
 	public function testTimestamp() {
-		$this->Roles = $this->getTableLocator()->get('Roles');
-		$entity = $this->Roles->newEntity(['name' => 'Foo', 'alias' => 'foo']);
-		$result = $this->Roles->save($entity);
+		$Roles = $this->getTableLocator()->get('Roles');
+		$entity = $Roles->newEntity(['name' => 'Foo', 'alias' => 'foo']);
+		$result = $Roles->save($entity);
 		$this->assertTrue(!empty($result['created']));
 		$this->assertTrue(!empty($result['modified']));
 	}
@@ -176,68 +177,68 @@ class TableTest extends TestCase {
 	 * @return void
 	 */
 	public function testValidateDate() {
-		$date = new Time('2010-01-22');
+		$date = new DateTime('2010-01-22');
 		$res = $this->Users->validateDate($date);
 		$this->assertTrue($res);
 
 		// Careful: now becomes 2010-03-01 in Cake3
 		// FIXME
-		$date = new Time('2010-02-29');
+		$date = new DateTime('2010-02-29');
 		$res = $this->Users->validateDate($date);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-23 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-22')]];
+		$date = new DateTime('2010-02-23 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-22')]];
 		$res = $this->Users->validateDate($date, ['after' => 'after'], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-23');
-		$context = ['data' => ['after' => new Time('2010-02-24 11:11:11')]];
+		$date = new DateTime('2010-02-23');
+		$context = ['data' => ['after' => new DateTime('2010-02-24 11:11:11')]];
 		$res = $this->Users->validateDate($date, ['after' => 'after'], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-25');
-		$context = ['data' => ['after' => new Time('2010-02-25')]];
+		$date = new DateTime('2010-02-25');
+		$context = ['data' => ['after' => new DateTime('2010-02-25')]];
 		$res = $this->Users->validateDate($date, ['after' => 'after'], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-25');
-		$context = ['data' => ['after' => new Time('2010-02-25')]];
+		$date = new DateTime('2010-02-25');
+		$context = ['data' => ['after' => new DateTime('2010-02-25')]];
 		$res = $this->Users->validateDate($date, ['after' => 'after', 'min' => 1], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-25');
-		$context = ['data' => ['after' => new Time('2010-02-24')]];
+		$date = new DateTime('2010-02-25');
+		$context = ['data' => ['after' => new DateTime('2010-02-24')]];
 		$res = $this->Users->validateDate($date, ['after' => 'after', 'min' => 2], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-25');
-		$context = ['data' => ['after' => new Time('2010-02-24')]];
+		$date = new DateTime('2010-02-25');
+		$context = ['data' => ['after' => new DateTime('2010-02-24')]];
 		$res = $this->Users->validateDate($date, ['after' => 'after', 'min' => 1], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-25');
-		$context = ['data' => ['after' => new Time('2010-02-24')]];
+		$date = new DateTime('2010-02-25');
+		$context = ['data' => ['after' => new DateTime('2010-02-24')]];
 		$res = $this->Users->validateDate($date, ['after' => 'after', 'min' => 2], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-24');
-		$context = ['data' => ['before' => new Time('2010-02-24')]];
+		$date = new DateTime('2010-02-24');
+		$context = ['data' => ['before' => new DateTime('2010-02-24')]];
 		$res = $this->Users->validateDate($date, ['before' => 'before', 'min' => 1], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-24');
-		$context = ['data' => ['before' => new Time('2010-02-25')]];
+		$date = new DateTime('2010-02-24');
+		$context = ['data' => ['before' => new DateTime('2010-02-25')]];
 		$res = $this->Users->validateDate($date, ['before' => 'before', 'min' => 1], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-24');
-		$context = ['data' => ['before' => new Time('2010-02-25')]];
+		$date = new DateTime('2010-02-24');
+		$context = ['data' => ['before' => new DateTime('2010-02-25')]];
 		$res = $this->Users->validateDate($date, ['before' => 'before', 'min' => 2], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-24');
-		$context = ['data' => ['before' => new Time('2010-02-26')]];
+		$date = new DateTime('2010-02-24');
+		$context = ['data' => ['before' => new DateTime('2010-02-26')]];
 		$res = $this->Users->validateDate($date, ['before' => 'before', 'min' => 2], $context);
 		$this->assertTrue($res);
 	}
@@ -248,7 +249,7 @@ class TableTest extends TestCase {
 	 * @return void
 	 */
 	public function testValidateDatetime() {
-		$date = new Time('2010-01-22 11:11:11');
+		$date = new DateTime('2010-01-22 11:11:11');
 		$res = $this->Users->validateDatetime($date);
 		$this->assertTrue($res);
 
@@ -260,7 +261,7 @@ class TableTest extends TestCase {
 		*/
 
 		//FIXME
-		$date = new Time('2010-02-29 11:11:11');
+		$date = new DateTime('2010-02-29 11:11:11');
 		$res = $this->Users->validateDatetime($date);
 		//$this->assertFalse($res);
 		$this->assertTrue($res);
@@ -276,58 +277,58 @@ class TableTest extends TestCase {
 		$this->assertTrue($res);
 		*/
 
-		$date = new Time('2010-02-23 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-22 11:11:11')]];
+		$date = new DateTime('2010-02-23 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-22 11:11:11')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after'], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-23 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-24 11:11:11')]];
+		$date = new DateTime('2010-02-23 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-24 11:11:11')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after'], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-23 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-23 11:11:11')]];
+		$date = new DateTime('2010-02-23 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-23 11:11:11')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after'], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-23 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-23 11:11:11')]];
+		$date = new DateTime('2010-02-23 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-23 11:11:11')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after', 'min' => 1], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-23 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-23 11:11:11')]];
+		$date = new DateTime('2010-02-23 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-23 11:11:11')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after', 'min' => 0], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-23 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-23 11:11:10')]];
+		$date = new DateTime('2010-02-23 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-23 11:11:10')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after'], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-23 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-23 11:11:12')]];
+		$date = new DateTime('2010-02-23 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-23 11:11:12')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after'], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-24 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-23 09:11:12')]];
+		$date = new DateTime('2010-02-24 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-23 09:11:12')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after', 'max' => 2 * DAY], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-24 11:11:11');
-		$context = ['data' => ['after' => new Time('2010-02-23 09:11:12')]];
+		$date = new DateTime('2010-02-24 11:11:11');
+		$context = ['data' => ['after' => new DateTime('2010-02-23 09:11:12')]];
 		$res = $this->Users->validateDatetime($date, ['after' => 'after', 'max' => DAY], $context);
 		$this->assertFalse($res);
 
-		$date = new Time('2010-02-24 11:11:11');
-		$context = ['data' => ['before' => new Time('2010-02-25 13:11:12')]];
+		$date = new DateTime('2010-02-24 11:11:11');
+		$context = ['data' => ['before' => new DateTime('2010-02-25 13:11:12')]];
 		$res = $this->Users->validateDatetime($date, ['before' => 'before', 'max' => 2 * DAY], $context);
 		$this->assertTrue($res);
 
-		$date = new Time('2010-02-24 11:11:11');
-		$context = ['data' => ['before' => new Time('2010-02-25 13:11:12')]];
+		$date = new DateTime('2010-02-24 11:11:11');
+		$context = ['data' => ['before' => new DateTime('2010-02-25 13:11:12')]];
 		$res = $this->Users->validateDatetime($date, ['before' => 'before', 'max' => DAY], $context);
 		$this->assertFalse($res);
 	}
@@ -347,13 +348,13 @@ class TableTest extends TestCase {
 		$this->assertFalse($res);
 
 		$date = '2010-02-23 11:11:11';
-		$context = ['data' => ['before' => new DateTime('2010-02-23 11:11:12')]];
+		$context = ['data' => ['before' => new NativeDateTime('2010-02-23 11:11:12')]];
 		$res = $this->Users->validateTime($date, ['before' => 'before'], $context);
 
 		$this->assertTrue($res);
 
 		$date = '2010-02-23 11:11:11';
-		$context = ['data' => ['after' => new DateTime('2010-02-23 11:11:12')]];
+		$context = ['data' => ['after' => new NativeDateTime('2010-02-23 11:11:12')]];
 		$res = $this->Users->validateTime($date, ['after' => 'after'], $context);
 
 		$this->assertFalse($res);

+ 1 - 1
tests/TestCase/Utility/DateTimeTest.php

@@ -12,7 +12,7 @@ class DateTimeTest extends TestCase {
 	/**
 	 * @var \Tools\Utility\DateTime
 	 */
-	protected $Time;
+	protected DateTime $Time;
 
 	/**
 	 * @return void

+ 173 - 0
tests/TestCase/Utility/MimeTest.php

@@ -0,0 +1,173 @@
+<?php
+
+namespace Tools\Test\TestCase\Utility;
+
+use Cake\Core\Plugin;
+use Shim\TestSuite\TestCase;
+use TestApp\Http\TestResponse;
+use TestApp\Utility\TestMime;
+
+class MimeTest extends TestCase {
+
+	/**
+	 * @var \Tools\Utility\Mime
+	 */
+	protected $Mime;
+
+	/**
+	 * @return void
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->Mime = new TestMime();
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testObject() {
+		$this->assertTrue(is_object($this->Mime));
+		$this->assertInstanceOf('Tools\Utility\Mime', $this->Mime);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testAll() {
+		$res = $this->Mime->mimeTypes();
+		$this->assertTrue(is_array($res) && count($res) > 100);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testSingle() {
+		$res = $this->Mime->getMimeTypeByAlias('odxs');
+		$this->assertNull($res);
+
+		$res = $this->Mime->getMimeTypeByAlias('ods');
+		$this->assertEquals('application/vnd.oasis.opendocument.spreadsheet', $res);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testOverwrite() {
+		$res = $this->Mime->getMimeTypeByAlias('ics');
+		$this->assertEquals('application/ics', $res);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testReverseToSingle() {
+		$res = $this->Mime->getMimeTypeByAlias('html');
+		$this->assertEquals('text/html', $res);
+
+		$res = $this->Mime->getMimeTypeByAlias('csv');
+		$this->assertEquals('text/csv', $res);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testReverseToMultiple() {
+		$res = $this->Mime->getMimeTypeByAlias('html', false);
+		$this->assertTrue(is_array($res));
+		$this->assertSame(2, count($res));
+
+		$res = $this->Mime->getMimeTypeByAlias('csv', false);
+		$this->assertTrue(is_array($res)); //  && count($res) > 2
+		$this->assertSame(2, count($res));
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testCorrectFileExtension() {
+		file_put_contents(TMP . 'sometest.txt', 'xyz');
+		$is = $this->Mime->detectMimeType(TMP . 'sometest.txt');
+		//pr($is);
+		$this->assertEquals($is, 'text/plain');
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testWrongFileExtension() {
+		file_put_contents(TMP . 'sometest.zip', 'xyz');
+		$is = $this->Mime->detectMimeType(TMP . 'sometest.zip');
+		//pr($is);
+		$this->assertEquals($is, 'text/plain');
+		//Test failes? finfo_open not availaible??
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testgetMimeTypeByAlias() {
+		$res = $this->Mime->detectMimeType('https://raw.githubusercontent.com/dereuromark/cakephp-ide-helper/master/docs/img/code_completion.png');
+		$this->assertEquals('image/png', $res);
+
+		$res = $this->Mime->detectMimeType('https://raw.githubusercontent.com/dereuromark/cakephp-ide-helper/master/docs/img/code_completion_invalid.png');
+		$this->assertEquals('', $res);
+
+		$res = $this->Mime->detectMimeType(Plugin::path('Tools') . 'tests' . DS . 'test_files' . DS . 'img' . DS . 'hotel.jpg');
+		$this->assertEquals('image/jpeg', $res);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testEncoding() {
+		file_put_contents(TMP . 'sometest.txt', 'xyz');
+		$is = $this->Mime->getEncoding(TMP . 'sometest.txt');
+		//pr($is);
+		$this->assertEquals($is, 'us-ascii');
+
+		file_put_contents(TMP . 'sometest.zip', mb_convert_encoding('xäääyz', 'UTF-8'));
+		$is = $this->Mime->getEncoding(TMP . 'sometest.zip');
+		//pr($is);
+		$this->assertEquals($is, 'utf-8');
+
+		file_put_contents(TMP . 'sometest.zip', mb_convert_encoding('xyz', 'UTF-8'));
+		$is = $this->Mime->getEncoding(TMP . 'sometest.zip');
+		//pr($is);
+		$this->assertEquals($is, 'us-ascii');
+		//Tests fail? finfo_open not availaible??
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testDifferenceBetweenPluginAndCore() {
+		$TestCakeResponse = new TestResponse();
+		$TestMime = new TestMime();
+
+		$core = $TestCakeResponse->getMimeTypes();
+		$plugin = $TestMime->getMimeTypes();
+
+		$diff = [
+			'coreonly' => [],
+			'pluginonly' => [],
+			'modified' => [],
+		];
+		foreach ($core as $key => $value) {
+			if (!isset($plugin[$key])) {
+				$diff['coreonly'][$key] = $value;
+			} elseif ($value !== $plugin[$key]) {
+				$diff['modified'][$key] = ['was' => $value, 'is' => $plugin[$key]];
+			}
+			unset($plugin[$key]);
+		}
+		foreach ($plugin as $key => $value) {
+			$diff['pluginonly'][$key] = $value;
+		}
+
+		$this->assertNotEmpty($diff['coreonly']);
+		$this->assertNotEmpty($diff['pluginonly']);
+		$this->assertNotEmpty($diff['modified']);
+	}
+
+}

+ 16 - 28
tests/TestCase/View/Helper/CommonHelperTest.php

@@ -28,7 +28,8 @@ class CommonHelperTest extends TestCase {
 		$View = new View(null);
 		$this->Common = new CommonHelper($View);
 
-		Router::scope('/', function(RouteBuilder $routes) {
+		$builder = Router::createRouteBuilder('/');
+		$builder->scope('/', function(RouteBuilder $routes) {
 			$routes->fallbacks(DashedRoute::class);
 		});
 	}
@@ -50,7 +51,7 @@ class CommonHelperTest extends TestCase {
 	 */
 	public function testMetaName() {
 		$result = $this->Common->metaName('foo', [1, 2, 3]);
-		$expected = '<meta name="foo" content="1, 2, 3" />';
+		$expected = '<meta name="foo" content="1, 2, 3">';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -61,7 +62,7 @@ class CommonHelperTest extends TestCase {
 	 */
 	public function testMetaDescription() {
 		$result = $this->Common->metaDescription('foo', 'deu');
-		$expected = '<meta lang="deu" name="description" content="foo"/>';
+		$expected = '<meta lang="deu" name="description" content="foo">';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -72,7 +73,7 @@ class CommonHelperTest extends TestCase {
 	 */
 	public function testMetaKeywords() {
 		$result = $this->Common->metaKeywords('foo bar', 'deu');
-		$expected = '<meta lang="deu" name="keywords" content="foo bar"/>';
+		$expected = '<meta lang="deu" name="keywords" content="foo bar">';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -83,7 +84,7 @@ class CommonHelperTest extends TestCase {
 	 */
 	public function testMetaRss() {
 		$result = $this->Common->metaRss('/some/url', 'some title');
-		$expected = '<link rel="alternate" type="application/rss+xml" title="some title" href="/some/url"/>';
+		$expected = '<link rel="alternate" type="application/rss+xml" title="some title" href="/some/url">';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -94,7 +95,7 @@ class CommonHelperTest extends TestCase {
 	 */
 	public function testMetaEquiv() {
 		$result = $this->Common->metaEquiv('type', 'value');
-		$expected = '<meta http-equiv="type" content="value"/>';
+		$expected = '<meta http-equiv="type" content="value">';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -103,10 +104,10 @@ class CommonHelperTest extends TestCase {
 	 */
 	public function testMetaCanonical() {
 		$is = $this->Common->metaCanonical('/some/url/param1');
-		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1') . '" rel="canonical"/>', trim($is));
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1') . '" rel="canonical">', trim($is));
 
 		$is = $this->Common->metaCanonical('/some/url/param1', true);
-		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1', ['fullBase' => true]) . '" rel="canonical"/>', trim($is));
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1', ['fullBase' => true]) . '" rel="canonical">', trim($is));
 	}
 
 	/**
@@ -114,32 +115,19 @@ class CommonHelperTest extends TestCase {
 	 */
 	public function testMetaAlternate() {
 		$is = $this->Common->metaAlternate('/some/url/param1', 'de-de', true);
-		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1', ['fullBase' => true]) . '" rel="alternate" hreflang="de-de"/>', trim($is));
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1', ['fullBase' => true]) . '" rel="alternate" hreflang="de-de">', trim($is));
 
 		$is = $this->Common->metaAlternate(['controller' => 'Some', 'action' => 'url'], 'de', true);
-		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de"/>', trim($is));
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de">', trim($is));
 
 		$is = $this->Common->metaAlternate(['controller' => 'Some', 'action' => 'url'], ['de', 'de-ch'], true);
-		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de"/>' . PHP_EOL . '<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de-ch"/>', trim($is));
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de">' . PHP_EOL . '<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de-ch">', trim($is));
 
 		$is = $this->Common->metaAlternate(['controller' => 'Some', 'action' => 'url'], ['de' => ['ch', 'at'], 'en' => ['gb', 'us']], true);
-		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de-ch"/>' . PHP_EOL .
-			'<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de-at"/>' . PHP_EOL .
-			'<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="en-gb"/>' . PHP_EOL .
-			'<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="en-us"/>', trim($is));
-	}
-
-	/**
-	 * @return void
-	 */
-	public function testAsp() {
-		$res = $this->Common->asp('House', 2, true);
-		$expected = __d('tools', 'Houses');
-		$this->assertEquals($expected, $res);
-
-		$res = $this->Common->asp('House', 1, true);
-		$expected = __d('tools', 'House');
-		$this->assertEquals($expected, $res);
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de-ch">' . PHP_EOL .
+			'<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="de-at">' . PHP_EOL .
+			'<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="en-gb">' . PHP_EOL .
+			'<link href="' . $this->Common->Url->build('/some/url', ['fullBase' => true]) . '" rel="alternate" hreflang="en-us">', trim($is));
 	}
 
 	/**

+ 21 - 12
tests/TestCase/View/Helper/FormatHelperTest.php

@@ -7,6 +7,7 @@ use Cake\View\View;
 use Shim\TestSuite\TestCase;
 use Tools\Utility\Text;
 use Tools\View\Helper\FormatHelper;
+use Tools\View\Icon\BootstrapIcon;
 
 class FormatHelperTest extends TestCase {
 
@@ -20,7 +21,7 @@ class FormatHelperTest extends TestCase {
 	/**
 	 * @var \Tools\View\Helper\FormatHelper
 	 */
-	protected $Format;
+	protected FormatHelper $Format;
 
 	/**
 	 * @return void
@@ -31,6 +32,11 @@ class FormatHelperTest extends TestCase {
 		$this->loadRoutes();
 
 		Configure::write('App.imageBaseUrl', 'img/');
+		Configure::write('Icon', [
+			'sets' => [
+				'bs' => BootstrapIcon::class,
+			],
+		]);
 
 		$this->Format = new FormatHelper(new View(null));
 	}
@@ -73,19 +79,19 @@ class FormatHelperTest extends TestCase {
 	 */
 	public function testYesNo() {
 		$result = $this->Format->yesNo(true);
-		$expected = '<i class="icon icon-yes fa fa-check" title="' . __d('tools', 'Yes') . '" data-placement="bottom" data-toggle="tooltip"></i>';
+		$expected = '<span class="bi bi-yes" title="Yes"></span>';
 		$this->assertEquals($expected, $result);
 
 		$result = $this->Format->yesNo(false);
-		$expected = '<i class="icon icon-no fa fa-times" title="' . __d('tools', 'No') . '" data-placement="bottom" data-toggle="tooltip"></i>';
+		$expected = '<span class="bi bi-no" title="No"></span>';
 		$this->assertEquals($expected, $result);
 
 		$result = $this->Format->yesNo('2', ['on' => 2, 'onTitle' => 'foo']);
-		$expected = '<i class="icon icon-yes fa fa-check" title="foo" data-placement="bottom" data-toggle="tooltip"></i>';
-		$this->assertTextContains($expected, $result);
+		$expected = '<span class="bi bi-yes" title="foo"></span>';
+		$this->assertEquals($expected, $result);
 
 		$result = $this->Format->yesNo('3', ['on' => 4, 'offTitle' => 'nope']);
-		$expected = '<i class="icon icon-no fa fa-times" title="nope" data-placement="bottom" data-toggle="tooltip"></i>';
+		$expected = '<span class="bi bi-no" title="nope"></span>';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -111,11 +117,11 @@ class FormatHelperTest extends TestCase {
 	 */
 	public function testThumbs() {
 		$result = $this->Format->thumbs(1);
-		$expected = '<i class="icon icon-pro fa fa-thumbs-up" title="Pro" data-placement="bottom" data-toggle="tooltip"></i>';
+		$expected = '<span class="bi bi-pro" title="Pro"></span>';
 		$this->assertEquals($expected, $result);
 
 		$result = $this->Format->thumbs(0);
-		$expected = '<i class="icon icon-contra fa fa-thumbs-down" title="Contra" data-placement="bottom" data-toggle="tooltip"></i>';
+		$expected = '<span class="bi bi-contra" title="Contra"></span>';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -127,17 +133,17 @@ class FormatHelperTest extends TestCase {
 	public function testGenderIcon() {
 		$result = $this->Format->genderIcon(0);
 
-		$expected = '<i class="icon icon-genderless fa fa-genderless" title="Inter" data-placement="bottom" data-toggle="tooltip"></i>';
+		$expected = '<span class="bi bi-genderless" title="Inter"></span>';
 		$this->assertEquals($expected, $result);
 
 		$result = $this->Format->genderIcon(1);
 
-		$expected = '<i class="icon icon-male fa fa-mars" title="Male" data-placement="bottom" data-toggle="tooltip"></i>';
+		$expected = '<span class="bi bi-male" title="Male"></span>';
 		$this->assertEquals($expected, $result);
 
 		$result = $this->Format->genderIcon(2);
 
-		$expected = '<i class="icon icon-female fa fa-venus" title="Female" data-placement="bottom" data-toggle="tooltip"></i>';
+		$expected = '<span class="bi bi-female" title="Female"></span>';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -227,7 +233,7 @@ class FormatHelperTest extends TestCase {
 		$url = ['controller' => 'MyController', 'action' => 'myAction'];
 		$result = $this->Format->neighbors($neighbors, 'foo', ['slug' => true, 'url' => $url]);
 
-		$expected = '<div class="next-prev-navi nextPrevNavi"><a href="/my-controller/my-action/1/My-Foo" title="My Foo"><i class="icon icon-prev fa fa-arrow-left" title="Prev" data-placement="bottom" data-toggle="tooltip"></i>&nbsp;prevRecord</a>&nbsp;&nbsp;<a href="/my-controller/my-action/2/My-FooBaz" title="My FooBaz"><i class="icon icon-next fa fa-arrow-right" title="Next" data-placement="bottom" data-toggle="tooltip"></i>&nbsp;nextRecord</a></div>';
+		$expected = '<div class="next-prev-navi nextPrevNavi"><a href="/my-controller/my-action/1/My-Foo" title="My Foo"><span class="bi bi-prev" title="Prev"></span>&nbsp;prevRecord</a>&nbsp;&nbsp;<a href="/my-controller/my-action/2/My-FooBaz" title="My FooBaz"><span class="bi bi-next" title="Next"></span>&nbsp;nextRecord</a></div>';
 		$this->assertEquals($expected, $result);
 	}
 
@@ -354,6 +360,9 @@ TEXT;
 		parent::tearDown();
 
 		unset($this->Format);
+
+		Configure::delete('App.imageBaseUrl');
+		Configure::delete('Icon');
 	}
 
 }

+ 2 - 1
tests/TestCase/View/Helper/HtmlHelperTest.php

@@ -60,7 +60,8 @@ class HtmlHelperTest extends TestCase {
 			->withParam('prefix', 'Admin');
 		$this->Html->getView()->setRequest($request);
 
-		Router::prefix('Admin', function (RouteBuilder $routes): void {
+		$builder = Router::createRouteBuilder('/');
+		$builder->prefix('Admin', function (RouteBuilder $routes): void {
 			$routes->fallbacks(DashedRoute::class);
 		});
 		Router::setRequest($request);

+ 9 - 6
tests/TestCase/View/Helper/UrlHelperTest.php

@@ -70,7 +70,9 @@ class UrlHelperTest extends TestCase {
 			->withParam('prefix', 'Admin');
 		$this->Url->getView()->setRequest($request);
 
-		Router::prefix('Admin', function (RouteBuilder $routes): void {
+		$builder = Router::createRouteBuilder('/');
+		$builder->setRouteClass(DashedRoute::class);
+		$builder->prefix('Admin', function (RouteBuilder $routes): void {
 			$routes->fallbacks();
 		});
 		Router::setRequest($this->Url->getView()->getRequest());
@@ -101,13 +103,14 @@ class UrlHelperTest extends TestCase {
 			->withParam('prefix', 'Admin')
 			->withParam('plugin', 'Foo');
 		$this->Url->getView()->setRequest($request);
-		Router::reload();
-		Router::defaultRouteClass(DashedRoute::class);
-		Router::connect('/:controller/:action/*');
-		Router::plugin('Foo', function (RouteBuilder $routes): void {
+
+		$builder = Router::createRouteBuilder('/');
+		$builder->setRouteClass(DashedRoute::class);
+		$builder->connect('/:controller/:action/*');
+		$builder->plugin('Foo', function (RouteBuilder $routes): void {
 			$routes->fallbacks(DashedRoute::class);
 		});
-		Router::prefix('Admin', function (RouteBuilder $routes): void {
+		$builder->prefix('Admin', function (RouteBuilder $routes): void {
 			$routes->plugin('Foo', function (RouteBuilder $routes): void {
 				$routes->fallbacks(DashedRoute::class);
 			});

+ 2 - 2
tests/bootstrap.php

@@ -90,7 +90,7 @@ Cake\Log\Log::setConfig('debug', [
 	'className' => 'Cake\Log\Engine\FileLog',
 	'path' => LOGS,
 	'file' => 'debug',
-	'scopes' => false,
+	'scopes' => null,
 	'levels' => ['notice', 'info', 'debug'],
 	'url' => env('LOG_DEBUG_URL', null),
 ]);
@@ -98,7 +98,7 @@ Cake\Log\Log::setConfig('error', [
 	'className' => 'Cake\Log\Engine\FileLog',
 	'path' => LOGS,
 	'file' => 'error',
-	'scopes' => false,
+	'scopes' => null,
 	'levels' => ['warning', 'error', 'critical', 'alert', 'emergency'],
 	'url' => env('LOG_ERROR_URL', null),
 ]);

+ 1 - 1
tests/test_app/Model/Table/ToolsUsersTable.php

@@ -11,6 +11,6 @@ class ToolsUsersTable extends Table {
 	 *
 	 * @var array
 	 */
-	protected $order = ['name' => 'ASC'];
+	protected array $order = ['name' => 'ASC'];
 
 }

+ 17 - 0
tests/test_app/Utility/TestMime.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace TestApp\Utility;
+
+use Tools\Utility\Mime;
+
+class TestMime extends Mime {
+
+	/**
+	 * @param bool $coreHasPrecedence
+	 * @return array
+	 */
+	public function getMimeTypes(bool $coreHasPrecedence = false): array {
+		return $this->_mimeTypesExt;
+	}
+
+}