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('