diff --git a/app/Controllers/Home.php b/app/Controllers/Home.php index c0645e0..00a3572 100644 --- a/app/Controllers/Home.php +++ b/app/Controllers/Home.php @@ -1,6 +1,5 @@ getNamespaces($prefix); + $paths = $namespaces[$prefix]; $filename = implode('/', $segments); break; } - - // IF we have a folder name, then the calling function - // expects this file to be within that folder, like 'Views', - // or 'libraries'. - if (! empty($folder) && strpos($path . $filename, '/' . $folder . '/') === false) + + // if no namespaces matched then quit + if (empty($paths)) { - $filename = $folder . '/' . $filename; + return false; } + + // Check each path in the namespace + foreach ($paths as $path) + { + // Ensure trailing slash + $path = rtrim($path, '/') . '/'; + + // If we have a folder name, then the calling function + // expects this file to be within that folder, like 'Views', + // or 'libraries'. + if (! empty($folder) && strpos($path . $filename, '/' . $folder . '/') === false) + { + $path .= trim($folder, '/') . '/'; + } - $path .= $filename; - - return is_file($path) ? $path : false; + $path .= $filename; + if (is_file($path)) + { + return $path; + } + } + + return false; } //-------------------------------------------------------------------- @@ -265,21 +280,10 @@ /** * Return the namespace mappings we know about. * - * @param string|null $prefix - * * @return array|string */ - protected function getNamespaces(string $prefix = null) + protected function getNamespaces() { - if ($prefix) - { - $path = $this->autoloader->getNamespace($prefix); - - return isset($path[0]) - ? rtrim($path[0], DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR - : ''; - } - $namespaces = []; foreach ($this->autoloader->getNamespace() as $prefix => $paths) diff --git a/system/Cache/CacheFactory.php b/system/Cache/CacheFactory.php index da2246d..1e33daf 100644 --- a/system/Cache/CacheFactory.php +++ b/system/Cache/CacheFactory.php @@ -39,6 +39,7 @@ namespace CodeIgniter\Cache; use CodeIgniter\Cache\Exceptions\CacheException; +use CodeIgniter\Exceptions\CriticalError; /** * Class Cache @@ -93,7 +94,20 @@ } } - $adapter->initialize(); + // If $adapter->initialization throws a CriticalError exception, we will attempt to + // use the $backup handler, if that also fails, we resort to the dummy handler. + try + { + $adapter->initialize(); + } + catch (CriticalError $e) + { + // log the fact that an exception occurred as well what handler we are resorting to + log_message('critical', $e->getMessage() . ' Resorting to using ' . $backup . ' handler.'); + + // get the next best cache handler (or dummy if the $backup also fails) + $adapter = self::getHandler($config, $backup, 'dummy'); + } return $adapter; } diff --git a/system/Cache/Handlers/MemcachedHandler.php b/system/Cache/Handlers/MemcachedHandler.php index 07a307d..974dc82 100644 --- a/system/Cache/Handlers/MemcachedHandler.php +++ b/system/Cache/Handlers/MemcachedHandler.php @@ -115,35 +115,68 @@ */ public function initialize() { - if (class_exists('\Memcached')) + // Try to connect to Memcache or Memcached, if an issue occurs throw a CriticalError exception, + // so that the CacheFactory can attempt to initiate the next cache handler. + try { - $this->memcached = new \Memcached(); - if ($this->config['raw']) + if (class_exists('\Memcached')) { - $this->memcached->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + // Create new instance of \Memcached + $this->memcached = new \Memcached(); + if ($this->config['raw']) + { + $this->memcached->setOption(\Memcached::OPT_BINARY_PROTOCOL, true); + } + + // Add server + $this->memcached->addServer( + $this->config['host'], $this->config['port'], $this->config['weight'] + ); + + // attempt to get status of servers + $stats = $this->memcached->getStats(); + + // $stats should be an associate array with a key in the format of host:port. + // If it doesn't have the key, we know the server is not working as expected. + if( !isset($stats[$this->config['host']. ':' .$this->config['port']]) ) + { + throw new CriticalError('Cache: Memcached connection failed.'); + } + } + elseif (class_exists('\Memcache')) + { + // Create new instance of \Memcache + $this->memcached = new \Memcache(); + + // Check if we can connect to the server + $can_connect = $this->memcached->connect( + $this->config['host'], $this->config['port'] + ); + + // If we can't connect, throw a CriticalError exception + if($can_connect == false){ + throw new CriticalError('Cache: Memcache connection failed.'); + } + + // Add server, third parameter is persistence and defaults to TRUE. + $this->memcached->addServer( + $this->config['host'], $this->config['port'], true, $this->config['weight'] + ); + } + else + { + throw new CriticalError('Cache: Not support Memcache(d) extension.'); } } - elseif (class_exists('\Memcache')) + catch (CriticalError $e) { - $this->memcached = new \Memcache(); + // If a CriticalError exception occurs, throw it up. + throw $e; } - else + catch (\Exception $e) { - throw new CriticalError('Cache: Not support Memcache(d) extension.'); - } - - if ($this->memcached instanceof \Memcached) - { - $this->memcached->addServer( - $this->config['host'], $this->config['port'], $this->config['weight'] - ); - } - elseif ($this->memcached instanceof \Memcache) - { - // Third parameter is persistence and defaults to TRUE. - $this->memcached->addServer( - $this->config['host'], $this->config['port'], true, $this->config['weight'] - ); + // If an \Exception occurs, convert it into a CriticalError exception and throw it. + throw new CriticalError('Cache: Memcache(d) connection refused (' . $e->getMessage() . ').'); } } diff --git a/system/Cache/Handlers/PredisHandler.php b/system/Cache/Handlers/PredisHandler.php index 847e183..0e1f79b 100644 --- a/system/Cache/Handlers/PredisHandler.php +++ b/system/Cache/Handlers/PredisHandler.php @@ -99,6 +99,8 @@ */ public function initialize() { + // Try to connect to Redis, if an issue occurs throw a CriticalError exception, + // so that the CacheFactory can attempt to initiate the next cache handler. try { // Create a new instance of Predis\Client @@ -110,7 +112,7 @@ catch (\Exception $e) { // thrown if can't connect to redis server. - throw new CriticalError('Cache: Predis connection refused (' . $e->getMessage() . ')'); + throw new CriticalError('Cache: Predis connection refused (' . $e->getMessage() . ').'); } } diff --git a/system/Cache/Handlers/RedisHandler.php b/system/Cache/Handlers/RedisHandler.php index a0a9d45..14a260b 100644 --- a/system/Cache/Handlers/RedisHandler.php +++ b/system/Cache/Handlers/RedisHandler.php @@ -39,6 +39,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\Cache\CacheInterface; +use CodeIgniter\Exceptions\CriticalError; /** * Redis cache handler @@ -116,19 +117,38 @@ $config = $this->config; $this->redis = new \Redis(); - if (! $this->redis->connect($config['host'], ($config['host'][0] === '/' ? 0 : $config['port']), $config['timeout'])) - { - log_message('error', 'Cache: Redis connection failed. Check your configuration.'); - } - if (isset($config['password']) && ! $this->redis->auth($config['password'])) + // Try to connect to Redis, if an issue occurs throw a CriticalError exception, + // so that the CacheFactory can attempt to initiate the next cache handler. + try { - log_message('error', 'Cache: Redis authentication failed.'); - } + // Note:: If Redis is your primary cache choice, and it is "offline", every page load will end up been delayed by the timeout duration. + // I feel like some sort of temporary flag should be set, to indicate that we think Redis is "offline", allowing us to bypass the timeout for a set period of time. - if (isset($config['database']) && ! $this->redis->select($config['database'])) + if (! $this->redis->connect($config['host'], ($config['host'][0] === '/' ? 0 : $config['port']), $config['timeout'])) + { + // Note:: I'm unsure if log_message() is necessary, however I'm not 100% comfortable removing it. + log_message('error', 'Cache: Redis connection failed. Check your configuration.'); + throw new CriticalError('Cache: Redis connection failed. Check your configuration.'); + } + + if (isset($config['password']) && ! $this->redis->auth($config['password'])) + { + log_message('error', 'Cache: Redis authentication failed.'); + throw new CriticalError('Cache: Redis authentication failed.'); + } + + if (isset($config['database']) && ! $this->redis->select($config['database'])) + { + log_message('error', 'Cache: Redis select database failed.'); + throw new CriticalError('Cache: Redis select database failed.'); + } + } + catch (\RedisException $e) { - log_message('error', 'Cache: Redis select database failed.'); + // $this->redis->connect() can sometimes throw a RedisException. + // We need to convert the exception into a CriticalError exception and throw it. + throw new CriticalError('Cache: RedisException occurred with message (' . $e->getMessage() . ').'); } } diff --git a/system/CodeIgniter.php b/system/CodeIgniter.php index e518db4..4e694c6 100644 --- a/system/CodeIgniter.php +++ b/system/CodeIgniter.php @@ -65,7 +65,7 @@ /** * The current version of CodeIgniter Framework */ - const CI_VERSION = '4.0.0-beta.3'; + const CI_VERSION = '4.0.0-beta.4'; /** * App startup time. @@ -642,7 +642,7 @@ */ protected function generateCacheName($config): string { - if (is_cli() && ! (ENVIRONMENT === 'testing')) + if (get_class($this->request) === CLIRequest::class) { return md5($this->request->getPath()); } @@ -704,7 +704,7 @@ } // $routes is defined in Config/Routes.php - $this->router = Services::router($routes); + $this->router = Services::router($routes, $this->request); $path = $this->determinePath(); @@ -985,16 +985,9 @@ /** * Modifies the Request Object to use a different method if a POST * variable called _method is found. - * - * Does not work on CLI commands. */ public function spoofRequestMethod() { - if (is_cli()) - { - return; - } - // Only works with POSTED forms if ($this->request->getMethod() !== 'post') { diff --git a/system/Commands/Database/MigrateRollback.php b/system/Commands/Database/MigrateRollback.php index 3d7cf17..0944ad8 100644 --- a/system/Commands/Database/MigrateRollback.php +++ b/system/Commands/Database/MigrateRollback.php @@ -126,7 +126,7 @@ } else { - // Get all namespaces form PSR4 paths. + // Get all namespaces from PSR4 paths. $config = new Autoload(); $namespaces = $config->psr4; foreach ($namespaces as $namespace => $path) diff --git a/system/Commands/Utilities/Routes.php b/system/Commands/Utilities/Routes.php index ad9f3a5..ba37395 100644 --- a/system/Commands/Utilities/Routes.php +++ b/system/Commands/Utilities/Routes.php @@ -109,6 +109,7 @@ 'get', 'head', 'post', + 'patch', 'put', 'delete', 'options', diff --git a/system/Common.php b/system/Common.php index a0ca219..213c6dd 100644 --- a/system/Common.php +++ b/system/Common.php @@ -814,6 +814,8 @@ // Set an HSTS header $response->setHeader('Strict-Transport-Security', 'max-age=' . $duration); $response->redirect($uri); + $response->sendHeaders(); + exit(); } } diff --git a/system/Config/Config.php b/system/Config/Config.php index ba80348..1abfd05 100644 --- a/system/Config/Config.php +++ b/system/Config/Config.php @@ -122,13 +122,28 @@ { return new $name(); } - + $locator = Services::locator(); $file = $locator->locateFile($name, 'Config'); - + if (empty($file)) { - return null; + // No file found - check if the class was namespaced + if (strpos($name, '\\') !== false) + { + // Class was namespaced and locateFile couldn't find it + return null; + } + + // Check all namespaces + $files = $locator->search('Config/' . $name); + if (empty($files)) + { + return null; + } + + // Get the first match (prioritizes user and framework) + $file = reset($files); } $name = $locator->getClassname($file); diff --git a/system/Config/Services.php b/system/Config/Services.php index 9fdcc01..afbf959 100644 --- a/system/Config/Services.php +++ b/system/Config/Services.php @@ -50,6 +50,7 @@ use CodeIgniter\HTTP\IncomingRequest; use CodeIgniter\HTTP\Negotiate; use CodeIgniter\HTTP\RedirectResponse; +use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface; @@ -640,15 +641,16 @@ * the correct Controller and Method to execute. * * @param \CodeIgniter\Router\RouteCollectionInterface $routes + * @param \CodeIgniter\HTTP\Request $request * @param boolean $getShared * * @return \CodeIgniter\Router\Router */ - public static function router(RouteCollectionInterface $routes = null, bool $getShared = true) + public static function router(RouteCollectionInterface $routes = null, Request $request = null, bool $getShared = true) { if ($getShared) { - return static::getSharedInstance('router', $routes); + return static::getSharedInstance('router', $routes, $request); } if (empty($routes)) @@ -656,7 +658,7 @@ $routes = static::routes(true); } - return new Router($routes); + return new Router($routes, $request); } //-------------------------------------------------------------------- diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 3bab4f1..0306651 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -1356,7 +1356,7 @@ * * @return BaseBuilder */ - public function set($key, string $value = '', bool $escape = null) + public function set($key, ?string $value = '', bool $escape = null) { $key = $this->objectToArray($key); diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 496c77f..8bcbac9 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1698,6 +1698,30 @@ //-------------------------------------------------------------------- /** + * Disables foreign key checks temporarily. + */ + public function disableForeignKeyChecks() + { + $sql = $this->_disableForeignKeyChecks(); + + return $this->query($sql); + } + + //-------------------------------------------------------------------- + + /** + * Enables foreign key checks temporarily. + */ + public function enableForeignKeyChecks() + { + $sql = $this->_enableForeignKeyChecks(); + + return $this->query($sql); + } + + //-------------------------------------------------------------------- + + /** * Allows the engine to be set into a mode where queries are not * actually executed, but they are still generated, timed, etc. * diff --git a/system/Database/BaseResult.php b/system/Database/BaseResult.php index 08e485d..a5c17f6 100644 --- a/system/Database/BaseResult.php +++ b/system/Database/BaseResult.php @@ -191,6 +191,11 @@ while ($row = $this->fetchObject($className)) { + if (method_exists($row, 'syncOriginal')) + { + $row->syncOriginal(); + } + $this->customResultObject[$className][] = $row; } @@ -277,6 +282,11 @@ is_null($this->rowData) || $this->dataSeek(0); while ($row = $this->fetchObject()) { + if (method_exists($row, 'syncOriginal')) + { + $row->syncOriginal(); + } + $this->resultObject[] = $row; } diff --git a/system/Database/Forge.php b/system/Database/Forge.php index 963e06d..6aae0d9 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -416,16 +416,16 @@ /** * Foreign Key Drop * - * @param string $table Table name - * @param string $foreign_name Foreign name + * @param string $table Table name + * @param string $foreignName Foreign name * * @return boolean|\CodeIgniter\Database\BaseResult|\CodeIgniter\Database\Query|false|mixed * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function dropForeignKey(string $table, string $foreign_name) + public function dropForeignKey(string $table, string $foreignName) { $sql = sprintf($this->dropConstraintStr, $this->db->escapeIdentifiers($this->db->DBPrefix . $table), - $this->db->escapeIdentifiers($this->db->DBPrefix . $foreign_name)); + $this->db->escapeIdentifiers($this->db->DBPrefix . $foreignName)); if ($sql === false) { @@ -484,7 +484,10 @@ if (($result = $this->db->query($sql)) !== false) { - empty($this->db->dataCache['table_names']) || ($this->db->dataCache['table_names'][] = $table); + if (! isset($this->db->dataCache['table_names'][$table])) + { + $this->db->dataCache['table_names'][] = $table; + } // Most databases don't support creating indexes from within the CREATE TABLE statement if (! empty($this->keys)) @@ -582,16 +585,16 @@ /** * Drop Table * - * @param string $table_name Table name - * @param boolean $if_exists Whether to add an IF EXISTS condition - * @param boolean $cascade Whether to add an CASCADE condition + * @param string $tableName Table name + * @param boolean $ifExists Whether to add an IF EXISTS condition + * @param boolean $cascade Whether to add an CASCADE condition * * @return mixed * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function dropTable(string $table_name, bool $if_exists = false, bool $cascade = false) + public function dropTable(string $tableName, bool $ifExists = false, bool $cascade = false) { - if ($table_name === '') + if ($tableName === '') { if ($this->db->DBDebug) { @@ -602,22 +605,26 @@ } // If the prefix is already starting the table name, remove it... - if (! empty($this->db->DBPrefix) && strpos($table_name, $this->db->DBPrefix) === 0) + if (! empty($this->db->DBPrefix) && strpos($tableName, $this->db->DBPrefix) === 0) { - $table_name = substr($table_name, strlen($this->db->DBPrefix)); + $tableName = substr($tableName, strlen($this->db->DBPrefix)); } - if (($query = $this->_dropTable($this->db->DBPrefix . $table_name, $if_exists, $cascade)) === true) + if (($query = $this->_dropTable($this->db->DBPrefix . $tableName, $ifExists, $cascade)) === true) { return true; } + $this->db->disableForeignKeyChecks(); + $query = $this->db->query($query); + $this->db->enableForeignKeyChecks(); + // Update table list cache if ($query && ! empty($this->db->dataCache['table_names'])) { - $key = array_search(strtolower($this->db->DBPrefix . $table_name), + $key = array_search(strtolower($this->db->DBPrefix . $tableName), array_map('strtolower', $this->db->dataCache['table_names']), true); if ($key !== false) { diff --git a/system/Database/MigrationRunner.php b/system/Database/MigrationRunner.php index 2154ab4..a371060 100644 --- a/system/Database/MigrationRunner.php +++ b/system/Database/MigrationRunner.php @@ -254,15 +254,15 @@ $this->checkMigrations($migrations, $method, $targetVersion); // loop migration for each namespace (module) - $migrationStatus = false; foreach ($migrations as $version => $migration) { - // Only include migrations within the scoop + // Only include migrations within the scope if (($method === 'up' && $version > $currentVersion && $version <= $targetVersion) || ( $method === 'down' && $version <= $currentVersion && $version > $targetVersion)) { $migrationStatus = false; include_once $migration->path; + // Get namespaced class name $class = $this->namespace . '\Database\Migrations\Migration_' . ($migration->name); diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index 6edc28c..4a455ff 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -569,6 +569,30 @@ //-------------------------------------------------------------------- /** + * Returns platform-specific SQL to disable foreign key checks. + * + * @return string + */ + protected function _disableForeignKeyChecks() + { + return 'SET FOREIGN_KEY_CHECKS=0'; + } + + //-------------------------------------------------------------------- + + /** + * Returns platform-specific SQL to enable foreign key checks. + * + * @return string + */ + protected function _enableForeignKeyChecks() + { + return 'SET FOREIGN_KEY_CHECKS=1'; + } + + //-------------------------------------------------------------------- + + /** * Returns the last error code and message. * * Must return an array with keys 'code' and 'message': diff --git a/system/Database/MySQLi/Result.php b/system/Database/MySQLi/Result.php index 4299b36..630a4f7 100644 --- a/system/Database/MySQLi/Result.php +++ b/system/Database/MySQLi/Result.php @@ -40,6 +40,7 @@ use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\ResultInterface; +use CodeIgniter\Entity; /** * Result for MySQLi @@ -156,10 +157,14 @@ * * @param string $className * - * @return object + * @return object|boolean|Entity */ protected function fetchObject(string $className = 'stdClass') { + if (is_subclass_of($className, Entity::class)) + { + return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data); + } return $this->resultID->fetch_object($className); } diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index d69314b..8becda2 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -419,6 +419,30 @@ //-------------------------------------------------------------------- /** + * Returns platform-specific SQL to disable foreign key checks. + * + * @return string + */ + protected function _disableForeignKeyChecks() + { + return 'SET CONSTRAINTS ALL DEFERRED'; + } + + //-------------------------------------------------------------------- + + /** + * Returns platform-specific SQL to enable foreign key checks. + * + * @return string + */ + protected function _enableForeignKeyChecks() + { + return 'SET CONSTRAINTS ALL IMMEDIATE;'; + } + + //-------------------------------------------------------------------- + + /** * Returns the last error code and message. * * Must return an array with keys 'code' and 'message': diff --git a/system/Database/Postgre/Result.php b/system/Database/Postgre/Result.php index 6842e36..106a7e1 100644 --- a/system/Database/Postgre/Result.php +++ b/system/Database/Postgre/Result.php @@ -40,6 +40,7 @@ use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\ResultInterface; +use CodeIgniter\Entity; /** * Result for Postgre @@ -154,10 +155,14 @@ * * @param string $className * - * @return object + * @return object|boolean|Entity */ protected function fetchObject(string $className = 'stdClass') { + if (is_subclass_of($className, Entity::class)) + { + return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data); + } return pg_fetch_object($this->resultID, null, $className); } diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index af14d99..576ebef 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -418,6 +418,30 @@ //-------------------------------------------------------------------- /** + * Returns platform-specific SQL to disable foreign key checks. + * + * @return string + */ + protected function _disableForeignKeyChecks() + { + return 'PRAGMA foreign_keys = OFF'; + } + + //-------------------------------------------------------------------- + + /** + * Returns platform-specific SQL to enable foreign key checks. + * + * @return string + */ + protected function _enableForeignKeyChecks() + { + return 'PRAGMA foreign_keys = ON'; + } + + //-------------------------------------------------------------------- + + /** * Returns the last error code and message. * * Must return an array with keys 'code' and 'message': @@ -504,7 +528,7 @@ * * @return boolean */ - protected function supportsForeignKeys(): bool + public function supportsForeignKeys(): bool { $result = $this->simpleQuery('PRAGMA foreign_keys'); diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index 7a5f77d..e19d4b6 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -303,15 +303,27 @@ /** * Foreign Key Drop * - * @param string $table Table name - * @param string $foreign_name Foreign name + * @param string $table Table name + * @param string $foreignName Foreign name * * @return boolean * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function dropForeignKey(string $table, string $foreign_name): bool + public function dropForeignKey(string $table, string $foreignName): bool { - throw new DatabaseException(lang('Database.dropForeignKeyUnsupported')); + // If this version of SQLite doesn't support it, we're done here + if ($this->db->supportsForeignKeys() !== true) + { + return true; + } + + // Otherwise we have to copy the table and recreate + // without the foreign key being involved now + $sqlTable = new Table($this->db, $this); + + return $sqlTable->fromTable($this->db->DBPrefix . $table) + ->dropForeignKey($foreignName) + ->run(); } //-------------------------------------------------------------------- diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php index 014a11f..3d56715 100644 --- a/system/Database/SQLite3/Result.php +++ b/system/Database/SQLite3/Result.php @@ -40,6 +40,7 @@ use CodeIgniter\Database\BaseResult; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\ResultInterface; +use CodeIgniter\Entity; /** * Result for SQLite3 @@ -182,6 +183,12 @@ } $classObj = new $className(); + + if (is_subclass_of($className, Entity::class)) + { + return $classObj->setAttributes($row); + } + $classSet = \Closure::bind(function ($key, $value) { $this->$key = $value; }, $classObj, $className diff --git a/system/Database/SQLite3/Table.php b/system/Database/SQLite3/Table.php index 865502e..14973d5 100644 --- a/system/Database/SQLite3/Table.php +++ b/system/Database/SQLite3/Table.php @@ -218,6 +218,40 @@ } /** + * Drops a foreign key from this table so that + * it won't be recreated in the future. + * + * @param string $column + * + * @return \CodeIgniter\Database\SQLite3\Table + */ + public function dropForeignKey(string $column) + { + if (empty($this->foreignKeys)) + { + return $this; + } + + for ($i = 0; $i < count($this->foreignKeys); $i++) + { + if ($this->foreignKeys[$i]->table_name !== $this->tableName) + { + continue; + } + + // The column name should be the first thing in the constraint name + if (strpos($this->foreignKeys[$i]->constraint_name, $column) !== 0) + { + continue; + } + + unset($this->foreignKeys[$i]); + } + + return $this; + } + + /** * Creates the new table based on our current fields. * * @return mixed diff --git a/system/Debug/Toolbar/Collectors/Routes.php b/system/Debug/Toolbar/Collectors/Routes.php index 2e3a1e3..aecbc32 100644 --- a/system/Debug/Toolbar/Collectors/Routes.php +++ b/system/Debug/Toolbar/Collectors/Routes.php @@ -80,7 +80,7 @@ public function display(): array { $rawRoutes = Services::routes(true); - $router = Services::router(null, true); + $router = Services::router(null, null, true); /* * Matched Route diff --git a/system/Entity.php b/system/Entity.php index 19cb2b3..85b2ad8 100644 --- a/system/Entity.php +++ b/system/Entity.php @@ -47,43 +47,45 @@ */ class Entity { - /** - * Maps names used in sets and gets against unique - * names within the class, allowing independence from - * database column names. - * - * Example: - * $datamap = [ - * 'db_name' => 'class_name' - * ]; - */ - protected $_options = [ - 'datamap' => [], + /** + * Maps names used in sets and gets against unique + * names within the class, allowing independence from + * database column names. + * + * Example: + * $datamap = [ + * 'db_name' => 'class_name' + * ]; + */ + protected $datamap = []; - /** - * Define properties that are automatically converted to Time instances. - */ - 'dates' => [ - 'created_at', - 'updated_at', - 'deleted_at', - ], - - /** - * Array of field names and the type of value to cast them as - * when they are accessed. - */ - 'casts' => [], + protected $dates = [ + 'created_at', + 'updated_at', + 'deleted_at', ]; /** + * Array of field names and the type of value to cast them as + * when they are accessed. + */ + protected $casts = []; + + /** + * Holds the current values of all class vars. + * + * @var array + */ + protected $attributes = []; + + /** * Holds original copies of all class vars so * we can determine what's actually been changed * and not accidentally write nulls where we shouldn't. * * @var array */ - protected $_original = []; + protected $original = []; /** * Holds info whenever properties have to be casted @@ -99,24 +101,9 @@ */ public function __construct(array $data = null) { - // Collect any original values of things - // so we can compare later to see what's changed - $properties = get_object_vars($this); + $this->syncOriginal(); - foreach ($properties as $key => $value) - { - if (substr($key, 0, 1) === '_') - { - unset($properties[$key]); - } - } - - $this->_original = $properties; - - if (is_array($data)) - { - $this->fill($data); - } + $this->fill($data); } /** @@ -128,8 +115,13 @@ * * @return \CodeIgniter\Entity */ - public function fill(array $data) + public function fill(array $data = null) { + if (! is_array($data)) + { + return $this; + } + foreach ($data as $key => $value) { $key = $this->mapProperty($key); @@ -140,9 +132,9 @@ { $this->$method($value); } - elseif (property_exists($this, $key)) + else { - $this->$key = $value; + $this->attributes[$key] = $value; } } @@ -170,16 +162,14 @@ // we need to loop over our properties so that we // allow our magic methods a chance to do their thing. - $properties = get_object_vars($this); - - foreach ($properties as $key => $value) + foreach ($this->attributes as $key => $value) { if (substr($key, 0, 1) === '_') { continue; } - if ($onlyChanged && ! $this->hasPropertyChanged($key, $value)) + if ($onlyChanged && ! $this->hasChanged($key)) { continue; } @@ -188,9 +178,9 @@ } // Loop over our mapped properties and add them to the list... - if (is_array($this->_options['datamap'])) + if (is_array($this->datamap)) { - foreach ($this->_options['datamap'] as $from => $to) + foreach ($this->datamap as $from => $to) { $return[$from] = $this->__get($to); } @@ -202,9 +192,7 @@ //-------------------------------------------------------------------- /** - * Converts the properties of this class into an array. Unlike toArray() - * this will not cast the data or use any magic accessors. It simply - * returns the raw data for use when saving to the model, etc. + * Returns the raw values of the current attributes. * * @param boolean $onlyChanged * @@ -214,21 +202,19 @@ { $return = []; - $properties = get_object_vars($this); - - foreach ($properties as $key => $value) + if (! $onlyChanged) { - if (substr($key, 0, 1) === '_') + return $this->attributes; + } + + foreach ($this->attributes as $key => $value) + { + if (! $this->hasChanged($key)) { continue; } - if ($onlyChanged && ! $this->hasPropertyChanged($key, $value)) - { - continue; - } - - $return[$key] = $this->$key; + $return[$key] = $this->attributes[$key]; } return $return; @@ -237,16 +223,46 @@ //-------------------------------------------------------------------- /** + * Ensures our "original" values match the current values. + * + * @return $this + */ + public function syncOriginal() + { + $this->original = $this->attributes; + + return $this; + } + + /** * Checks a property to see if it has changed since the entity was created. + * Or, without a parameter, checks if any properties have changed. * * @param string $key - * @param null $value * * @return boolean */ - protected function hasPropertyChanged(string $key, $value = null): bool + public function hasChanged(string $key = null): bool { - return ! (($this->_original[$key] === null && $value === null) || $this->_original[$key] === $value); + // If no parameter was given then check all attributes + if ($key === null) + { + return $this->original !== $this->attributes; + } + + // Key doesn't exist in either + if (! array_key_exists($key, $this->original) && ! array_key_exists($key, $this->attributes)) + { + return false; + } + + // It's a new element + if (! array_key_exists($key, $this->original) && array_key_exists($key, $this->attributes)) + { + return true; + } + + return $this->original[$key] !== $this->attributes[$key]; } /** @@ -266,7 +282,8 @@ */ public function __get(string $key) { - $key = $this->mapProperty($key); + $key = $this->mapProperty($key); + $result = null; // Convert to CamelCase for the method $method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $key))); @@ -280,25 +297,20 @@ // Otherwise return the protected property // if it exists. - else if (property_exists($this, $key)) + else if (array_key_exists($key, $this->attributes)) { - $result = $this->$key; + $result = $this->attributes[$key]; } // Do we need to mutate this into a date? - if (in_array($key, $this->_options['dates'])) + if (in_array($key, $this->dates)) { $result = $this->mutateDate($result); } // Or cast it as something? - else if ($this->_cast && isset($this->_options['casts'][$key]) && ! empty($this->_options['casts'][$key])) + else if ($this->_cast && isset($this->casts[$key]) && ! empty($this->casts[$key])) { - $result = $this->castAs($result, $this->_options['casts'][$key]); - } - - if (! isset($result) && ! property_exists($this, $key)) - { - throw EntityException::forTryingToAccessNonExistentProperty($key, get_called_class()); + $result = $this->castAs($result, $this->casts[$key]); } return $result; @@ -326,7 +338,7 @@ $key = $this->mapProperty($key); // Check if the field should be mutated into a date - if (in_array($key, $this->_options['dates'])) + if (in_array($key, $this->dates)) { $value = $this->mutateDate($value); } @@ -334,10 +346,10 @@ $isNullable = false; $castTo = false; - if (array_key_exists($key, $this->_options['casts'])) + if (array_key_exists($key, $this->casts)) { - $isNullable = substr($this->_options['casts'][$key], 0, 1) === '?'; - $castTo = $isNullable ? substr($this->_options['casts'][$key], 1) : $this->_options['casts'][$key]; + $isNullable = substr($this->casts[$key], 0, 1) === '?'; + $castTo = $isNullable ? substr($this->casts[$key], 1) : $this->casts[$key]; } if (! $isNullable || ! is_null($value)) @@ -381,7 +393,7 @@ // they cannot be saved. Useful for // grabbing values through joins, // assigning relationships, etc. - $this->$key = $value; + $this->attributes[$key] = $value; return $this; } @@ -389,9 +401,7 @@ //-------------------------------------------------------------------- /** - * Unsets a protected/private class property. Sets the value to null. - * However, if there was a default value for the parent class, this - * attribute will be reset to that default value. + * Unsets an attribute property. * * @param string $key * @@ -399,24 +409,7 @@ */ public function __unset(string $key) { - // If not actual property exists, get out - // before we confuse our data mapping. - if (! property_exists($this, $key)) - { - return; - } - - $this->$key = null; - - // Get the class' original default value for this property - // so we can reset it to the original value. - $reflectionClass = new \ReflectionClass($this); - $defaultProperties = $reflectionClass->getDefaultProperties(); - - if (isset($defaultProperties[$key])) - { - $this->$key = $defaultProperties[$key]; - } + unset($this->attributes[$key]); } //-------------------------------------------------------------------- @@ -431,11 +424,20 @@ */ public function __isset(string $key): bool { - // Ensure an actual property exists, otherwise - // we confuse the data mapping. - $value = property_exists($this, $key) ? $this->$key : null; + return isset($this->attributes[$key]); + } - return ! is_null($value); + /** + * Set raw data array without any mutations + * + * @param array $data + * @return $this + */ + public function setAttributes(array $data) + { + $this->attributes = $data; + $this->syncOriginal(); + return $this; } //-------------------------------------------------------------------- @@ -450,14 +452,14 @@ */ protected function mapProperty(string $key) { - if (empty($this->_options['datamap'])) + if (empty($this->datamap)) { return $key; } - if (isset($this->_options['datamap'][$key]) && ! empty($this->_options['datamap'][$key])) + if (isset($this->datamap[$key]) && ! empty($this->datamap[$key])) { - return $this->_options['datamap'][$key]; + return $this->datamap[$key]; } return $key; @@ -586,7 +588,7 @@ $tmp = ! is_null($value) ? ($asArray ? [] : new \stdClass) : null; if (function_exists('json_decode')) { - if ((is_string($value) && (strpos($value, '[') === 0 || strpos($value, '{') === 0 || (strpos($value, '"') === 0 && strrpos($value, '"') === 0 ))) || is_numeric($value)) + if ((is_string($value) && strlen($value) > 1 && in_array($value{0}, ['[','{','"'])) || is_numeric($value)) { $tmp = json_decode($value, $asArray); diff --git a/system/Exceptions/EntityException.php b/system/Exceptions/EntityException.php deleted file mode 100644 index 40e3017..0000000 --- a/system/Exceptions/EntityException.php +++ /dev/null @@ -1,22 +0,0 @@ -getExtension(); + $extension = $this->getExtension(); + $extension = empty($extension) ? '' : '.' . $extension; + return time() . '_' . bin2hex(random_bytes(10)) . $extension; } //-------------------------------------------------------------------- @@ -203,6 +205,7 @@ while (is_file($destination)) { $info = pathinfo($destination); + $extension = isset($info['extension']) ? '.' . $info['extension'] : ''; if (strpos($info['filename'], $delimiter) !== false) { $parts = explode($delimiter, $info['filename']); @@ -211,16 +214,16 @@ $i = end($parts); array_pop($parts); array_push($parts, ++ $i); - $destination = $info['dirname'] . '/' . implode($delimiter, $parts) . '.' . $info['extension']; + $destination = $info['dirname'] . '/' . implode($delimiter, $parts) . $extension; } else { - $destination = $info['dirname'] . '/' . $info['filename'] . $delimiter . ++ $i . '.' . $info['extension']; + $destination = $info['dirname'] . '/' . $info['filename'] . $delimiter . ++ $i . $extension; } } else { - $destination = $info['dirname'] . '/' . $info['filename'] . $delimiter . ++ $i . '.' . $info['extension']; + $destination = $info['dirname'] . '/' . $info['filename'] . $delimiter . ++ $i . $extension; } } return $destination; diff --git a/system/Filters/Filters.php b/system/Filters/Filters.php index 80b8833..fb2e918 100644 --- a/system/Filters/Filters.php +++ b/system/Filters/Filters.php @@ -483,7 +483,7 @@ // need to make pseudo wildcard real $path = strtolower(str_replace('*', '.*', $path)); // Does this rule apply here? - if (preg_match('#' . $path . '#', $uri, $match) === 1) + if (preg_match('#^' . $path . '$#', $uri, $match) === 1) { return true; } diff --git a/system/HTTP/CLIRequest.php b/system/HTTP/CLIRequest.php index 518bb62..8c80811 100644 --- a/system/HTTP/CLIRequest.php +++ b/system/HTTP/CLIRequest.php @@ -71,6 +71,13 @@ */ protected $options = []; + /** + * Set the expected HTTP verb + * + * @var string + */ + protected $method = 'cli'; + //-------------------------------------------------------------------- /** diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index a0ba1ef..b0ca8b7 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -538,7 +538,7 @@ // See http://tools.ietf.org/html/rfc7230#section-3.3.2 if (is_null($this->getHeader('content-length'))) { - $this->setHeader('Content-Length', 0); + $this->setHeader('Content-Length', '0'); } } else if ($method === 'HEAD') @@ -675,7 +675,7 @@ if (isset($config['debug'])) { $curl_options[CURLOPT_VERBOSE] = $config['debug'] === true ? 1 : 0; - $curl_options[CURLOPT_STDERR] = is_bool($config['debug']) ? fopen('php://output', 'w+') : $config['debug']; + $curl_options[CURLOPT_STDERR] = $config['debug'] === true ? fopen('php://output', 'w+') : $config['debug']; } // Decode Content diff --git a/system/HTTP/IncomingRequest.php b/system/HTTP/IncomingRequest.php index 9b0cd82..185b3bb 100755 --- a/system/HTTP/IncomingRequest.php +++ b/system/HTTP/IncomingRequest.php @@ -595,6 +595,9 @@ $this->uri->setHost(parse_url($baseURL, PHP_URL_HOST)); $this->uri->setPort(parse_url($baseURL, PHP_URL_PORT)); $this->uri->resolveRelativeURI(parse_url($baseURL, PHP_URL_PATH)); + + // Ensure we have any query vars + $this->uri->setQuery($_SERVER['QUERY_STRING'] ?? ''); } else { diff --git a/system/HTTP/Request.php b/system/HTTP/Request.php index 6940504..7e64f52 100644 --- a/system/HTTP/Request.php +++ b/system/HTTP/Request.php @@ -84,7 +84,10 @@ { $this->proxyIPs = $config->proxyIPs; - $this->method = $this->getServer('REQUEST_METHOD') ?? 'GET'; + if (empty($this->method)) + { + $this->method = $this->getServer('REQUEST_METHOD') ?? 'GET'; + } } //-------------------------------------------------------------------- diff --git a/system/Helpers/date_helper.php b/system/Helpers/date_helper.php index 451657a..815e037 100644 --- a/system/Helpers/date_helper.php +++ b/system/Helpers/date_helper.php @@ -69,3 +69,34 @@ return mktime($hour, $minute, $second, $month, $day, $year); } } + +if (! function_exists('timezone_select')) +{ + /** + * Generates a select field of all available timezones + * + * Returns a string with the formatted HTML + * + * @param string $class Optional class to apply to the select field + * @param string $default Default value for initial selection + * @param int $what One of the DateTimeZone class constants (for listIdentifiers) + * @param string $country A two-letter ISO 3166-1 compatible country code (for listIdentifiers) + * + * @return string + * @throws \Exception + */ + function timezone_select(string $class = '', string $default = '', int $what = \DateTimeZone::ALL, string $country = null): string + { + $timezones = \DateTimeZone::listIdentifiers($what, $country); + + $buffer = "" . PHP_EOL; + + return $buffer; + } +} diff --git a/system/Language/en/Database.php b/system/Language/en/Database.php index 250550c..22d719c 100644 --- a/system/Language/en/Database.php +++ b/system/Language/en/Database.php @@ -26,6 +26,7 @@ 'featureUnavailable' => 'This feature is not available for the database you are using.', 'tableNotFound' => 'Table `{0}` was not found in the current database.', 'noPrimaryKey' => '`{0}` model class does not specify a Primary Key.', + 'noDateFormat' => '`{0}` model class does not have a valid dateFormat.', 'fieldNotExists' => 'Field `{0}` not found.', 'forEmptyInputGiven' => 'Empty statement is given for the field `{0}`', 'forFindColumnHaveMultipleColumns' => 'Only single column allowed in Column name.', diff --git a/system/Model.php b/system/Model.php index 155ef07..2842ec3 100644 --- a/system/Model.php +++ b/system/Model.php @@ -120,7 +120,7 @@ /** * If this model should use "softDeletes" and - * simply set a flag when rows are deleted, or + * simply set a date when rows are deleted, or * do hard deletes. * * @var boolean @@ -182,7 +182,7 @@ * * @var string */ - protected $deletedField = 'deleted'; + protected $deletedField = 'deleted_at'; /** * Used by asArray and asObject to provide @@ -356,7 +356,7 @@ if ($this->tempUseSoftDeletes === true) { - $builder->where($this->table . '.' . $this->deletedField, 0); + $builder->where($this->table . '.' . $this->deletedField, null); } if (is_array($id)) @@ -395,6 +395,7 @@ * @param string $columnName * * @return array|null The resulting row of data, or null if no data found. + * @throws \CodeIgniter\Database\Exceptions\DataException */ public function findColumn(string $columnName) { @@ -427,7 +428,7 @@ if ($this->tempUseSoftDeletes === true) { - $builder->where($this->table . '.' . $this->deletedField, 0); + $builder->where($this->table . '.' . $this->deletedField, null); } $row = $builder->limit($limit, $offset) @@ -457,7 +458,7 @@ if ($this->tempUseSoftDeletes === true) { - $builder->where($this->table . '.' . $this->deletedField, 0); + $builder->where($this->table . '.' . $this->deletedField, null); } // Some databases, like PostgreSQL, need order @@ -713,17 +714,21 @@ $result = $this->builder() ->set($data['data'], '', $escape) ->insert(); - + + // If insertion succeeded then save the insert ID + if ($result) + { + $this->insertID = $this->db->insertID(); + } + $this->trigger('afterInsert', ['data' => $originalData, 'result' => $result]); - // If insertion failed, get our of here + // If insertion failed, get out of here if (! $result) { return $result; } - $this->insertID = $this->db->insertID(); - // otherwise return the insertID, if requested. return $returnID ? $this->insertID : $result; } @@ -908,7 +913,7 @@ if ($this->useSoftDeletes && ! $purge) { - $set[$this->deletedField] = 1; + $set[$this->deletedField] = $this->setDate(); if ($this->useTimestamps && ! empty($this->updatedField)) { @@ -943,8 +948,8 @@ } return $this->builder() - ->where($this->deletedField, 1) - ->delete(); + ->where($this->table . '.' . $this->deletedField . ' IS NOT NULL') + ->delete(); } //-------------------------------------------------------------------- @@ -977,7 +982,7 @@ $this->tempUseSoftDeletes = false; $this->builder() - ->where($this->deletedField, 1); + ->where($this->table . '.' . $this->deletedField . ' IS NOT NULL'); return $this; } @@ -1148,6 +1153,7 @@ * @param string $table * * @return BaseBuilder + * @throws \CodeIgniter\Exceptions\ModelException; */ protected function builder(string $table = null) { @@ -1222,8 +1228,8 @@ /** * A utility function to allow child models to use the type of * date/time format that they prefer. This is primarily used for - * setting created_at and updated_at values, but can be used - * by inheriting classes. + * setting created_at, updated_at and deleted_at values, but can be + * used by inheriting classes. * * The available time formats are: * - 'int' - Stores the date as an integer timestamp @@ -1233,6 +1239,7 @@ * @param integer $userData An optional PHP timestamp to be converted. * * @return mixed + * @throws \CodeIgniter\Exceptions\ModelException; */ protected function setDate(int $userData = null) { @@ -1249,6 +1256,8 @@ case 'date': return date('Y-m-d', $currentDate); break; + default: + throw ModelException::forNoDateFormat(get_class($this)); } } @@ -1539,7 +1548,7 @@ { if ($this->tempUseSoftDeletes === true) { - $this->builder()->where($this->deletedField, 0); + $this->builder()->where($this->table . '.' . $this->deletedField, null); } return $this->builder()->countAllResults($reset, $test); diff --git a/system/Pager/Pager.php b/system/Pager/Pager.php index b61fdc8..8d43b06 100644 --- a/system/Pager/Pager.php +++ b/system/Pager/Pager.php @@ -147,20 +147,22 @@ * @param integer $page * @param integer $perPage * @param integer $total - * @param string $template The output template alias to render. - * @param integer $segment (if page number is provided by URI segment) + * @param string $template The output template alias to render. + * @param integer $segment (if page number is provided by URI segment) * + * @param string|null $group optional group (i.e. if we'd like to define custom path) * @return string */ - public function makeLinks(int $page, int $perPage, int $total, string $template = 'default_full', int $segment = 0): string + public function makeLinks(int $page, int $perPage, int $total, string $template = 'default_full', int $segment = 0, ?string $group = null): string { $name = time(); - $this->store($name, $page, $perPage, $total, $segment); + $this->store($group ?? $name, $page, $perPage, $total, $segment); - return $this->displayLinks($name, $template); + return $this->displayLinks($group ?? $name, $template); } + //-------------------------------------------------------------------- /** diff --git a/system/Router/RouteCollection.php b/system/Router/RouteCollection.php index 7fe559e..37963de 100644 --- a/system/Router/RouteCollection.php +++ b/system/Router/RouteCollection.php @@ -39,6 +39,8 @@ namespace CodeIgniter\Router; +use CodeIgniter\HTTP\Request; +use Config\Services; use CodeIgniter\Autoloader\FileLocator; use CodeIgniter\Router\Exceptions\RouterException; @@ -233,11 +235,7 @@ */ public function __construct(FileLocator $locator, $moduleConfig) { - // Get HTTP verb - $this->HTTPVerb = strtolower($_SERVER['REQUEST_METHOD'] ?? 'cli'); - - $this->fileLocator = $locator; - + $this->fileLocator = $locator; $this->moduleConfig = $moduleConfig; } @@ -1116,6 +1114,12 @@ $from = key($route['route']); $to = $route['route'][$from]; + // ignore closures + if (! is_string($to)) + { + continue; + } + // Lose any namespace slash at beginning of strings // to ensure more consistent match. $to = ltrim($to, '\\'); diff --git a/system/Router/Router.php b/system/Router/Router.php index 31da9ee..843e842 100644 --- a/system/Router/Router.php +++ b/system/Router/Router.php @@ -38,6 +38,7 @@ namespace CodeIgniter\Router; +use CodeIgniter\HTTP\Request; use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Router\Exceptions\RedirectException; use CodeIgniter\Router\Exceptions\RouterException; @@ -135,13 +136,16 @@ * Stores a reference to the RouteCollection object. * * @param RouteCollectionInterface $routes + * @param Request $request */ - public function __construct(RouteCollectionInterface $routes) + public function __construct(RouteCollectionInterface $routes, Request $request = null) { $this->collection = $routes; $this->controller = $this->collection->getDefaultController(); $this->method = $this->collection->getDefaultMethod(); + + $this->collection->setHTTPVerb($request->getMethod() ?? strtolower($_SERVER['REQUEST_METHOD'])); } //-------------------------------------------------------------------- @@ -564,7 +568,9 @@ */ protected function validateRequest(array $segments): array { - $segments = array_filter($segments); + $segments = array_filter($segments, function ($segment) { + return ! empty($segment) || ($segment !== '0' || $segment !== 0); + }); $segments = array_values($segments); $c = count($segments); diff --git a/system/Router/RouterInterface.php b/system/Router/RouterInterface.php index 1823487..5c5056f 100644 --- a/system/Router/RouterInterface.php +++ b/system/Router/RouterInterface.php @@ -38,6 +38,8 @@ namespace CodeIgniter\Router; +use CodeIgniter\HTTP\Request; + /** * Expected behavior of a Router. */ @@ -47,9 +49,10 @@ /** * Stores a reference to the RouteCollection object. * - * @param RouteCollectionInterface $routes + * @param RouteCollectionInterface $routes + * @param \CodeIgniter\HTTP\Request $request */ - public function __construct(RouteCollectionInterface $routes); + public function __construct(RouteCollectionInterface $routes, Request $request = null); //-------------------------------------------------------------------- diff --git a/system/Session/Handlers/ArrayHandler.php b/system/Session/Handlers/ArrayHandler.php new file mode 100644 index 0000000..632509d --- /dev/null +++ b/system/Session/Handlers/ArrayHandler.php @@ -0,0 +1,152 @@ +markTestSkipped('XDebug not found.'); + } + foreach (xdebug_get_headers() as $emitted) { $found = $ignoreCase ? @@ -155,6 +160,11 @@ { $found = false; + if (! function_exists('xdebug_get_headers')) + { + $this->markTestSkipped('XDebug not found.'); + } + foreach (xdebug_get_headers() as $emitted) { $found = $ignoreCase ? @@ -254,6 +264,11 @@ { $found = false; + if (! function_exists('xdebug_get_headers')) + { + $this->markTestSkipped('XDebug not found.'); + } + foreach (xdebug_get_headers() as $emitted) { $found = $ignoreCase ? diff --git a/system/Test/FeatureTestCase.php b/system/Test/FeatureTestCase.php index 83ddc8e..229cfe3 100644 --- a/system/Test/FeatureTestCase.php +++ b/system/Test/FeatureTestCase.php @@ -151,7 +151,8 @@ public function call(string $method, string $path, array $params = null) { // Simulate having a blank session - $_SESSION = []; + $_SESSION = []; + $_SERVER['REQUEST_METHOD'] = $method; $request = $this->setupRequest($method, $path, $params); $request = $this->populateGlobals($method, $request, $params); @@ -165,7 +166,6 @@ // Make sure any other classes that might call the request // instance get the right one. Services::injectMock('request', $request); - $_SERVER['REQUEST_METHOD'] = $method; $response = $this->app ->setRequest($request) diff --git a/system/Throttle/Throttler.php b/system/Throttle/Throttler.php index b549a70..db0edf7 100644 --- a/system/Throttle/Throttler.php +++ b/system/Throttle/Throttler.php @@ -138,12 +138,12 @@ $tokenName = $this->prefix . $key; // Check to see if the bucket has even been created yet. - if (($tokens = $this->cache->get($tokenName)) === false) + if (($tokens = $this->cache->get($tokenName)) === null) { // If it hasn't been created, then we'll set it to the maximum // capacity - 1, and save it to the cache. $this->cache->save($tokenName, $capacity - $cost, $seconds); - $this->cache->save($tokenName . 'Time', time()); + $this->cache->save($tokenName . 'Time', time(), $seconds); return true; } @@ -152,12 +152,15 @@ // based on how long it's been since the last update. $throttleTime = $this->cache->get($tokenName . 'Time'); $elapsed = $this->time() - $throttleTime; + // Number of tokens to add back per second $rate = $capacity / $seconds; - // We must have a minimum wait of 1 second for a new token + // How many seconds till a new token is available. + // We must have a minimum wait of 1 second for a new token. // Primarily stored to allow devs to report back to users. - $this->tokenTime = max(1, $rate); + $newTokenAvailable = (1 / $rate) - $elapsed; + $this->tokenTime = max(1, $newTokenAvailable); // Add tokens based up on number per second that // should be refilled, then checked against capacity @@ -165,19 +168,17 @@ $tokens += $rate * $elapsed; $tokens = $tokens > $capacity ? $capacity : $tokens; - // If $tokens > 0, then we are save to perform the action, but + // If $tokens > 0, then we are safe to perform the action, but // we need to decrement the number of available tokens. - $response = false; - if ($tokens > 0) { - $response = true; + $this->cache->save($tokenName, $tokens - $cost, $seconds); + $this->cache->save($tokenName . 'Time', time(), $seconds); - $this->cache->save($tokenName, $tokens - $cost, $elapsed); - $this->cache->save($tokenName . 'Time', time()); + return true; } - return $response; + return false; } //-------------------------------------------------------------------- diff --git a/tests/_support/Autoloader/UnnamespacedClass.php b/tests/_support/Autoloader/UnnamespacedClass.php new file mode 100644 index 0000000..67b1ed3 --- /dev/null +++ b/tests/_support/Autoloader/UnnamespacedClass.php @@ -0,0 +1,5 @@ +prefix . $key; + + return array_key_exists($key, $this->cache) + ? $this->cache[$key] + : null; + } + + //-------------------------------------------------------------------- + + /** + * Saves an item to the cache store. + * + * The $raw parameter is only utilized by Mamcache in order to + * allow usage of increment() and decrement(). + * + * @param string $key Cache item name + * @param $value the data to save + * @param null $ttl Time To Live, in seconds (default 60) + * @param boolean $raw Whether to store the raw value. + * + * @return mixed + */ + public function save(string $key, $value, int $ttl = 60, bool $raw = false) + { + $key = $this->prefix . $key; + + $this->cache[$key] = $value; + + return true; + } + + //-------------------------------------------------------------------- + + /** + * Deletes a specific item from the cache store. + * + * @param string $key Cache item name + * + * @return mixed + */ + public function delete(string $key) + { + unset($this->cache[$key]); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic incrementation of a raw stored value. + * + * @param string $key Cache ID + * @param integer $offset Step/value to increase by + * + * @return mixed + */ + public function increment(string $key, int $offset = 1) + { + $key = $this->prefix . $key; + + $data = $this->cache[$key] ?: null; + + if (empty($data)) + { + $data = 0; + } + elseif (! is_int($data)) + { + return false; + } + + return $this->save($key, $data + $offset); + } + + //-------------------------------------------------------------------- + + /** + * Performs atomic decrementation of a raw stored value. + * + * @param string $key Cache ID + * @param integer $offset Step/value to increase by + * + * @return mixed + */ + public function decrement(string $key, int $offset = 1) + { + $key = $this->prefix . $key; + + $data = $this->cache[$key] ?: null; + + if (empty($data)) + { + $data = 0; + } + elseif (! is_int($data)) + { + return false; + } + + return $this->save($key, $data - $offset); + } + + //-------------------------------------------------------------------- + + /** + * Will delete all items in the entire cache. + * + * @return mixed + */ + public function clean() + { + $this->cache = []; + } + + //-------------------------------------------------------------------- + + /** + * Returns information on the entire cache. + * + * The information returned and the structure of the data + * varies depending on the handler. + * + * @return mixed + */ + public function getCacheInfo() + { + return []; + } + + //-------------------------------------------------------------------- + + /** + * Returns detailed information about the specific item in the cache. + * + * @param string $key Cache item name. + * + * @return mixed + */ + public function getMetaData(string $key) + { + return false; + } + + //-------------------------------------------------------------------- + + /** + * Determines if the driver is supported on this system. + * + * @return boolean + */ + public function isSupported(): bool + { + return true; + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/_support/Commands/AbstractInfo.php b/tests/_support/Commands/AbstractInfo.php new file mode 100644 index 0000000..2ca684e --- /dev/null +++ b/tests/_support/Commands/AbstractInfo.php @@ -0,0 +1,13 @@ +showError($oops); + } + } + + public function helpme() + { + $this->call('help'); + } +} diff --git a/tests/_support/Commands/CommandsTestStreamFilter.php b/tests/_support/Commands/CommandsTestStreamFilter.php new file mode 100644 index 0000000..599075b --- /dev/null +++ b/tests/_support/Commands/CommandsTestStreamFilter.php @@ -0,0 +1,18 @@ +data; + $consumed += $bucket->datalen; + } + return PSFS_PASS_ON; + } +} + +stream_filter_register('CommandsTestStreamFilter', 'CodeIgniter\Commands\CommandsTestStreamFilter'); diff --git a/tests/_support/Config/BadRegistrar.php b/tests/_support/Config/BadRegistrar.php new file mode 100644 index 0000000..a651d11 --- /dev/null +++ b/tests/_support/Config/BadRegistrar.php @@ -0,0 +1,18 @@ + [ + /* + * The log levels that this handler will handle. + */ + 'handles' => [ + 'critical', + 'alert', + 'emergency', + 'debug', + 'error', + 'info', + 'notice', + 'warning', + ], + ], + ]; + +} diff --git a/tests/_support/Config/MockServices.php b/tests/_support/Config/MockServices.php new file mode 100644 index 0000000..0bd6400 --- /dev/null +++ b/tests/_support/Config/MockServices.php @@ -0,0 +1,28 @@ + TESTPATH . '_support/', + ]; + public $classmap = []; + + //-------------------------------------------------------------------- + + public function __construct() + { + // Don't call the parent since we don't want the default mappings. + // parent::__construct(); + } + + //-------------------------------------------------------------------- + public static function locator(bool $getShared = true) + { + return new \CodeIgniter\Autoloader\FileLocator(static::autoloader()); + } + +} diff --git a/tests/_support/Config/Registrar.php b/tests/_support/Config/Registrar.php new file mode 100644 index 0000000..9d00c6a --- /dev/null +++ b/tests/_support/Config/Registrar.php @@ -0,0 +1,27 @@ + [ + 'first', + 'second', + ], + 'format' => 'nice', + 'fruit' => [ + 'apple', + 'banana', + ], + ]; + } + +} diff --git a/tests/_support/Config/Routes.php b/tests/_support/Config/Routes.php new file mode 100644 index 0000000..2369e33 --- /dev/null +++ b/tests/_support/Config/Routes.php @@ -0,0 +1,7 @@ +add('testing', 'TestController::index'); diff --git a/tests/_support/Controllers/Popcorn.php b/tests/_support/Controllers/Popcorn.php new file mode 100644 index 0000000..8af998d --- /dev/null +++ b/tests/_support/Controllers/Popcorn.php @@ -0,0 +1,77 @@ +respond('Oops', 567, 'Surprise'); + } + + public function popper() + { + throw new \RuntimeException('Surprise', 500); + } + + public function weasel() + { + $this->respond('', 200); + } + + public function oops() + { + $this->failUnauthorized(); + } + + public function goaway() + { + return redirect()->to('/'); + } + + // @see https://github.com/codeigniter4/CodeIgniter4/issues/1834 + public function index3() + { + $response = $this->response->setJSON([ + 'lang' => $this->request->getLocale(), + ]); + + // echo var_dump($this->response->getBody()); + return $response; + } + + public function canyon() + { + echo 'Hello-o-o'; + } + + public function cat() + { + } + + public function json() + { + $this->responsd(['answer' => 42]); + } + + public function xml() + { + $this->respond('cat'); + } + +} diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php new file mode 100644 index 0000000..b980a80 --- /dev/null +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -0,0 +1,148 @@ +db->DBDriver === 'SQLite3' ? 'unique' : 'auto_increment'; + + // User Table + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 3, + $unique_or_auto => true, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 80, + ], + 'email' => [ + 'type' => 'VARCHAR', + 'constraint' => 100, + ], + 'country' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'deleted_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('user', true); + + // Job Table + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 3, + $unique_or_auto => true, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + ], + 'description' => [ + 'type' => 'TEXT', + 'null' => true, + ], + 'created_at' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + 'null' => true, + ], + 'deleted_at' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('job', true); + + // Misc Table + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 3, + $unique_or_auto => true, + ], + 'key' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + ], + 'value' => ['type' => 'TEXT'], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('misc', true); + + // Empty Table + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 3, + $unique_or_auto => true, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + ], + 'created_at' => [ + 'type' => 'DATE', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATE', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('empty', true); + + // Secondary Table + $this->forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 3, + $unique_or_auto => true, + ], + 'key' => [ + 'type' => 'VARCHAR', + 'constraint' => 40, + ], + 'value' => ['type' => 'TEXT'], + ]); + $this->forge->addKey('id', true); + $this->forge->createTable('secondary', true); + } + + //-------------------------------------------------------------------- + + public function down() + { + $this->forge->dropTable('user', true); + $this->forge->dropTable('job', true); + $this->forge->dropTable('misc', true); + $this->forge->dropTable('empty', true); + $this->forge->dropTable('secondary', true); + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/_support/Database/MockBuilder.php b/tests/_support/Database/MockBuilder.php new file mode 100644 index 0000000..98c7fb6 --- /dev/null +++ b/tests/_support/Database/MockBuilder.php @@ -0,0 +1,15 @@ +returnValues[$method] = $return; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Orchestrates a query against the database. Queries must use + * Database\Statement objects to store the query and build it. + * This method works with the cache. + * + * Should automatically handle different connections for read/write + * queries if needed. + * + * @param string $sql + * @param mixed ...$binds + * @param boolean $setEscapeFlags + * @param string $queryClass + * + * @return \CodeIgniter\Database\BaseResult|\CodeIgniter\Database\Query|false + */ + + public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = 'CodeIgniter\\Database\\Query') + { + $queryClass = str_replace('Connection', 'Query', get_class($this)); + + $query = new $queryClass($this); + + $query->setQuery($sql, $binds, $setEscapeFlags); + + if (! empty($this->swapPre) && ! empty($this->DBPrefix)) + { + $query->swapPrefix($this->DBPrefix, $this->swapPre); + } + + $startTime = microtime(true); + + $this->lastQuery = $query; + + // Run the query + if (false === ($this->resultID = $this->simpleQuery($query->getQuery()))) + { + $query->setDuration($startTime, $startTime); + + // @todo deal with errors + + return false; + } + + $query->setDuration($startTime); + + $resultClass = str_replace('Connection', 'Result', get_class($this)); + + return new $resultClass($this->connID, $this->resultID); + } + + //-------------------------------------------------------------------- + + /** + * Connect to the database. + * + * @param boolean $persistent + * + * @return mixed + */ + public function connect(bool $persistent = false) + { + $return = $this->returnValues['connect'] ?? true; + + if (is_array($return)) + { + // By removing the top item here, we can + // get a different value for, say, testing failover connections. + $return = array_shift($this->returnValues['connect']); + } + + return $return; + } + + //-------------------------------------------------------------------- + + /** + * Keep or establish the connection if no queries have been sent for + * a length of time exceeding the server's idle timeout. + * + * @return boolean + */ + public function reconnect(): bool + { + return true; + } + + //-------------------------------------------------------------------- + + /** + * Select a specific database table to use. + * + * @param string $databaseName + * + * @return mixed + */ + public function setDatabase(string $databaseName) + { + $this->database = $databaseName; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Returns a string containing the version of the database being used. + * + * @return string + */ + public function getVersion(): string + { + return CodeIgniter::CI_VERSION; + } + + //-------------------------------------------------------------------- + + /** + * Executes the query against the database. + * + * @param string $sql + * + * @return mixed + */ + protected function execute(string $sql) + { + return $this->returnValues['execute']; + } + + //-------------------------------------------------------------------- + + /** + * Returns the total number of rows affected by this query. + * + * @return int + */ + public function affectedRows(): int + { + return 1; + } + + //-------------------------------------------------------------------- + + /** + * Returns the last error code and message. + * + * Must return an array with keys 'code' and 'message': + * + * return ['code' => null, 'message' => null); + * + * @return array + */ + public function error(): array + { + return [ + 'code' => null, + 'message' => null, + ]; + } + + //-------------------------------------------------------------------- + + /** + * Insert ID + * + * @return integer + */ + public function insertID(): int + { + return $this->connID->insert_id; + } + + //-------------------------------------------------------------------- + + /** + * Generates the SQL for listing tables in a platform-dependent manner. + * + * @param boolean $constrainByPrefix + * + * @return string + */ + protected function _listTables(bool $constrainByPrefix = false): string + { + return ''; + } + + //-------------------------------------------------------------------- + + /** + * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string $table + * + * @return string + */ + protected function _listColumns(string $table = ''): string + { + return ''; + } + + /** + * @param string $table + * @return array + */ + protected function _fieldData(string $table): array + { + return []; + } + + /** + * @param string $table + * @return array + */ + protected function _indexData(string $table): array + { + return []; + } + + /** + * @param string $table + * @return array + */ + protected function _foreignKeyData(string $table): array + { + return []; + } + + //-------------------------------------------------------------------- + + /** + * Close the connection. + */ + protected function _close() + { + return; + } + + //-------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return boolean + */ + protected function _transBegin(): bool + { + return true; + } + + //-------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return boolean + */ + protected function _transCommit(): bool + { + return true; + } + + //-------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return boolean + */ + protected function _transRollback(): bool + { + return true; + } + + //-------------------------------------------------------------------- +} diff --git a/tests/_support/Database/MockQuery.php b/tests/_support/Database/MockQuery.php new file mode 100644 index 0000000..0008104 --- /dev/null +++ b/tests/_support/Database/MockQuery.php @@ -0,0 +1,8 @@ + [ + [ + 'name' => 'Derek Jones', + 'email' => 'derek@world.com', + 'country' => 'US', + ], + [ + 'name' => 'Ahmadinejad', + 'email' => 'ahmadinejad@world.com', + 'country' => 'Iran', + ], + [ + 'name' => 'Richard A Causey', + 'email' => 'richard@world.com', + 'country' => 'US', + ], + [ + 'name' => 'Chris Martin', + 'email' => 'chris@world.com', + 'country' => 'UK', + ], + ], + 'job' => [ + [ + 'name' => 'Developer', + 'description' => 'Awesome job, but sometimes makes you bored', + ], + [ + 'name' => 'Politician', + 'description' => 'This is not really a job', + ], + [ + 'name' => 'Accountant', + 'description' => 'Boring job, but you will get free snack at lunch', + ], + [ + 'name' => 'Musician', + 'description' => 'Only Coldplay can actually called Musician', + ], + ], + 'misc' => [ + [ + 'key' => '\\xxxfoo456', + 'value' => 'Entry with \\xxx', + ], + [ + 'key' => '\\%foo456', + 'value' => 'Entry with \\%', + ], + [ + 'key' => 'spaces and tabs', + 'value' => ' One two three tab', + ], + ], + ]; + + foreach ($data as $table => $dummy_data) + { + $this->db->table($table)->truncate(); + + foreach ($dummy_data as $single_dummy_data) + { + $this->db->table($table)->insert($single_dummy_data); + } + } + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/_support/Database/SupportMigrations/001_Some_migration.php b/tests/_support/Database/SupportMigrations/001_Some_migration.php new file mode 100644 index 0000000..e2ace64 --- /dev/null +++ b/tests/_support/Database/SupportMigrations/001_Some_migration.php @@ -0,0 +1,24 @@ +forge->addField([ + 'key' => [ + 'type' => 'VARCHAR', + 'constraint' => 255, + ], + ]); + $this->forge->createTable('foo', true); + + $this->db->table('foo')->insert([ + 'key' => 'foobar', + ]); + } + + public function down() + { + $this->forge->dropTable('foo', true); + } +} diff --git a/tests/_support/Events/MockEvents.php b/tests/_support/Events/MockEvents.php new file mode 100644 index 0000000..eb9186b --- /dev/null +++ b/tests/_support/Events/MockEvents.php @@ -0,0 +1,66 @@ +output = $output; + + return $this; + } + + //-------------------------------------------------------------------- + + protected function sendRequest(array $curl_options = []): string + { + // Save so we can access later. + $this->curl_options = $curl_options; + + return $this->output; + } + + //-------------------------------------------------------------------- + // for testing purposes only + public function getBaseURI() + { + return $this->baseURI; + } + + // for testing purposes only + public function getDelay() + { + return $this->delay; + } + +} diff --git a/tests/_support/HTTP/MockIncomingRequest.php b/tests/_support/HTTP/MockIncomingRequest.php new file mode 100644 index 0000000..627fc9a --- /dev/null +++ b/tests/_support/HTTP/MockIncomingRequest.php @@ -0,0 +1,17 @@ +pretend; + } + + // artificial error for testing + public function misbehave() + { + $this->statusCode = 0; + } + +} diff --git a/tests/_support/Images/EXIFsamples/down-mirrored.jpg b/tests/_support/Images/EXIFsamples/down-mirrored.jpg new file mode 100644 index 0000000..34a7b1d --- /dev/null +++ b/tests/_support/Images/EXIFsamples/down-mirrored.jpg Binary files differ diff --git a/tests/_support/Images/EXIFsamples/down.jpg b/tests/_support/Images/EXIFsamples/down.jpg new file mode 100644 index 0000000..9077a7c --- /dev/null +++ b/tests/_support/Images/EXIFsamples/down.jpg Binary files differ diff --git a/tests/_support/Images/EXIFsamples/left-mirrored.jpg b/tests/_support/Images/EXIFsamples/left-mirrored.jpg new file mode 100644 index 0000000..1832702 --- /dev/null +++ b/tests/_support/Images/EXIFsamples/left-mirrored.jpg Binary files differ diff --git a/tests/_support/Images/EXIFsamples/left.jpg b/tests/_support/Images/EXIFsamples/left.jpg new file mode 100644 index 0000000..ad1f898 --- /dev/null +++ b/tests/_support/Images/EXIFsamples/left.jpg Binary files differ diff --git a/tests/_support/Images/EXIFsamples/right-mirrored.jpg b/tests/_support/Images/EXIFsamples/right-mirrored.jpg new file mode 100644 index 0000000..cc8a29a --- /dev/null +++ b/tests/_support/Images/EXIFsamples/right-mirrored.jpg Binary files differ diff --git a/tests/_support/Images/EXIFsamples/right.jpg b/tests/_support/Images/EXIFsamples/right.jpg new file mode 100644 index 0000000..183ffeb --- /dev/null +++ b/tests/_support/Images/EXIFsamples/right.jpg Binary files differ diff --git a/tests/_support/Images/EXIFsamples/up-mirrored.jpg b/tests/_support/Images/EXIFsamples/up-mirrored.jpg new file mode 100644 index 0000000..e1865a5 --- /dev/null +++ b/tests/_support/Images/EXIFsamples/up-mirrored.jpg Binary files differ diff --git a/tests/_support/Images/EXIFsamples/up.jpg b/tests/_support/Images/EXIFsamples/up.jpg new file mode 100644 index 0000000..70fc26f --- /dev/null +++ b/tests/_support/Images/EXIFsamples/up.jpg Binary files differ diff --git a/tests/_support/Images/Steveston_dusk.JPG b/tests/_support/Images/Steveston_dusk.JPG new file mode 100644 index 0000000..c3b9b12 --- /dev/null +++ b/tests/_support/Images/Steveston_dusk.JPG Binary files differ diff --git a/tests/_support/Images/ci-logo.gif b/tests/_support/Images/ci-logo.gif new file mode 100644 index 0000000..3001b2f --- /dev/null +++ b/tests/_support/Images/ci-logo.gif Binary files differ diff --git a/tests/_support/Images/ci-logo.jpeg b/tests/_support/Images/ci-logo.jpeg new file mode 100644 index 0000000..1b0178b --- /dev/null +++ b/tests/_support/Images/ci-logo.jpeg Binary files differ diff --git a/tests/_support/Images/ci-logo.png b/tests/_support/Images/ci-logo.png new file mode 100644 index 0000000..34fb010 --- /dev/null +++ b/tests/_support/Images/ci-logo.png Binary files differ diff --git a/tests/_support/Language/MockLanguage.php b/tests/_support/Language/MockLanguage.php new file mode 100644 index 0000000..49301db --- /dev/null +++ b/tests/_support/Language/MockLanguage.php @@ -0,0 +1,61 @@ +language[$locale ?? $this->locale][$file] = $data; + + return $this; + } + + //-------------------------------------------------------------------- + + /** + * Provides an override that allows us to set custom + * data to be returned easily during testing. + * + * @param string $path + * + * @return array|mixed + */ + protected function requireFile(string $path): array + { + return $this->data ?? []; + } + + //-------------------------------------------------------------------- + + /** + * Arbitrarily turnoff internationalization support for testing + */ + public function disableIntlSupport() + { + $this->intlSupport = false; + } + +} diff --git a/tests/_support/Language/SecondMockLanguage.php b/tests/_support/Language/SecondMockLanguage.php new file mode 100644 index 0000000..7142885 --- /dev/null +++ b/tests/_support/Language/SecondMockLanguage.php @@ -0,0 +1,27 @@ +load($file, $locale, $return); + } + + //-------------------------------------------------------------------- + + /** + * Expose the loaded language files + */ + public function loaded(string $locale = 'en') + { + return $this->loadedFiles[$locale]; + } + +} diff --git a/tests/_support/Language/ab-CD/Allin.php b/tests/_support/Language/ab-CD/Allin.php new file mode 100644 index 0000000..3b15388 --- /dev/null +++ b/tests/_support/Language/ab-CD/Allin.php @@ -0,0 +1,8 @@ + 'Pyramid of Giza', + 'tre' => 'Colossus of Rhodes', + 'fiv' => 'Temple of Artemis', + 'sev' => 'Hanging Gardens of Babylon', +]; diff --git a/tests/_support/Language/ab/Allin.php b/tests/_support/Language/ab/Allin.php new file mode 100644 index 0000000..6912075 --- /dev/null +++ b/tests/_support/Language/ab/Allin.php @@ -0,0 +1,8 @@ + 'gluttony', + 'tre' => 'greed', + 'six' => 'envy', + 'sev' => 'pride', +]; diff --git a/tests/_support/Language/en-ZZ/More.php b/tests/_support/Language/en-ZZ/More.php new file mode 100644 index 0000000..6681020 --- /dev/null +++ b/tests/_support/Language/en-ZZ/More.php @@ -0,0 +1,6 @@ + 'These are not the droids you are looking for', + 'notaMoon' => "It's made of cheese", + 'wisdom' => 'There is no try', +]; diff --git a/tests/_support/Language/en/Allin.php b/tests/_support/Language/en/Allin.php new file mode 100644 index 0000000..6a10dcc --- /dev/null +++ b/tests/_support/Language/en/Allin.php @@ -0,0 +1,8 @@ + 'four calling birds', + 'fiv' => 'five golden rings', + 'six' => 'six geese a laying', + 'sev' => 'seven swans a swimming', +]; diff --git a/tests/_support/Language/en/Core.php b/tests/_support/Language/en/Core.php new file mode 100644 index 0000000..b2fc4c1 --- /dev/null +++ b/tests/_support/Language/en/Core.php @@ -0,0 +1,20 @@ + '{0} extension could not be found.', + 'bazillion' => 'billions and billions', // adds a new setting +]; diff --git a/tests/_support/Language/en/More.php b/tests/_support/Language/en/More.php new file mode 100644 index 0000000..5b4eaea --- /dev/null +++ b/tests/_support/Language/en/More.php @@ -0,0 +1,7 @@ + 'These are not the droids you are looking for', + 'notaMoon' => "That's no moon... it's a space station", + 'cannotMove' => 'I have a very bad feeling about this', +]; diff --git a/tests/_support/Language/ru/Language.php b/tests/_support/Language/ru/Language.php new file mode 100644 index 0000000..d6c6632 --- /dev/null +++ b/tests/_support/Language/ru/Language.php @@ -0,0 +1,5 @@ + 'Whatever this would be, translated', +]; diff --git a/tests/_support/Log/Handlers/MockChromeHandler.php b/tests/_support/Log/Handlers/MockChromeHandler.php new file mode 100644 index 0000000..1a64b22 --- /dev/null +++ b/tests/_support/Log/Handlers/MockChromeHandler.php @@ -0,0 +1,25 @@ +json['rows'][0]; + } + +} diff --git a/tests/_support/Log/Handlers/MockFileHandler.php b/tests/_support/Log/Handlers/MockFileHandler.php new file mode 100644 index 0000000..efd72a9 --- /dev/null +++ b/tests/_support/Log/Handlers/MockFileHandler.php @@ -0,0 +1,25 @@ +handles = $config['handles'] ?? []; + $this->destination = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension; + } + +} diff --git a/tests/_support/Log/Handlers/TestHandler.php b/tests/_support/Log/Handlers/TestHandler.php new file mode 100644 index 0000000..e22559c --- /dev/null +++ b/tests/_support/Log/Handlers/TestHandler.php @@ -0,0 +1,64 @@ +handles = $config['handles'] ?? []; + $this->destination = $this->path . 'log-' . date('Y-m-d') . '.' . $this->fileExtension; + + self::$logs = []; + } + + //-------------------------------------------------------------------- + + /** + * Handles logging the message. + * If the handler returns false, then execution of handlers + * will stop. Any handlers that have not run, yet, will not + * be run. + * + * @param $level + * @param $message + * + * @return boolean + */ + public function handle($level, $message): bool + { + $date = date($this->dateFormat); + + self::$logs[] = strtoupper($level) . ' - ' . $date . ' --> ' . $message; + + return true; + } + + //-------------------------------------------------------------------- + + public static function getLogs() + { + return self::$logs; + } + + //-------------------------------------------------------------------- +} diff --git a/tests/_support/Log/TestLogger.php b/tests/_support/Log/TestLogger.php new file mode 100644 index 0000000..a88eb62 --- /dev/null +++ b/tests/_support/Log/TestLogger.php @@ -0,0 +1,82 @@ +assertLogged() methods. + * + * @param string $level + * @param string $message + * @param array $context + * + * @return boolean + */ + public function log($level, $message, array $context = []): bool + { + // While this requires duplicate work, we want to ensure + // we have the final message to test against. + $log_message = $this->interpolate($message, $context); + + // Determine the file and line by finding the first + // backtrace that is not part of our logging system. + $trace = debug_backtrace(); + $file = null; + + foreach ($trace as $row) + { + if (! in_array($row['function'], ['log', 'log_message'])) + { + $file = basename($row['file'] ?? ''); + break; + } + } + + self::$op_logs[] = [ + 'level' => $level, + 'message' => $log_message, + 'file' => $file, + ]; + + // Let the parent do it's thing. + return parent::log($level, $message, $context); + } + + //-------------------------------------------------------------------- + + /** + * Used by CIUnitTestCase class to provide ->assertLogged() methods. + * + * @param string $level + * @param string $message + * + * @return boolean + */ + public static function didLog(string $level, $message) + { + foreach (self::$op_logs as $log) + { + if (strtolower($log['level']) === strtolower($level) && $message === $log['message']) + { + return true; + } + } + + return false; + } + + //-------------------------------------------------------------------- + // Expose cleanFileNames() + public function cleanup($file) + { + return $this->cleanFileNames($file); + } + +} diff --git a/tests/_support/MockCodeIgniter.php b/tests/_support/MockCodeIgniter.php new file mode 100644 index 0000000..3e31d00 --- /dev/null +++ b/tests/_support/MockCodeIgniter.php @@ -0,0 +1,11 @@ +tokens[] = 'beforeInsert'; + + return $data; + } + + protected function afterInsertMethod(array $data) + { + $this->tokens[] = 'afterInsert'; + + return $data; + } + + protected function beforeUpdateMethod(array $data) + { + $this->tokens[] = 'beforeUpdate'; + + return $data; + } + + protected function afterUpdateMethod(array $data) + { + $this->tokens[] = 'afterUpdate'; + + return $data; + } + + protected function afterFindMethod(array $data) + { + $this->tokens[] = 'afterFind'; + + return $data; + } + + protected function afterDeleteMethod(array $data) + { + $this->tokens[] = 'afterDelete'; + + return $data; + } + + public function hasToken(string $token) + { + return in_array($token, $this->tokens); + } + +} diff --git a/tests/_support/Models/JobModel.php b/tests/_support/Models/JobModel.php new file mode 100644 index 0000000..98c4ad2 --- /dev/null +++ b/tests/_support/Models/JobModel.php @@ -0,0 +1,23 @@ + [ + 'required', + 'min_length[10]', + 'errors' => [ + 'min_length' => 'Minimum Length Error', + ] + ], + 'token' => 'in_list[{id}]', + ]; +} diff --git a/tests/_support/Models/ValidModel.php b/tests/_support/Models/ValidModel.php new file mode 100644 index 0000000..69842d8 --- /dev/null +++ b/tests/_support/Models/ValidModel.php @@ -0,0 +1,34 @@ + [ + 'required', + 'min_length[3]', + ], + 'token' => 'in_list[{id}]', + ]; + + protected $validationMessages = [ + 'name' => [ + 'required' => 'You forgot to name the baby.', + 'min_length' => 'Too short, man!', + ], + ]; +} diff --git a/tests/_support/Security/MockSecurity.php b/tests/_support/Security/MockSecurity.php new file mode 100644 index 0000000..7a9239f --- /dev/null +++ b/tests/_support/Security/MockSecurity.php @@ -0,0 +1,17 @@ +CSRFHash; + + return $this; + } + + //-------------------------------------------------------------------- + +} diff --git a/tests/_support/Services.php b/tests/_support/Services.php new file mode 100644 index 0000000..48e8283 --- /dev/null +++ b/tests/_support/Services.php @@ -0,0 +1,70 @@ +driver, true); + } + + //-------------------------------------------------------------------- + + /** + * Starts the session. + * Extracted for testing reasons. + */ + protected function startSession() + { + // session_start(); + } + + //-------------------------------------------------------------------- + + /** + * Takes care of setting the cookie on the client side. + * Extracted for testing reasons. + */ + protected function setCookie() + { + $this->cookies[] = [ + $this->sessionCookieName, + session_id(), + (empty($this->sessionExpiration) ? 0 : time() + $this->sessionExpiration), + $this->cookiePath, + $this->cookieDomain, + $this->cookieSecure, + true, + ]; + } + + //-------------------------------------------------------------------- + + public function regenerate(bool $destroy = false) + { + $this->didRegenerate = true; + $_SESSION['__ci_last_regenerate'] = time(); + } + + //-------------------------------------------------------------------- +} diff --git a/tests/_support/SomeEntity.php b/tests/_support/SomeEntity.php new file mode 100644 index 0000000..3d22993 --- /dev/null +++ b/tests/_support/SomeEntity.php @@ -0,0 +1,14 @@ + null, + 'bar' => null, + ]; + +} diff --git a/tests/_support/Validation/TestRules.php b/tests/_support/Validation/TestRules.php new file mode 100644 index 0000000..2c40f32 --- /dev/null +++ b/tests/_support/Validation/TestRules.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/tests/_support/View/Views/simpler.php b/tests/_support/View/Views/simpler.php new file mode 100644 index 0000000..0588b62 --- /dev/null +++ b/tests/_support/View/Views/simpler.php @@ -0,0 +1 @@ +

{testString}

\ No newline at end of file diff --git a/tests/_support/_bootstrap.php b/tests/_support/_bootstrap.php new file mode 100644 index 0000000..65dd9a1 --- /dev/null +++ b/tests/_support/_bootstrap.php @@ -0,0 +1,32 @@ +