Initial project files: BESCMS full source

This commit is contained in:
bes
2026-08-13 23:37:46 +08:00
parent fa6cddb540
commit 2e26f85723
2166 changed files with 396625 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache;
use CodeIgniter\Cache\Exceptions\CacheException;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\Test\Mock\MockCache;
use Config\Cache;
/**
* A factory for loading the desired
*
* @see \CodeIgniter\Cache\CacheFactoryTest
*/
class CacheFactory
{
/**
* The class to use when mocking
*
* @var string
*/
public static $mockClass = MockCache::class;
/**
* The service to inject the mock as
*
* @var string
*/
public static $mockServiceName = 'cache';
/**
* Attempts to create the desired cache handler, based upon the
*
* @param non-empty-string|null $handler
* @param non-empty-string|null $backup
*
* @return CacheInterface
*/
public static function getHandler(Cache $config, ?string $handler = null, ?string $backup = null)
{
if (! isset($config->validHandlers) || $config->validHandlers === []) {
throw CacheException::forInvalidHandlers();
}
if (! isset($config->handler) || ! isset($config->backupHandler)) {
throw CacheException::forNoBackup();
}
$handler ??= $config->handler;
$backup ??= $config->backupHandler;
if (! array_key_exists($handler, $config->validHandlers) || ! array_key_exists($backup, $config->validHandlers)) {
throw CacheException::forHandlerNotFound();
}
$adapter = new $config->validHandlers[$handler]($config);
if (! $adapter->isSupported()) {
$adapter = new $config->validHandlers[$backup]($config);
if (! $adapter->isSupported()) {
// Fall back to the dummy adapter.
$adapter = new $config->validHandlers['dummy']();
}
}
// If $adapter->initialize() 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_message('critical', $e . ' 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;
}
}
+106
View File
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache;
interface CacheInterface
{
/**
* Takes care of any handler-specific setup that must be done.
*
* @return void
*/
public function initialize();
/**
* Attempts to fetch an item from the cache store.
*
* @param string $key Cache item name
*
* @return mixed
*/
public function get(string $key);
/**
* Saves an item to the cache store.
*
* @param string $key Cache item name
* @param mixed $value The data to save
* @param int $ttl Time To Live, in seconds (default 60)
*
* @return bool Success or failure
*/
public function save(string $key, $value, int $ttl = 60);
/**
* Deletes a specific item from the cache store.
*
* @param string $key Cache item name
*
* @return bool Success or failure
*/
public function delete(string $key);
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param int $offset Step/value to increase by
*
* @return bool|int
*/
public function increment(string $key, int $offset = 1);
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param int $offset Step/value to increase by
*
* @return bool|int
*/
public function decrement(string $key, int $offset = 1);
/**
* Will delete all items in the entire cache.
*
* @return bool Success or failure
*/
public function clean();
/**
* Returns information on the entire cache.
*
* The information returned and the structure of the data
* varies depending on the handler.
*
* @return array<array-key, mixed>|false|object|null
*/
public function getCacheInfo();
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return array<string, mixed>|false|null Returns null if the item does not exist, otherwise array<string, mixed>
* with at least the 'expire' key for absolute epoch expiry (or null).
* Some handlers may return false when an item does not exist, which is deprecated.
*/
public function getMetaData(string $key);
/**
* Determines if the driver is supported on this system.
*/
public function isSupported(): bool;
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\Exceptions;
use CodeIgniter\Exceptions\DebugTraceableTrait;
use CodeIgniter\Exceptions\RuntimeException;
class CacheException extends RuntimeException
{
use DebugTraceableTrait;
/**
* Thrown when handler has no permission to write cache.
*
* @return static
*/
public static function forUnableToWrite(string $path)
{
return new static(lang('Cache.unableToWrite', [$path]));
}
/**
* Thrown when an unrecognized handler is used.
*
* @return static
*/
public static function forInvalidHandlers()
{
return new static(lang('Cache.invalidHandlers'));
}
/**
* Thrown when no backup handler is setup in config.
*
* @return static
*/
public static function forNoBackup()
{
return new static(lang('Cache.noBackup'));
}
/**
* Thrown when specified handler was not found.
*
* @return static
*/
public static function forHandlerNotFound()
{
return new static(lang('Cache.handlerNotFound'));
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache;
use CodeIgniter\Cache\FactoriesCache\FileVarExportHandler;
use CodeIgniter\Config\Factories;
final class FactoriesCache
{
private readonly CacheInterface|FileVarExportHandler $cache;
public function __construct(CacheInterface|FileVarExportHandler|null $cache = null)
{
$this->cache = $cache ?? new FileVarExportHandler();
}
public function save(string $component): void
{
if (! Factories::isUpdated($component)) {
return;
}
$data = Factories::getComponentInstances($component);
$this->cache->save($this->getCacheKey($component), $data, 3600 * 24);
}
private function getCacheKey(string $component): string
{
return 'FactoriesCache_' . $component;
}
public function load(string $component): bool
{
$key = $this->getCacheKey($component);
$data = $this->cache->get($key);
if (! is_array($data) || $data === []) {
return false;
}
Factories::setComponentInstances($component, $data);
return true;
}
public function delete(string $component): void
{
$this->cache->delete($this->getCacheKey($component));
}
}
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\FactoriesCache;
final class FileVarExportHandler
{
private string $path = WRITEPATH . 'cache';
public function save(string $key, mixed $val): void
{
$val = var_export($val, true);
// Write to temp file first to ensure atomicity
$tmp = $this->path . "/{$key}." . uniqid('', true) . '.tmp';
file_put_contents($tmp, '<?php return ' . $val . ';', LOCK_EX);
rename($tmp, $this->path . "/{$key}");
}
public function delete(string $key): void
{
@unlink($this->path . "/{$key}");
}
public function get(string $key): mixed
{
return @include $this->path . "/{$key}";
}
}
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\Handlers;
use Closure;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Exceptions\BadMethodCallException;
use CodeIgniter\Exceptions\InvalidArgumentException;
use Config\Cache;
use Exception;
/**
* Base class for cache handling
*
* @see \CodeIgniter\Cache\Handlers\BaseHandlerTest
*/
abstract class BaseHandler implements CacheInterface
{
/**
* Reserved characters that cannot be used in a key or tag. May be overridden by the config.
* From https://github.com/symfony/cache-contracts/blob/c0446463729b89dd4fa62e9aeecc80287323615d/ItemInterface.php#L43
*
* @deprecated in favor of the Cache config
*/
public const RESERVED_CHARACTERS = '{}()/\@:';
/**
* Maximum key length.
*/
public const MAX_KEY_LENGTH = PHP_INT_MAX;
/**
* Prefix to apply to cache keys.
* May not be used by all handlers.
*
* @var string
*/
protected $prefix;
/**
* Validates a cache key according to PSR-6.
* Keys that exceed MAX_KEY_LENGTH are hashed.
* From https://github.com/symfony/cache/blob/7b024c6726af21fd4984ac8d1eae2b9f3d90de88/CacheItem.php#L158
*
* @param mixed $key The key to validate
* @param string $prefix Optional prefix to include in length calculations
*
* @throws InvalidArgumentException When $key is not valid
*/
public static function validateKey($key, $prefix = ''): string
{
if (! is_string($key)) {
throw new InvalidArgumentException('Cache key must be a string');
}
if ($key === '') {
throw new InvalidArgumentException('Cache key cannot be empty.');
}
$reserved = config(Cache::class)->reservedCharacters;
if ($reserved !== '' && strpbrk($key, $reserved) !== false) {
throw new InvalidArgumentException('Cache key contains reserved characters ' . $reserved);
}
// If the key with prefix exceeds the length then return the hashed version
return strlen($prefix . $key) > static::MAX_KEY_LENGTH ? $prefix . md5($key) : $prefix . $key;
}
/**
* Get an item from the cache, or execute the given Closure and store the result.
*
* @param string $key Cache item name
* @param int $ttl Time to live
* @param Closure(): mixed $callback Callback return value
*
* @return mixed
*/
public function remember(string $key, int $ttl, Closure $callback)
{
$value = $this->get($key);
if ($value !== null) {
return $value;
}
$this->save($key, $value = $callback(), $ttl);
return $value;
}
/**
* Deletes items from the cache store matching a given pattern.
*
* @param string $pattern Cache items glob-style pattern
*
* @return int
*
* @throws Exception
*/
public function deleteMatching(string $pattern)
{
throw new BadMethodCallException('The deleteMatching method is not implemented.');
}
}
@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\Handlers;
use Closure;
/**
* Dummy cache handler
*
* @see \CodeIgniter\Cache\Handlers\DummyHandlerTest
*/
class DummyHandler extends BaseHandler
{
/**
* {@inheritDoc}
*/
public function initialize()
{
}
/**
* {@inheritDoc}
*/
public function get(string $key)
{
return null;
}
/**
* {@inheritDoc}
*/
public function remember(string $key, int $ttl, Closure $callback)
{
return null;
}
/**
* {@inheritDoc}
*/
public function save(string $key, $value, int $ttl = 60)
{
return true;
}
/**
* {@inheritDoc}
*/
public function delete(string $key)
{
return true;
}
/**
* {@inheritDoc}
*
* @return int
*/
public function deleteMatching(string $pattern)
{
return 0;
}
/**
* {@inheritDoc}
*/
public function increment(string $key, int $offset = 1)
{
return true;
}
/**
* {@inheritDoc}
*/
public function decrement(string $key, int $offset = 1)
{
return true;
}
/**
* {@inheritDoc}
*/
public function clean()
{
return true;
}
/**
* {@inheritDoc}
*/
public function getCacheInfo()
{
return null;
}
/**
* {@inheritDoc}
*/
public function getMetaData(string $key)
{
return null;
}
/**
* {@inheritDoc}
*/
public function isSupported(): bool
{
return true;
}
}
@@ -0,0 +1,472 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Cache\Exceptions\CacheException;
use CodeIgniter\I18n\Time;
use Config\Cache;
use Throwable;
/**
* File system cache handler
*
* @see \CodeIgniter\Cache\Handlers\FileHandlerTest
*/
class FileHandler extends BaseHandler
{
/**
* Maximum key length.
*/
public const MAX_KEY_LENGTH = 255;
/**
* Where to store cached files on the disk.
*
* @var string
*/
protected $path;
/**
* Mode for the stored files.
* Must be chmod-safe (octal).
*
* @var int
*
* @see https://www.php.net/manual/en/function.chmod.php
*/
protected $mode;
/**
* Note: Use `CacheFactory::getHandler()` to instantiate.
*
* @throws CacheException
*/
public function __construct(Cache $config)
{
/*
$options = [
...['storePath' => WRITEPATH . 'cache', 'mode' => 0640],
...$config->file,
];*/
$options = array_merge(
['storePath' => WRITEPATH . 'cache', 'mode' => 0640],
$config->file ?? []
);
$this->path = $options['storePath'] !== '' ? $options['storePath'] : WRITEPATH . 'cache';
$this->path = rtrim($this->path, '\\/') . '/';
if (! is_really_writable($this->path)) {
throw CacheException::forUnableToWrite($this->path);
}
$this->mode = $options['mode'];
$this->prefix = $config->prefix;
helper('filesystem');
}
/**
* {@inheritDoc}
*/
public function initialize()
{
}
/**
* {@inheritDoc}
*/
public function get(string $key)
{
$key = static::validateKey($key, $this->prefix);
$data = $this->getItem($key);
return is_array($data) ? $data['data'] : null;
}
/**
* {@inheritDoc}
*/
public function save(string $key, $value, int $ttl = 60)
{
$key = static::validateKey($key, $this->prefix);
$contents = [
'time' => Time::now()->getTimestamp(),
'ttl' => $ttl,
'data' => $value,
];
if (write_file($this->path . $key, serialize($contents))) {
try {
chmod($this->path . $key, $this->mode);
// @codeCoverageIgnoreStart
} catch (Throwable $e) {
log_message('debug', 'Failed to set mode on cache file: ' . $e);
// @codeCoverageIgnoreEnd
}
return true;
}
return false;
}
/**
* {@inheritDoc}
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return is_file($this->path . $key) && unlink($this->path . $key);
}
/**
* {@inheritDoc}
*
* @return int
*/
public function deleteMatching(string $pattern)
{
$deleted = 0;
foreach (glob($this->path . $pattern, GLOB_NOSORT) as $filename) {
if (is_file($filename) && @unlink($filename)) {
$deleted++;
}
}
return $deleted;
}
/**
* {@inheritDoc}
*/
public function increment(string $key, int $offset = 1)
{
$prefixedKey = static::validateKey($key, $this->prefix);
$tmp = $this->getItem($prefixedKey);
if ($tmp === false) {
$tmp = ['data' => 0, 'ttl' => 60];
}
['data' => $value, 'ttl' => $ttl] = $tmp;
if (! is_int($value)) {
return false;
}
$value += $offset;
return $this->save($key, $value, $ttl) ? $value : false;
}
/**
* {@inheritDoc}
*/
public function decrement(string $key, int $offset = 1)
{
return $this->increment($key, -$offset);
}
/**
* {@inheritDoc}
*/
public function clean()
{
return delete_files($this->path, false, true);
}
/**
* {@inheritDoc}
*/
public function getCacheInfo()
{
return get_dir_file_info($this->path);
}
/**
* {@inheritDoc}
*/
public function getMetaData(string $key)
{
$key = static::validateKey($key, $this->prefix);
if (false === $data = $this->getItem($key)) {
return false; // @TODO This will return null in a future release
}
return [
'expire' => $data['ttl'] > 0 ? $data['time'] + $data['ttl'] : null,
'mtime' => filemtime($this->path . $key),
'data' => $data['data'],
];
}
/**
* {@inheritDoc}
*/
public function isSupported(): bool
{
return is_writable($this->path);
}
/**
* Does the heavy lifting of actually retrieving the file and
* verifying its age.
*
* @return array{data: mixed, ttl: int, time: int}|false
*/
protected function getItem(string $filename)
{
if (! is_file($this->path . $filename)) {
return false;
}
$content = @file_get_contents($this->path . $filename);
if ($content === false) {
return false;
}
try {
$data = unserialize($content);
} catch (Throwable) {
return false;
}
if (! is_array($data)) {
return false;
}
if (! isset($data['ttl']) || ! is_int($data['ttl'])) {
return false;
}
if (! isset($data['time']) || ! is_int($data['time'])) {
return false;
}
if ($data['ttl'] > 0 && Time::now()->getTimestamp() > $data['time'] + $data['ttl']) {
@unlink($this->path . $filename);
return false;
}
return $data;
}
/**
* Writes a file to disk, or returns false if not successful.
*
* @deprecated 4.6.1 Use `write_file()` instead.
*
* @param string $path
* @param string $data
* @param string $mode
*
* @return bool
*/
protected function writeFile($path, $data, $mode = 'wb')
{
if (($fp = @fopen($path, $mode)) === false) {
return false;
}
flock($fp, LOCK_EX);
$result = 0;
for ($written = 0, $length = strlen($data); $written < $length; $written += $result) {
if (($result = fwrite($fp, substr($data, $written))) === false) {
break;
}
}
flock($fp, LOCK_UN);
fclose($fp);
return is_int($result);
}
/**
* Deletes all files contained in the supplied directory path.
* Files must be writable or owned by the system in order to be deleted.
* If the second parameter is set to TRUE, any directories contained
* within the supplied base directory will be nuked as well.
*
* @deprecated 4.6.1 Use `delete_files()` instead.
*
* @param string $path File path
* @param bool $delDir Whether to delete any directories found in the path
* @param bool $htdocs Whether to skip deleting .htaccess and index page files
* @param int $_level Current directory depth level (default: 0; internal use only)
*/
protected function deleteFiles(string $path, bool $delDir = false, bool $htdocs = false, int $_level = 0): bool
{
// Trim the trailing slash
$path = rtrim($path, '/\\');
if (! $currentDir = @opendir($path)) {
return false;
}
while (false !== ($filename = @readdir($currentDir))) {
if ($filename !== '.' && $filename !== '..') {
if (is_dir($path . DIRECTORY_SEPARATOR . $filename) && $filename[0] !== '.') {
$this->deleteFiles($path . DIRECTORY_SEPARATOR . $filename, $delDir, $htdocs, $_level + 1);
} elseif (! $htdocs || preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename) !== 1) {
@unlink($path . DIRECTORY_SEPARATOR . $filename);
}
}
}
closedir($currentDir);
return ($delDir && $_level > 0) ? @rmdir($path) : true;
}
/**
* Reads the specified directory and builds an array containing the filenames,
* filesize, dates, and permissions
*
* Any sub-folders contained within the specified path are read as well.
*
* @deprecated 4.6.1 Use `get_dir_file_info()` instead.
*
* @param string $sourceDir Path to source
* @param bool $topLevelOnly Look only at the top level directory specified?
* @param bool $_recursion Internal variable to determine recursion status - do not use in calls
*
* @return array<string, array{
* name: string,
* server_path: string,
* size: int,
* date: int,
* relative_path: string,
* }>|false
*/
protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, bool $_recursion = false)
{
static $filedata = [];
$relativePath = $sourceDir;
$filePointer = @opendir($sourceDir);
if (! is_bool($filePointer)) {
// reset the array and make sure $sourceDir has a trailing slash on the initial call
if ($_recursion === false) {
$filedata = [];
$resolvedSrc = realpath($sourceDir);
$resolvedSrc = $resolvedSrc === false ? $sourceDir : $resolvedSrc;
$sourceDir = rtrim($resolvedSrc, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}
// Used to be foreach (scandir($sourceDir, 1) as $file), but scandir() is simply not as fast
while (false !== $file = readdir($filePointer)) {
if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false) {
$this->getDirFileInfo($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true);
} elseif (! is_dir($sourceDir . $file) && $file[0] !== '.') {
$filedata[$file] = $this->getFileInfo($sourceDir . $file);
$filedata[$file]['relative_path'] = $relativePath;
}
}
closedir($filePointer);
return $filedata;
}
return false;
}
/**
* Given a file and path, returns the name, path, size, date modified
* Second parameter allows you to explicitly declare what information you want returned
* Options are: name, server_path, size, date, readable, writable, executable, fileperms
* Returns FALSE if the file cannot be found.
*
* @deprecated 4.6.1 Use `get_file_info()` instead.
*
* @param string $file Path to file
* @param list<string>|string $returnedValues Array or comma separated string of information returned
*
* @return array{
* name?: string,
* server_path?: string,
* size?: int,
* date?: int,
* readable?: bool,
* writable?: bool,
* executable?: bool,
* fileperms?: int
* }|false
*/
protected function getFileInfo(string $file, $returnedValues = ['name', 'server_path', 'size', 'date'])
{
if (! is_file($file)) {
return false;
}
if (is_string($returnedValues)) {
$returnedValues = explode(',', $returnedValues);
}
$fileInfo = [];
foreach ($returnedValues as $key) {
switch ($key) {
case 'name':
$fileInfo['name'] = basename($file);
break;
case 'server_path':
$fileInfo['server_path'] = $file;
break;
case 'size':
$fileInfo['size'] = filesize($file);
break;
case 'date':
$fileInfo['date'] = filemtime($file);
break;
case 'readable':
$fileInfo['readable'] = is_readable($file);
break;
case 'writable':
$fileInfo['writable'] = is_writable($file);
break;
case 'executable':
$fileInfo['executable'] = is_executable($file);
break;
case 'fileperms':
$fileInfo['fileperms'] = fileperms($file);
break;
}
}
return $fileInfo;
}
}
@@ -0,0 +1,268 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Exceptions\BadMethodCallException;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\I18n\Time;
use Config\Cache;
use Exception;
use Memcache;
use Memcached;
/**
* Mamcached cache handler
*
* @see \CodeIgniter\Cache\Handlers\MemcachedHandlerTest
*/
class MemcachedHandler extends BaseHandler
{
/**
* The memcached object
*
* @var Memcache|Memcached
*/
protected $memcached;
/**
* Memcached Configuration
*
* @var array{host: string, port: int, weight: int, raw: bool}
*/
protected $config = [
'host' => '127.0.0.1',
'port' => 11211,
'weight' => 1,
'raw' => false,
];
/**
* Note: Use `CacheFactory::getHandler()` to instantiate.
*/
public function __construct(Cache $config)
{
$this->prefix = $config->prefix;
$this->config = array_merge($this->config, $config->memcached);
}
/**
* Closes the connection to Memcache(d) if present.
*/
public function __destruct()
{
if ($this->memcached instanceof Memcached) {
$this->memcached->quit();
} elseif ($this->memcached instanceof Memcache) {
$this->memcached->close();
}
}
/**
* {@inheritDoc}
*/
public function initialize()
{
try {
if (class_exists(Memcached::class)) {
$this->memcached = new Memcached();
if ($this->config['raw']) {
$this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
}
$this->memcached->addServer(
$this->config['host'],
$this->config['port'],
$this->config['weight'],
);
$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 (! is_array($stats) || ! isset($stats[$this->config['host'] . ':' . $this->config['port']])) {
throw new CriticalError('Cache: Memcached connection failed.');
}
} elseif (class_exists(Memcache::class)) {
$this->memcached = new Memcache();
if (! $this->memcached->connect($this->config['host'], $this->config['port'])) {
throw new CriticalError('Cache: Memcache connection failed.');
}
$this->memcached->addServer(
$this->config['host'],
$this->config['port'],
true,
$this->config['weight'],
);
} else {
throw new CriticalError('Cache: Not support Memcache(d) extension.');
}
} catch (Exception $e) {
throw new CriticalError('Cache: Memcache(d) connection refused (' . $e->getMessage() . ').');
}
}
/**
* {@inheritDoc}
*/
public function get(string $key)
{
$data = [];
$key = static::validateKey($key, $this->prefix);
if ($this->memcached instanceof Memcached) {
$data = $this->memcached->get($key);
// check for unmatched key
if ($this->memcached->getResultCode() === Memcached::RES_NOTFOUND) {
return null;
}
} elseif ($this->memcached instanceof Memcache) {
$flags = false;
$data = $this->memcached->get($key, $flags);
// check for unmatched key (i.e. $flags is untouched)
if ($flags === false) {
return null;
}
}
return is_array($data) ? $data[0] : $data;
}
/**
* {@inheritDoc}
*/
public function save(string $key, $value, int $ttl = 60)
{
$key = static::validateKey($key, $this->prefix);
if (! $this->config['raw']) {
$value = [
$value,
Time::now()->getTimestamp(),
$ttl,
];
}
if ($this->memcached instanceof Memcached) {
return $this->memcached->set($key, $value, $ttl);
}
if ($this->memcached instanceof Memcache) {
return $this->memcached->set($key, $value, 0, $ttl);
}
return false;
}
/**
* {@inheritDoc}
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return $this->memcached->delete($key);
}
/**
* {@inheritDoc}
*
* @return never
*/
public function deleteMatching(string $pattern)
{
throw new BadMethodCallException('The deleteMatching method is not implemented for Memcached. You must select File, Redis or Predis handlers to use it.');
}
/**
* {@inheritDoc}
*/
public function increment(string $key, int $offset = 1)
{
if (! $this->config['raw']) {
return false;
}
$key = static::validateKey($key, $this->prefix);
return $this->memcached->increment($key, $offset, $offset, 60);
}
/**
* {@inheritDoc}
*/
public function decrement(string $key, int $offset = 1)
{
if (! $this->config['raw']) {
return false;
}
$key = static::validateKey($key, $this->prefix);
// FIXME: third parameter isn't other handler actions.
return $this->memcached->decrement($key, $offset, $offset, 60);
}
/**
* {@inheritDoc}
*/
public function clean()
{
return $this->memcached->flush();
}
/**
* {@inheritDoc}
*/
public function getCacheInfo()
{
return $this->memcached->getStats();
}
/**
* {@inheritDoc}
*/
public function getMetaData(string $key)
{
$key = static::validateKey($key, $this->prefix);
$stored = $this->memcached->get($key);
// if not an array, don't try to count for PHP7.2
if (! is_array($stored) || count($stored) !== 3) {
return false; // @TODO This will return null in a future release
}
[$data, $time, $limit] = $stored;
return [
'expire' => $limit > 0 ? $time + $limit : null,
'mtime' => $time,
'data' => $data,
];
}
/**
* {@inheritDoc}
*/
public function isSupported(): bool
{
return extension_loaded('memcached') || extension_loaded('memcache');
}
}
@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\I18n\Time;
use Config\Cache;
use Exception;
use Predis\Client;
use Predis\Collection\Iterator\Keyspace;
use Predis\Response\Status;
/**
* Predis cache handler
*
* @see \CodeIgniter\Cache\Handlers\PredisHandlerTest
*/
class PredisHandler extends BaseHandler
{
/**
* Default config
*
* @var array{
* scheme: string,
* host: string,
* password: string|null,
* port: int,
* timeout: int
* }
*/
protected $config = [
'scheme' => 'tcp',
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
];
/**
* Predis connection
*
* @var Client
*/
protected $redis;
/**
* Note: Use `CacheFactory::getHandler()` to instantiate.
*/
public function __construct(Cache $config)
{
$this->prefix = $config->prefix;
if (isset($config->redis)) {
$this->config = array_merge($this->config, $config->redis);
}
}
/**
* {@inheritDoc}
*/
public function initialize()
{
try {
$this->redis = new Client($this->config, ['prefix' => $this->prefix]);
$this->redis->time();
} catch (Exception $e) {
throw new CriticalError('Cache: Predis connection refused (' . $e->getMessage() . ').');
}
}
/**
* {@inheritDoc}
*/
public function get(string $key)
{
$key = static::validateKey($key);
$data = array_combine(
['__ci_type', '__ci_value'],
$this->redis->hmget($key, ['__ci_type', '__ci_value']),
);
if (! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false) {
return null;
}
return match ($data['__ci_type']) {
'array', 'object' => unserialize($data['__ci_value']),
// Yes, 'double' is returned and NOT 'float'
'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null,
default => null,
};
}
/**
* {@inheritDoc}
*/
public function save(string $key, $value, int $ttl = 60)
{
$key = static::validateKey($key);
switch ($dataType = gettype($value)) {
case 'array':
case 'object':
$value = serialize($value);
break;
case 'boolean':
case 'integer':
case 'double': // Yes, 'double' is returned and NOT 'float'
case 'string':
case 'NULL':
break;
case 'resource':
default:
return false;
}
if (! $this->redis->hmset($key, ['__ci_type' => $dataType, '__ci_value' => $value]) instanceof Status) {
return false;
}
if ($ttl !== 0) {
$this->redis->expireat($key, Time::now()->getTimestamp() + $ttl);
}
return true;
}
/**
* {@inheritDoc}
*/
public function delete(string $key)
{
$key = static::validateKey($key);
return $this->redis->del($key) === 1;
}
/**
* {@inheritDoc}
*
* @return int
*/
public function deleteMatching(string $pattern)
{
$matchedKeys = [];
foreach (new Keyspace($this->redis, $pattern) as $key) {
$matchedKeys[] = $key;
}
return $this->redis->del($matchedKeys);
}
/**
* {@inheritDoc}
*/
public function increment(string $key, int $offset = 1)
{
$key = static::validateKey($key);
return $this->redis->hincrby($key, 'data', $offset);
}
/**
* {@inheritDoc}
*/
public function decrement(string $key, int $offset = 1)
{
$key = static::validateKey($key);
return $this->redis->hincrby($key, 'data', -$offset);
}
/**
* {@inheritDoc}
*
* 使用 deleteMatching 仅删除带缓存前缀的键,避免 flushdb 清空同库的 Session 等数据
*/
public function clean()
{
$pattern = $this->prefix !== '' ? $this->prefix . '*' : '*';
$deleted = $this->deleteMatching($pattern);
return $deleted >= 0;
}
/**
* {@inheritDoc}
*/
public function getCacheInfo()
{
return $this->redis->info();
}
/**
* {@inheritDoc}
*/
public function getMetaData(string $key)
{
$key = static::validateKey($key);
$data = array_combine(['__ci_value'], $this->redis->hmget($key, ['__ci_value']));
if (isset($data['__ci_value']) && $data['__ci_value'] !== false) {
$time = Time::now()->getTimestamp();
$ttl = $this->redis->ttl($key);
return [
'expire' => $ttl > 0 ? $time + $ttl : null,
'mtime' => $time,
'data' => $data['__ci_value'],
];
}
return null;
}
/**
* {@inheritDoc}
*/
public function isSupported(): bool
{
return class_exists(Client::class);
}
}
@@ -0,0 +1,268 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\I18n\Time;
use Config\Cache;
use Redis;
use RedisException;
/**
* Redis cache handler
*
* @see \CodeIgniter\Cache\Handlers\RedisHandlerTest
*/
class RedisHandler extends BaseHandler
{
/**
* Default config
*
* @var array{
* host: string,
* password: string|null,
* port: int,
* timeout: int,
* database: int,
* }
*/
protected $config = [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'database' => 0,
];
/**
* Redis connection
*
* @var Redis|null
*/
protected $redis;
/**
* Note: Use `CacheFactory::getHandler()` to instantiate.
*/
public function __construct(Cache $config)
{
$this->prefix = $config->prefix;
$this->config = array_merge($this->config, $config->redis);
}
/**
* Closes the connection to Redis if present.
*/
public function __destruct()
{
if (isset($this->redis)) {
$this->redis->close();
}
}
/**
* {@inheritDoc}
*/
public function initialize()
{
$config = $this->config;
$this->redis = new Redis();
try {
// 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 (! $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) {
throw new CriticalError('Cache: RedisException occurred with message (' . $e->getMessage() . ').');
}
}
/**
* {@inheritDoc}
*/
public function get(string $key)
{
$key = static::validateKey($key, $this->prefix);
$data = $this->redis->hMget($key, ['__ci_type', '__ci_value']);
if (! isset($data['__ci_type'], $data['__ci_value']) || $data['__ci_value'] === false) {
return null;
}
return match ($data['__ci_type']) {
'array', 'object' => unserialize($data['__ci_value']),
// Yes, 'double' is returned and NOT 'float'
'boolean', 'integer', 'double', 'string', 'NULL' => settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null,
default => null,
};
}
/**
* {@inheritDoc}
*/
public function save(string $key, $value, int $ttl = 60)
{
$key = static::validateKey($key, $this->prefix);
switch ($dataType = gettype($value)) {
case 'array':
case 'object':
$value = serialize($value);
break;
case 'boolean':
case 'integer':
case 'double': // Yes, 'double' is returned and NOT 'float'
case 'string':
case 'NULL':
break;
case 'resource':
default:
return false;
}
if (! $this->redis->hMset($key, ['__ci_type' => $dataType, '__ci_value' => $value])) {
return false;
}
if ($ttl !== 0) {
$this->redis->expireAt($key, Time::now()->getTimestamp() + $ttl);
}
return true;
}
/**
* {@inheritDoc}
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return $this->redis->del($key) === 1;
}
/**
* {@inheritDoc}
*
* @return int
*/
public function deleteMatching(string $pattern)
{
/** @var list<string> $matchedKeys */
$matchedKeys = [];
$pattern = static::validateKey($pattern, $this->prefix);
$iterator = null;
do {
/** @var false|list<string> $keys */
$keys = $this->redis->scan($iterator, $pattern);
if (is_array($keys)) {
//$matchedKeys = [...$matchedKeys, ...$keys];
$matchedKeys = array_merge($matchedKeys, $keys);
}
} while ($iterator > 0);
return $this->redis->del($matchedKeys);
}
/**
* {@inheritDoc}
*/
public function increment(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
return $this->redis->hIncrBy($key, '__ci_value', $offset);
}
/**
* {@inheritDoc}
*/
public function decrement(string $key, int $offset = 1)
{
return $this->increment($key, -$offset);
}
/**
* {@inheritDoc}
*
* 使用 deleteMatching 仅删除带缓存前缀的键,避免 flushDB 清空同库的 Session 等数据
*/
public function clean()
{
$deleted = $this->deleteMatching('*');
return $deleted >= 0;
}
/**
* {@inheritDoc}
*/
public function getCacheInfo()
{
return $this->redis->info();
}
/**
* {@inheritDoc}
*/
public function getMetaData(string $key)
{
$value = $this->get($key);
if ($value !== null) {
$time = Time::now()->getTimestamp();
$ttl = $this->redis->ttl(static::validateKey($key, $this->prefix));
assert(is_int($ttl));
return [
'expire' => $ttl > 0 ? $time + $ttl : null,
'mtime' => $time,
'data' => $value,
];
}
return null;
}
/**
* {@inheritDoc}
*/
public function isSupported(): bool
{
return extension_loaded('redis');
}
}
@@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Exceptions\BadMethodCallException;
use CodeIgniter\I18n\Time;
use Config\Cache;
/**
* Cache handler for WinCache from Microsoft & IIS.
*
* @codeCoverageIgnore
*/
class WincacheHandler extends BaseHandler
{
/**
* Note: Use `CacheFactory::getHandler()` to instantiate.
*/
public function __construct(Cache $config)
{
$this->prefix = $config->prefix;
}
/**
* {@inheritDoc}
*/
public function initialize()
{
}
/**
* {@inheritDoc}
*/
public function get(string $key)
{
$key = static::validateKey($key, $this->prefix);
$success = false;
$data = wincache_ucache_get($key, $success);
// Success returned by reference from wincache_ucache_get()
return $success ? $data : null;
}
/**
* {@inheritDoc}
*/
public function save(string $key, $value, int $ttl = 60)
{
$key = static::validateKey($key, $this->prefix);
return wincache_ucache_set($key, $value, $ttl);
}
/**
* {@inheritDoc}
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return wincache_ucache_delete($key);
}
/**
* {@inheritDoc}
*
* @return never
*/
public function deleteMatching(string $pattern)
{
throw new BadMethodCallException('The deleteMatching method is not implemented for Wincache. You must select File, Redis or Predis handlers to use it.');
}
/**
* {@inheritDoc}
*/
public function increment(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
return wincache_ucache_inc($key, $offset);
}
/**
* {@inheritDoc}
*/
public function decrement(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
return wincache_ucache_dec($key, $offset);
}
/**
* {@inheritDoc}
*/
public function clean()
{
return wincache_ucache_clear();
}
/**
* {@inheritDoc}
*/
public function getCacheInfo()
{
return wincache_ucache_info(true);
}
/**
* {@inheritDoc}
*/
public function getMetaData(string $key)
{
$key = static::validateKey($key, $this->prefix);
if ($stored = wincache_ucache_info(false, $key)) {
$age = $stored['ucache_entries'][1]['age_seconds'];
$ttl = $stored['ucache_entries'][1]['ttl_seconds'];
$hitcount = $stored['ucache_entries'][1]['hitcount'];
return [
'expire' => $ttl > 0 ? Time::now()->getTimestamp() + $ttl : null,
'hitcount' => $hitcount,
'age' => $age,
'ttl' => $ttl,
];
}
return false; // @TODO This will return null in a future release
}
/**
* {@inheritDoc}
*/
public function isSupported(): bool
{
return extension_loaded('wincache') && ini_get('wincache.ucenabled');
}
}
+148
View File
@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Cache;
use CodeIgniter\Exceptions\RuntimeException;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\Header;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Cache as CacheConfig;
/**
* Web Page Caching
*
* @see \CodeIgniter\Cache\ResponseCacheTest
*/
final class ResponseCache
{
/**
* Whether to take the URL query string into consideration when generating
* output cache files. Valid options are:
*
* false = Disabled
* true = Enabled, take all query parameters into account.
* Please be aware that this may result in numerous cache
* files generated for the same page over and over again.
* array('q') = Enabled, but only take into account the specified list
* of query parameters.
*
* @var bool|list<string>
*/
private array|bool $cacheQueryString = false;
/**
* Cache time to live (TTL) in seconds.
*/
private int $ttl = 0;
public function __construct(CacheConfig $config, private readonly CacheInterface $cache)
{
$this->cacheQueryString = $config->cacheQueryString;
}
public function setTtl(int $ttl): self
{
$this->ttl = $ttl;
return $this;
}
/**
* Generates the cache key to use from the current request.
*
* @internal for testing purposes only
*/
public function generateCacheKey(CLIRequest|IncomingRequest $request): string
{
if ($request instanceof CLIRequest) {
return md5($request->getPath());
}
$uri = clone $request->getUri();
$query = (bool) $this->cacheQueryString
? $uri->getQuery(is_array($this->cacheQueryString) ? ['only' => $this->cacheQueryString] : [])
: '';
return md5($request->getMethod() . ':' . $uri->setFragment('')->setQuery($query));
}
/**
* Caches the response.
*/
public function make(CLIRequest|IncomingRequest $request, ResponseInterface $response): bool
{
if ($this->ttl === 0) {
return true;
}
$headers = [];
foreach ($response->headers() as $name => $value) {
if ($value instanceof Header) {
$headers[$name] = $value->getValueLine();
} else {
foreach ($value as $header) {
$headers[$name][] = $header->getValueLine();
}
}
}
return $this->cache->save(
$this->generateCacheKey($request),
serialize(['headers' => $headers, 'output' => $response->getBody()]),
$this->ttl,
);
}
/**
* Gets the cached response for the request.
*/
public function get(CLIRequest|IncomingRequest $request, ResponseInterface $response): ?ResponseInterface
{
$cachedResponse = $this->cache->get($this->generateCacheKey($request));
if (is_string($cachedResponse) && $cachedResponse !== '') {
$cachedResponse = unserialize($cachedResponse);
if (
! is_array($cachedResponse)
|| ! isset($cachedResponse['output'])
|| ! isset($cachedResponse['headers'])
) {
throw new RuntimeException('Error unserializing page cache');
}
$headers = $cachedResponse['headers'];
$output = $cachedResponse['output'];
// Clear all default headers
foreach (array_keys($response->headers()) as $key) {
$response->removeHeader($key);
}
// Set cached headers
foreach ($headers as $name => $value) {
$response->setHeader($name, $value);
}
$response->setBody($output);
return $response;
}
return null;
}
}
+1
View File
@@ -0,0 +1 @@
<?php
+284
View File
@@ -0,0 +1,284 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
use CodeIgniter\Autoloader\FileLocatorInterface;
use CodeIgniter\Exceptions\ConfigException;
use CodeIgniter\Exceptions\RuntimeException;
use Config\Encryption;
use Config\Modules;
use ReflectionClass;
use ReflectionException;
/**
* Class BaseConfig
*
* Not intended to be used on its own, this class will attempt to
* automatically populate the child class' properties with values
* from the environment.
*
* These can be set within the .env file.
*
* @phpstan-consistent-constructor
* @see \CodeIgniter\Config\BaseConfigTest
*/
class BaseConfig
{
/**
* An optional array of classes that will act as Registrars
* for rapidly setting config class properties.
*
* @var array
*/
public static $registrars = [];
/**
* Whether to override properties by Env vars and Registrars.
*/
public static $override = true;
/**
* Has module discovery completed?
*
* @var bool
*/
protected static $didDiscovery = false;
/**
* Is module discovery running or not?
*/
protected static $discovering = false;
/**
* The processing Registrar file for error message.
*/
protected static $registrarFile = '';
/**
* The modules configuration.
*
* @var Modules|null
*/
protected static $moduleConfig;
public static function __set_state(array $array)
{
static::$override = false;
$obj = new static();
static::$override = true;
$properties = array_keys(get_object_vars($obj));
foreach ($properties as $property) {
$obj->{$property} = $array[$property];
}
return $obj;
}
/**
* @internal For testing purposes only.
* @testTag
*/
public static function setModules(Modules $modules): void
{
static::$moduleConfig = $modules;
}
/**
* @internal For testing purposes only.
* @testTag
*/
public static function reset(): void
{
static::$registrars = [];
static::$override = true;
static::$didDiscovery = false;
static::$moduleConfig = null;
}
/**
* Will attempt to get environment variables with names
* that match the properties of the child class.
*
* The "shortPrefix" is the lowercase-only config class name.
*/
public function __construct()
{
if (! static::$override) {
return;
}
$this->registerProperties();
$properties = array_keys(get_object_vars($this));
$prefix = static::class;
$slashAt = strrpos($prefix, '\\');
$shortPrefix = strtolower(substr($prefix, $slashAt === false ? 0 : $slashAt + 1));
foreach ($properties as $property) {
$this->initEnvValue($this->{$property}, $property, $prefix, $shortPrefix);
if ($this instanceof Encryption && $property === 'key') {
if (str_starts_with($this->{$property}, 'hex2bin:')) {
// Handle hex2bin prefix
$this->{$property} = hex2bin(substr($this->{$property}, 8));
} elseif (str_starts_with($this->{$property}, 'base64:')) {
// Handle base64 prefix
$this->{$property} = base64_decode(substr($this->{$property}, 7), true);
}
}
}
}
/**
* Initialization an environment-specific configuration setting
*
* @param array|bool|float|int|string|null $property
*
* @return void
*/
protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix)
{
if (is_array($property)) {
foreach (array_keys($property) as $key) {
$this->initEnvValue($property[$key], "{$name}.{$key}", $prefix, $shortPrefix);
}
} elseif (($value = $this->getEnvValue($name, $prefix, $shortPrefix)) !== false && $value !== null) {
if ($value === 'false') {
$value = false;
} elseif ($value === 'true') {
$value = true;
}
if (is_bool($value)) {
$property = $value;
return;
}
$value = trim($value, '\'"');
if (is_int($property)) {
$value = (int) $value;
} elseif (is_float($property)) {
$value = (float) $value;
}
// If the default value of the property is `null` and the type is not
// `string`, TypeError will happen.
// So cannot set `declare(strict_types=1)` in this file.
$property = $value;
}
}
/**
* Retrieve an environment-specific configuration setting
*
* @return string|null
*/
protected function getEnvValue(string $property, string $prefix, string $shortPrefix)
{
$shortPrefix = ltrim($shortPrefix, '\\');
$underscoreProperty = str_replace('.', '_', $property);
switch (true) {
case array_key_exists("{$shortPrefix}.{$property}", $_ENV):
return $_ENV["{$shortPrefix}.{$property}"];
case array_key_exists("{$shortPrefix}_{$underscoreProperty}", $_ENV):
return $_ENV["{$shortPrefix}_{$underscoreProperty}"];
case array_key_exists("{$shortPrefix}.{$property}", $_SERVER):
return $_SERVER["{$shortPrefix}.{$property}"];
case array_key_exists("{$shortPrefix}_{$underscoreProperty}", $_SERVER):
return $_SERVER["{$shortPrefix}_{$underscoreProperty}"];
case array_key_exists("{$prefix}.{$property}", $_ENV):
return $_ENV["{$prefix}.{$property}"];
case array_key_exists("{$prefix}_{$underscoreProperty}", $_ENV):
return $_ENV["{$prefix}_{$underscoreProperty}"];
case array_key_exists("{$prefix}.{$property}", $_SERVER):
return $_SERVER["{$prefix}.{$property}"];
case array_key_exists("{$prefix}_{$underscoreProperty}", $_SERVER):
return $_SERVER["{$prefix}_{$underscoreProperty}"];
default:
$value = getenv("{$shortPrefix}.{$property}");
$value = $value === false ? getenv("{$shortPrefix}_{$underscoreProperty}") : $value;
$value = $value === false ? getenv("{$prefix}.{$property}") : $value;
$value = $value === false ? getenv("{$prefix}_{$underscoreProperty}") : $value;
return $value === false ? null : $value;
}
}
/**
* Provides external libraries a simple way to register one or more
* options into a config file.
*
* @return void
*
* @throws ReflectionException
*/
protected function registerProperties()
{
if (! static::$didDiscovery) {
// Discovery must be completed before the first instantiation of any Config class.
if (static::$discovering) {
throw new ConfigException(
'During Auto-Discovery of Registrars,'
. ' "' . static::class . '" executes Auto-Discovery again.'
. ' "' . clean_path(static::$registrarFile) . '" seems to have bad code.'
);
}
static::$discovering = true;
static::$didDiscovery = true;
static::$discovering = false;
}
$shortName = (new ReflectionClass($this))->getShortName();
// Check the registrar class for a method named after this class' shortName
foreach (static::$registrars as $callable) {
// ignore non-applicable registrars
if (! method_exists($callable, $shortName)) {
continue; // @codeCoverageIgnore
}
$properties = $callable::$shortName();
if (! is_array($properties)) {
throw new RuntimeException('Registrars must return an array of properties and their values.');
}
foreach ($properties as $property => $value) {
if (isset($this->{$property}) && is_array($this->{$property}) && is_array($value)) {
$this->{$property} = array_merge($this->{$property}, $value);
} else {
$this->{$property} = $value;
}
}
}
}
}
+405
View File
@@ -0,0 +1,405 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
use CodeIgniter\Autoloader\Autoloader;
use CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Autoloader\FileLocatorCached;
use CodeIgniter\Autoloader\FileLocatorInterface;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\ResponseCache;
use CodeIgniter\CLI\Commands;
use CodeIgniter\CodeIgniter;
use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Database\MigrationRunner;
use CodeIgniter\Debug\Exceptions;
use CodeIgniter\Debug\Iterator;
use CodeIgniter\Debug\Timer;
use CodeIgniter\Debug\Toolbar;
use CodeIgniter\Email\Email;
use CodeIgniter\Encryption\EncrypterInterface;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\Filters\Filters;
use CodeIgniter\Format\Format;
use CodeIgniter\Honeypot\Honeypot;
use CodeIgniter\HTTP\CLIRequest;
use CodeIgniter\HTTP\ContentSecurityPolicy;
use CodeIgniter\HTTP\CURLRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Negotiate;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\Request;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\SiteURIFactory;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Images\Handlers\BaseHandler;
use CodeIgniter\Language\Language;
use CodeIgniter\Log\Logger;
use CodeIgniter\Pager\Pager;
use CodeIgniter\Router\RouteCollection;
use CodeIgniter\Router\RouteCollectionInterface;
use CodeIgniter\Router\Router;
use CodeIgniter\Security\Security;
use CodeIgniter\Session\Session;
use CodeIgniter\Superglobals;
use CodeIgniter\Throttle\Throttler;
use CodeIgniter\Typography\Typography;
use CodeIgniter\Validation\ValidationInterface;
use CodeIgniter\View\Cell;
use CodeIgniter\View\Parser;
use CodeIgniter\View\RendererInterface;
use CodeIgniter\View\View;
use Config\App;
use Config\Autoload;
use Config\Cache;
use Config\ContentSecurityPolicy as CSPConfig;
use Config\Encryption;
use Config\Exceptions as ConfigExceptions;
use Config\Filters as ConfigFilters;
use Config\Format as ConfigFormat;
use Config\Honeypot as ConfigHoneyPot;
use Config\Images;
use Config\Migrations;
use Config\Modules;
use Config\Optimize;
use Config\Pager as ConfigPager;
use CodeIgniter\Config\Services as AppServices;
use Config\Session as ConfigSession;
use Config\Toolbar as ConfigToolbar;
use Config\Validation as ConfigValidation;
use Config\View as ConfigView;
/**
* Services Configuration file.
*
* Services are simply other classes/libraries that the system uses
* to do its job. This is used by CodeIgniter to allow the core of the
* framework to be swapped out easily without affecting the usage within
* the rest of your application.
*
* This is used in place of a Dependency Injection container primarily
* due to its simplicity, which allows a better long-term maintenance
* of the applications built on top of CodeIgniter. A bonus side-effect
* is that IDEs are able to determine what class you are calling
* whereas with DI Containers there usually isn't a way for them to do this.
*
* Warning: To allow overrides by service providers do not use static calls,
* instead call out to \Config\Services (imported as AppServices).
*
* @see http://blog.ircmaxell.com/2015/11/simple-easy-risk-and-change.html
* @see http://www.infoq.com/presentations/Simple-Made-Easy
*
* @method static CacheInterface cache(Cache $config = null, $getShared = true)
* @method static CLIRequest clirequest(App $config = null, $getShared = true)
* @method static CodeIgniter codeigniter(App $config = null, $getShared = true)
* @method static Commands commands($getShared = true)
* @method static void createRequest(App $config, bool $isCli = false)
* @method static ContentSecurityPolicy csp(CSPConfig $config = null, $getShared = true)
* @method static CURLRequest curlrequest($options = [], ResponseInterface $response = null, App $config = null, $getShared = true)
* @method static Email email($config = null, $getShared = true)
* @method static EncrypterInterface encrypter(Encryption $config = null, $getShared = false)
* @method static Exceptions exceptions(ConfigExceptions $config = null, $getShared = true)
* @method static Filters filters(ConfigFilters $config = null, $getShared = true)
* @method static Format format(ConfigFormat $config = null, $getShared = true)
* @method static Honeypot honeypot(ConfigHoneyPot $config = null, $getShared = true)
* @method static BaseHandler image($handler = null, Images $config = null, $getShared = true)
* @method static IncomingRequest incomingrequest(?App $config = null, bool $getShared = true)
* @method static Iterator iterator($getShared = true)
* @method static Language language($locale = null, $getShared = true)
* @method static Logger logger($getShared = true)
* @method static MigrationRunner migrations(Migrations $config = null, ConnectionInterface $db = null, $getShared = true)
* @method static Negotiate negotiator(RequestInterface $request = null, $getShared = true)
* @method static Pager pager(ConfigPager $config = null, RendererInterface $view = null, $getShared = true)
* @method static Parser parser($viewPath = null, ConfigView $config = null, $getShared = true)
* @method static RedirectResponse redirectresponse(App $config = null, $getShared = true)
* @method static View renderer($viewPath = null, ConfigView $config = null, $getShared = true)
* @method static IncomingRequest|CLIRequest request(App $config = null, $getShared = true)
* @method static ResponseInterface response(App $config = null, $getShared = true)
* @method static ResponseCache responsecache(?Cache $config = null, ?CacheInterface $cache = null, bool $getShared = true)
* @method static Router router(RouteCollectionInterface $routes = null, Request $request = null, $getShared = true)
* @method static RouteCollection routes($getShared = true)
* @method static Security security(App $config = null, $getShared = true)
* @method static Session session(ConfigSession $config = null, $getShared = true)
* @method static SiteURIFactory siteurifactory(App $config = null, Superglobals $superglobals = null, $getShared = true)
* @method static Superglobals superglobals(array $server = null, array $get = null, bool $getShared = true)
* @method static Throttler throttler($getShared = true)
* @method static Timer timer($getShared = true)
* @method static Toolbar toolbar(ConfigToolbar $config = null, $getShared = true)
* @method static Typography typography($getShared = true)
* @method static URI uri($uri = null, $getShared = true)
* @method static ValidationInterface validation(ConfigValidation $config = null, $getShared = true)
* @method static Cell viewcell($getShared = true)
*/
class BaseService
{
/**
* Cache for instance of any services that
* have been requested as a "shared" instance.
* Keys should be lowercase service names.
*
* @var array<string, object> [key => instance]
*/
protected static $instances = [];
/**
* Factory method list.
*
* @var array<string, (callable(mixed ...$params): object)> [key => callable]
*/
protected static array $factories = [];
/**
* Mock objects for testing which are returned if exist.
*
* @var array<string, object> [key => instance]
*/
protected static $mocks = [];
/**
* Have we already discovered other Services?
*
* @var bool
*/
protected static $discovered = false;
/**
* A cache of other service classes we've found.
*
* @var array
*
* @deprecated 4.5.0 No longer used.
*/
protected static $services = [];
/**
* A cache of the names of services classes found.
*
* @var list<string>
*/
private static array $serviceNames = [];
/**
* Simple method to get an entry fast.
*
* @param string $key Identifier of the entry to look for.
*
* @return object|null Entry.
*/
public static function get(string $key): ?object
{
return static::$instances[$key] ?? static::__callStatic($key, []);
}
/**
* Sets an entry.
*
* @param string $key Identifier of the entry.
*/
public static function set(string $key, object $value): void
{
if (isset(static::$instances[$key])) {
throw new InvalidArgumentException('The entry for "' . $key . '" is already set.');
}
static::$instances[$key] = $value;
}
/**
* Overrides an existing entry.
*
* @param string $key Identifier of the entry.
*/
public static function override(string $key, object $value): void
{
static::$instances[$key] = $value;
}
/**
* Returns a shared instance of any of the class' services.
*
* $key must be a name matching a service.
*
* @param array|bool|float|int|object|string|null ...$params
*
* @return object
*/
protected static function getSharedInstance(string $key, ...$params)
{
$key = strtolower($key);
// Returns mock if exists
if (isset(static::$mocks[$key])) {
return static::$mocks[$key];
}
if (! isset(static::$instances[$key])) {
// Make sure $getShared is false
$params[] = false;
static::$instances[$key] = AppServices::$key(...$params);
}
return static::$instances[$key];
}
/**
* The Autoloader class is the central class that handles our
* spl_autoload_register method, and helper methods.
*
* @return Autoloader
*/
public static function autoloader(bool $getShared = true)
{
if ($getShared) {
if (empty(static::$instances['autoloader'])) {
static::$instances['autoloader'] = new Autoloader();
}
return static::$instances['autoloader'];
}
return new Autoloader();
}
/**
* The file locator provides utility methods for looking for non-classes
* within namespaced folders, as well as convenience methods for
* loading 'helpers', and 'libraries'.
*
* @return FileLocatorInterface
*/
public static function locator(bool $getShared = true)
{
if ($getShared) {
if (empty(static::$instances['locator'])) {
$cacheEnabled = class_exists(Optimize::class)
&& (new Optimize())->locatorCacheEnabled;
if ($cacheEnabled) {
static::$instances['locator'] = new FileLocatorCached(new FileLocator(static::autoloader()));
} else {
static::$instances['locator'] = new FileLocator(static::autoloader());
}
}
return static::$mocks['locator'] ?? static::$instances['locator'];
}
return new FileLocator(static::autoloader());
}
/**
* Provides the ability to perform case-insensitive calling of service
* names.
*
* @return object|null
*/
public static function __callStatic(string $name, array $arguments)
{
if (isset(static::$factories[$name])) {
return static::$factories[$name](...$arguments);
}
$service = static::serviceExists($name);
if ($service === null) {
return null;
}
return $service::$name(...$arguments);
}
/**
* Check if the requested service is defined and return the declaring
* class. Return null if not found.
*/
public static function serviceExists(string $name): ?string
{
static::buildServicesCache();
$services = array_merge(self::$serviceNames, [Services::class]);
$name = strtolower($name);
foreach ($services as $service) {
if (method_exists($service, $name)) {
static::$factories[$name] = [$service, $name];
return $service;
}
}
return null;
}
/**
* Reset shared instances and mocks for testing.
*
* @return void
*
* @testTag only available to test code
*/
public static function reset(bool $initAutoloader = true)
{
static::$mocks = [];
static::$instances = [];
static::$factories = [];
if ($initAutoloader) {
static::autoloader()->initialize(new Autoload(), new Modules());
}
}
/**
* Resets any mock and shared instances for a single service.
*
* @return void
*
* @testTag only available to test code
*/
public static function resetSingle(string $name)
{
$name = strtolower($name);
unset(static::$mocks[$name], static::$instances[$name]);
}
/**
* Inject mock object for testing.
*
* @param object $mock
*
* @return void
*
* @testTag only available to test code
*/
public static function injectMock(string $name, $mock)
{
static::$instances[$name] = $mock;
static::$mocks[strtolower($name)] = $mock;
}
/**
* Resets the service cache.
*/
public static function resetServicesCache(): void
{
self::$serviceNames = [];
static::$discovered = false;
}
protected static function buildServicesCache(): void
{
}
}
+191
View File
@@ -0,0 +1,191 @@
<?php namespace Config;
/**
* {{www.xunruicms.com}}
* {{迅睿内容管理框架系统}}
* 本文件是框架系统文件,二次开发时不可以修改本文件
**/
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Handlers\DummyHandler;
use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\Handlers\MemcachedHandler;
use CodeIgniter\Cache\Handlers\PredisHandler;
use CodeIgniter\Cache\Handlers\RedisHandler;
use CodeIgniter\Cache\Handlers\WincacheHandler;
use CodeIgniter\Config\BaseConfig;
class Cache extends BaseConfig
{
/*
|--------------------------------------------------------------------------
| Primary Handler
|--------------------------------------------------------------------------
|
| The name of the preferred handler that should be used. If for some reason
| it is not available, the $backupHandler will be used in its place.
|
*/
public $handler = SYS_CACHE_TYPE == 1 ? 'memcached' : (SYS_CACHE_TYPE == 2 ? 'redis' : 'file');
/*
|--------------------------------------------------------------------------
| Backup Handler
|--------------------------------------------------------------------------
|
| The name of the handler that will be used in case the first one is
| unreachable. Often, 'file' is used here since the filesystem is
| always available, though that's not always practical for the app.
|
*/
public $backupHandler = 'file';
/*
|--------------------------------------------------------------------------
| Cache Directory Path
|--------------------------------------------------------------------------
|
| The path to where cache files should be stored, if using a file-based
| system.
|
*/
public $storePath = WRITEPATH.'file/';
/*
|--------------------------------------------------------------------------
| Cache Include Query String
|--------------------------------------------------------------------------
|
| Whether to take the URL query string into consideration when generating
| output cache files. Valid options are:
|
| false = Disabled
| true = Enabled, take all query parameters into account.
| Please be aware that this may result in numerous cache
| files generated for the same page over and over again.
| array('q') = Enabled, but only take into account the specified list
| of query parameters.
|
*/
public $cacheQueryString = false;
/*
|--------------------------------------------------------------------------
| Key Prefix
|--------------------------------------------------------------------------
|
| This string is added to all cache item names to help avoid collisions
| if you run multiple applications with the same cache engine.
|
*/
public string $prefix = '';
/**
* --------------------------------------------------------------------------
* Default TTL
* --------------------------------------------------------------------------
*
* The default number of seconds to save items when none is specified.
*
* WARNING: This is not used by framework handlers where 60 seconds is
* hard-coded, but may be useful to projects and modules. This will replace
* the hard-coded value in a future release.
*
* @var integer
*/
public int $ttl = 600;
/**
* --------------------------------------------------------------------------
* Reserved Characters
* --------------------------------------------------------------------------
*
* A string of reserved characters that will not be allowed in keys or tags.
* Strings that violate this restriction will cause handlers to throw.
* Default: {}()/\@:
* Note: The default set is required for PSR-6 compliance.
*
* @var string
*/
public string $reservedCharacters = '{}()/\@:';
/**
* --------------------------------------------------------------------------
* File settings
* --------------------------------------------------------------------------
* Your file storage preferences can be specified below, if you are using
* the File driver.
*
* @var array<string, int|string|null>
*/
public array $file = [
'storePath' => WRITEPATH . 'file/',
'mode' => 0640,
];
/*
| -------------------------------------------------------------------------
| Memcached settings
| -------------------------------------------------------------------------
| Your Memcached servers can be specified below, if you are using
| the Memcached drivers.
|
| See: https://codeigniter.com/user_guide/libraries/caching.html#memcached
|
*/
public array $memcached = [
'host' => '127.0.0.1',
'port' => 11211,
'weight' => 1,
'raw' => false,
];
/*
| -------------------------------------------------------------------------
| Redis settings
| -------------------------------------------------------------------------
| Your Redis server can be specified below, if you are using
| the Redis or Predis drivers.
|
*/
public array $redis = [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'database' => 0,
];
/*
|--------------------------------------------------------------------------
| Available Cache Handlers
|--------------------------------------------------------------------------
|
| This is an array of cache engine alias' and class names. Only engines
| that are listed here are allowed to be used.
|
*/
public array $validHandlers = [
'dummy' => DummyHandler::class,
'file' => FileHandler::class,
'memcached' => MemcachedHandler::class,
'predis' => PredisHandler::class,
'redis' => RedisHandler::class,
'wincache' => WincacheHandler::class,
];
public function __construct()
{
parent::__construct();
if (is_file(CONFIGPATH.'redis.php')) {
$this->redis = require CONFIGPATH.'redis.php';
} elseif (is_file(ROOTPATH.'config/redis.php')) {
$this->redis = require ROOTPATH.'config/redis.php';
}
if (is_file(CONFIGPATH.'memcached.php')) {
$this->memcached = require CONFIGPATH.'memcached.php';
} elseif (is_file(ROOTPATH.'config/memcached.php')) {
$this->memcached = require ROOTPATH.'config/memcached.php';
}
$this->prefix = substr(SYS_KEY, 0, 10).'-';
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php namespace Config;
/**
* {{www.xunruicms.com}}
* {{迅睿内容管理框架系统}}
* 本文件是框架系统文件,二次开发时不可以修改本文件
**/
use CodeIgniter\Database\Config;
/**
* Database Configuration
*/
class Database extends Config
{
/**
* The directory that holds the Migrations
* and Seeds directories.
* @var string
*/
public $filesPath = WRITEPATH.'database/';
/**
* Lets you choose which connection group to
* use if no other is specified.
*
* @var string
*/
public $defaultGroup = 'default';
/**
* The default database connection.
*
* @var array
*/
public $default = [
'DSN' => '',
'hostname' => 'localhost',
'username' => '',
'password' => '',
'database' => '',
'DBDriver' => 'MySQLi',
'DBPrefix' => '',
'pConnect' => false,
'DBDebug' => true,
'cacheOn' => true,
'cacheDir' => WRITEPATH.'database/',
'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_general_ci',
'swapPre' => '',
'encrypt' => false,
'compress' => false,
'strictOn' => false,
'foreignKeys' => true,
'numberNative' => false,
'failover' => []
];
public $db1 = [];
public $db2 = [];
public $db3 = [];
public $db4 = [];
public $db5 = [];
public $db6 = [];
public $db7 = [];
public $db8 = [];
public $db9 = [];
private $mykey = 1;
private $mydb = [];
//--------------------------------------------------------------------
public function __construct()
{
parent::__construct();
if (!is_file(CONFIGPATH.'database.php')) {
return $this;
}
$db = [];
require CONFIGPATH.'database.php';
if (isset($db['failover']) && $db['failover']) {
// 备用库
$this->default['failover'] = $db['failover'];
unset($db['failover']);
}
foreach ($this->default as $p => $t) {
foreach ($db as $name => $v) {
if (isset($this->$name)) {
// 默认库
$this->$name[$p] = isset($v[$p]) ? $v[$p] : $t;
} else {
// 自定义库
$key = $this->_get_key($name);
if ($key) {
$this->$key[$p] = isset($v[$p]) ? $v[$p] : $t;
}
}
}
}
// 判断数据库名称的规范性(SQLite3 为文件路径,允许 .db 等扩展名)
$driver = isset($this->default['DBDriver']) ? (string) $this->default['DBDriver'] : 'MySQLi';
if (strcasecmp($driver, 'SQLite3') !== 0) {
if (is_numeric($this->default['database'])) {
exit('数据库名称不能是数字');
} elseif (strpos((string) $this->default['database'], '.') !== false) {
exit('数据库名称不能存在.号');
}
} else {
// SQLite 默认等待写锁,降低 database is locked 概率
if (!isset($this->default['busyTimeout'])) {
$this->default['busyTimeout'] = 10000;
}
}
}
private function _get_key($name) {
if (isset($this->mydb[$name])) {
return $this->mydb[$name];
} else {
$this->mydb[$name] = 'db'.$this->mykey;
$this->mykey++;
}
return $this->mydb[$name];
}
public function get_group($name) {
if (isset($this->mydb[$name])) {
return $this->mydb[$name];
}
return 'default';
}
//--------------------------------------------------------------------
}
+1
View File
@@ -0,0 +1 @@
<?php
+106
View File
@@ -0,0 +1,106 @@
<?php namespace Config;
/**
* {{www.xunruicms.com}}
* {{迅睿内容管理框架系统}}
* 本文件是框架系统文件,二次开发时不可以修改本文件
**/
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Debug\ExceptionHandler;
use CodeIgniter\Debug\ExceptionHandlerInterface;
use Throwable;
/**
* Setup how the exception handler works.
*/
class Exceptions extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* LOG EXCEPTIONS?
* --------------------------------------------------------------------------
* If true, then exceptions will be logged
* through Services::Log.
*
* Default: true
*/
public bool $log = true;
/**
* --------------------------------------------------------------------------
* DO NOT LOG STATUS CODES
* --------------------------------------------------------------------------
* Any status codes here will NOT be logged if logging is turned on.
* By default, only 404 (Page Not Found) exceptions are ignored.
*/
public array $ignoreCodes = [404];
/**
* --------------------------------------------------------------------------
* Error Views Path
* --------------------------------------------------------------------------
* This is the path to the directory that contains the 'cli' and 'html'
* directories that hold the views used to generate errors.
*
* Default: APPPATH.'Views/errors'
*/
public $errorViewPath = FRAMEPATH . 'Exceptions/Views';
/**
* --------------------------------------------------------------------------
* HIDE FROM DEBUG TRACE
* --------------------------------------------------------------------------
* Any data that you would like to hide from the debug trace.
* In order to specify 2 levels, use "/" to separate.
* ex. ['server', 'setup/password', 'secret_token']
*/
public array $sensitiveDataInTrace = [];
/**
* --------------------------------------------------------------------------
* LOG DEPRECATIONS INSTEAD OF THROWING?
* --------------------------------------------------------------------------
* By default, CodeIgniter converts deprecations into exceptions. Also,
* starting in PHP 8.1 will cause a lot of deprecated usage warnings.
* Use this option to temporarily cease the warnings and instead log those.
* This option also works for user deprecations.
*/
public bool $logDeprecations = true;
/**
* --------------------------------------------------------------------------
* LOG LEVEL THRESHOLD FOR DEPRECATIONS
* --------------------------------------------------------------------------
* If `$logDeprecations` is set to `true`, this sets the log level
* to which the deprecation will be logged. This should be one of the log
* levels recognized by PSR-3.
*
* The related `Config\Logger::$threshold` should be adjusted, if needed,
* to capture logging the deprecations.
*/
public string $deprecationLogLevel = 'warning';
/*
* DEFINE THE HANDLERS USED
* --------------------------------------------------------------------------
* Given the HTTP status code, returns exception handler that
* should be used to deal with this error. By default, it will run CodeIgniter's
* default handler and display the error information in the expected format
* for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected
* response format.
*
* Custom handlers can be returned if you want to handle one or more specific
* error codes yourself like:
*
* if (in_array($statusCode, [400, 404, 500])) {
* return new \App\Libraries\MyExceptionHandler();
* }
* if ($exception instanceOf PageNotFoundException) {
* return new \App\Libraries\MyExceptionHandler();
* }
*/
public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
{
return new ExceptionHandler($this);
}
}
+539
View File
@@ -0,0 +1,539 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
use CodeIgniter\Autoloader\FileLocatorInterface;
use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\Model;
/**
* Factories for creating instances.
*
* Factories allow dynamic loading of components by their path
* and name. The "shared instance" implementation provides a
* large performance boost and helps keep code clean of lengthy
* instantiation checks.
*
* @method static BaseConfig|null config(...$arguments)
* @method static Model|null models(string $alias, array $options = [], ?ConnectionInterface &$conn = null)
* @see \CodeIgniter\Config\FactoriesTest
*/
final class Factories
{
/**
* Store of component-specific options, usually
* from CodeIgniter\Config\Factory.
*
* @var array<string, array<string, bool|string|null>>
*/
private static $options = [];
/**
* Explicit options for the Config
* component to prevent logic loops.
*
* @var array<string, bool|string|null>
*/
private static $configOptions = [
'component' => 'config',
'path' => 'Config',
'instanceOf' => null,
'getShared' => true,
'preferApp' => true,
];
/**
* Mapping of class aliases to their true Fully Qualified Class Name (FQCN).
*
* Class aliases can be:
* - FQCN. E.g., 'App\Lib\SomeLib'
* - short classname. E.g., 'SomeLib'
* - short classname with sub-directories. E.g., 'Sub/SomeLib'
*
* [component => [alias => FQCN]]
*
* @var array<string, array<string, class-string>>
*/
private static $aliases = [];
/**
* Store for instances of any component that
* has been requested as "shared".
*
* A multi-dimensional array with components as
* keys to the array of name-indexed instances.
*
* [component => [FQCN => instance]]
*
* @var array<string, array<class-string, object>>
*/
private static $instances = [];
/**
* Whether the component instances are updated?
*
* @var array<string, true> [component => true]
*
* @internal For caching only
*/
private static $updated = [];
/**
* Define the class to load. You can *override* the concrete class.
*
* @param string $component Lowercase, plural component name
* @param string $alias Class alias. See the $aliases property.
* @param class-string $classname FQCN to be loaded
*/
public static function define(string $component, string $alias, string $classname): void
{
$component = strtolower($component);
if (isset(self::$aliases[$component][$alias])) {
if (self::$aliases[$component][$alias] === $classname) {
return;
}
$message = 'Already defined in Factories: ' . $component . ' ' . $alias . ' -> ' . self::$aliases[$component][$alias];
throw new InvalidArgumentException($message);
}
if (! class_exists($classname)) {
throw new InvalidArgumentException('No such class: ' . $classname);
}
// Force a configuration to exist for this component.
// Otherwise, getOptions() will reset the component.
self::getOptions($component);
self::$aliases[$component][$alias] = $classname;
self::$updated[$component] = true;
}
/**
* Loads instances based on the method component name. Either
* creates a new instance or returns an existing shared instance.
*
* @return object|null
*/
public static function __callStatic(string $component, array $arguments)
{
$component = strtolower($component);
// First argument is the class alias, second is options
$alias = trim(array_shift($arguments), '\\ ');
$options = array_shift($arguments) ?? [];
// Determine the component-specific options
$options = array_merge(self::getOptions($component), $options);
if (! $options['getShared']) {
if (isset(self::$aliases[$options['component']][$alias])) {
$class = self::$aliases[$options['component']][$alias];
return new $class(...$arguments);
}
// Try to locate the class
$class = self::locateClass($options, $alias);
if ($class !== null) {
return new $class(...$arguments);
}
return null;
}
// Check for an existing definition
$instance = self::getDefinedInstance($options, $alias, $arguments);
if ($instance !== null) {
return $instance;
}
// Try to locate the class
if (($class = self::locateClass($options, $alias)) === null) {
return null;
}
self::createInstance($options['component'], $class, $arguments);
self::setAlias($options['component'], $alias, $class);
return self::$instances[$options['component']][$class];
}
/**
* Simple method to get the shared instance fast.
*/
public static function get(string $component, string $alias): ?object
{
if (isset(self::$aliases[$component][$alias])) {
$class = self::$aliases[$component][$alias];
if (isset(self::$instances[$component][$class])) {
return self::$instances[$component][$class];
}
}
return self::__callStatic($component, [$alias]);
}
/**
* Gets the defined instance. If not exists, creates new one.
*
* @return object|null
*/
private static function getDefinedInstance(array $options, string $alias, array $arguments)
{
// The alias is already defined.
if (isset(self::$aliases[$options['component']][$alias])) {
$class = self::$aliases[$options['component']][$alias];
// Need to verify if the shared instance matches the request
if (self::verifyInstanceOf($options, $class)) {
// Check for an existing instance
if (isset(self::$instances[$options['component']][$class])) {
return self::$instances[$options['component']][$class];
}
self::createInstance($options['component'], $class, $arguments);
return self::$instances[$options['component']][$class];
}
}
// Try to locate the class
if (($class = self::locateClass($options, $alias)) === null) {
return null;
}
// Check for an existing instance for the class
if (isset(self::$instances[$options['component']][$class])) {
self::setAlias($options['component'], $alias, $class);
return self::$instances[$options['component']][$class];
}
return null;
}
/**
* Creates the shared instance.
*/
private static function createInstance(string $component, string $class, array $arguments): void
{
self::$instances[$component][$class] = new $class(...$arguments);
self::$updated[$component] = true;
}
/**
* Sets alias
*/
private static function setAlias(string $component, string $alias, string $class): void
{
self::$aliases[$component][$alias] = $class;
self::$updated[$component] = true;
// If a short classname is specified, also register FQCN to share the instance.
if (! isset(self::$aliases[$component][$class]) && ! self::isNamespaced($alias)) {
self::$aliases[$component][$class] = $class;
}
}
/**
* Is the component Config?
*
* @param string $component Lowercase, plural component name
*/
private static function isConfig(string $component): bool
{
return $component === 'config';
}
/**
* Finds a component class
*
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
private static function locateClass(array $options, string $alias): ?string
{
// Check for low-hanging fruit
if (
class_exists($alias, false)
&& self::verifyPreferApp($options, $alias)
&& self::verifyInstanceOf($options, $alias)
) {
return $alias;
}
// Determine the relative class names we need
$basename = self::getBasename($alias);
$appname = self::isConfig($options['component'])
? 'Config\\' . $basename
: rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename;
// If an App version was requested then see if it verifies
if (
// preferApp is used only for no namespaced class.
! self::isNamespaced($alias)
&& $options['preferApp'] && class_exists($appname)
&& self::verifyInstanceOf($options, $alias)
) {
return $appname;
}
// If we have ruled out an App version and the class exists then try it
if (class_exists($alias) && self::verifyInstanceOf($options, $alias)) {
return $alias;
}
return null;
}
/**
* Is the class alias namespaced or not?
*
* @param string $alias Class alias. See the $aliases property.
*/
private static function isNamespaced(string $alias): bool
{
return str_contains($alias, '\\');
}
/**
* Verifies that a class & config satisfy the "preferApp" option
*
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
private static function verifyPreferApp(array $options, string $alias): bool
{
// Anything without that restriction passes
if (! $options['preferApp']) {
return true;
}
// Special case for Config since its App namespace is actually \Config
if (self::isConfig($options['component'])) {
return str_starts_with($alias, 'Config');
}
return str_starts_with($alias, APP_NAMESPACE);
}
/**
* Verifies that a class & config satisfy the "instanceOf" option
*
* @param array $options The array of component-specific directives
* @param string $alias Class alias. See the $aliases property.
*/
private static function verifyInstanceOf(array $options, string $alias): bool
{
// Anything without that restriction passes
if (! $options['instanceOf']) {
return true;
}
return is_a($alias, $options['instanceOf'], true);
}
/**
* Returns the component-specific configuration
*
* @param string $component Lowercase, plural component name
*
* @return array<string, bool|string|null>
*
* @internal For testing only
* @testTag
*/
public static function getOptions(string $component): array
{
$component = strtolower($component);
// Check for a stored version
if (isset(self::$options[$component])) {
return self::$options[$component];
}
$values = self::isConfig($component)
// Handle Config as a special case to prevent logic loops
? self::$configOptions
// Load values from the best Factory configuration (will include Registrars)
: config('Factory')->{$component} ?? [];
// The setOptions() reset the component. So getOptions() may reset
// the component.
return self::setOptions($component, $values);
}
/**
* Normalizes, stores, and returns the configuration for a specific component
*
* @param string $component Lowercase, plural component name
* @param array $values option values
*
* @return array<string, bool|string|null> The result after applying defaults and normalization
*/
public static function setOptions(string $component, array $values): array
{
$component = strtolower($component);
// Allow the config to replace the component name, to support "aliases"
$values['component'] = strtolower($values['component'] ?? $component);
// Reset this component so instances can be rediscovered with the updated config
self::reset($values['component']);
// If no path was available then use the component
$values['path'] = trim($values['path'] ?? ucfirst($values['component']), '\\ ');
// Add defaults for any missing values
$values = array_merge(Factory::$default, $values);
// Store the result to the supplied name and potential alias
self::$options[$component] = $values;
self::$options[$values['component']] = $values;
return $values;
}
/**
* Resets the static arrays, optionally just for one component
*
* @param string|null $component Lowercase, plural component name
*
* @return void
*/
public static function reset(?string $component = null)
{
if ($component !== null) {
unset(
self::$options[$component],
self::$aliases[$component],
self::$instances[$component],
self::$updated[$component]
);
return;
}
self::$options = [];
self::$aliases = [];
self::$instances = [];
self::$updated = [];
}
/**
* Helper method for injecting mock instances
*
* @param string $component Lowercase, plural component name
* @param string $alias Class alias. See the $aliases property.
*
* @return void
*
* @internal For testing only
* @testTag
*/
public static function injectMock(string $component, string $alias, object $instance)
{
$component = strtolower($component);
// Force a configuration to exist for this component
self::getOptions($component);
//$class = $instance::class;
$class = get_class($instance);
self::$instances[$component][$class] = $instance;
self::$aliases[$component][$alias] = $class;
if (self::isConfig($component)) {
if (self::isNamespaced($alias)) {
self::$aliases[$component][self::getBasename($alias)] = $class;
} else {
self::$aliases[$component]['Config\\' . $alias] = $class;
}
}
}
/**
* Gets a basename from a class alias, namespaced or not.
*
* @internal For testing only
* @testTag
*/
public static function getBasename(string $alias): string
{
// Determine the basename
if ($basename = strrchr($alias, '\\')) {
return substr($basename, 1);
}
return $alias;
}
/**
* Gets component data for caching.
*
* @return array{
* options: array<string, bool|string|null>,
* aliases: array<string, class-string>,
* instances: array<class-string, object>,
* }
*
* @internal For caching only
*/
public static function getComponentInstances(string $component): array
{
if (! isset(self::$aliases[$component])) {
return [
'options' => [],
'aliases' => [],
'instances' => [],
];
}
return [
'options' => self::$options[$component],
'aliases' => self::$aliases[$component],
'instances' => self::$instances[$component],
];
}
/**
* Sets component data
*
* @internal For caching only
*/
public static function setComponentInstances(string $component, array $data): void
{
self::$options[$component] = $data['options'];
self::$aliases[$component] = $data['aliases'];
self::$instances[$component] = $data['instances'];
unset(self::$updated[$component]);
}
/**
* Whether the component instances are updated?
*
* @internal For caching only
*/
public static function isUpdated(string $component): bool
{
return isset(self::$updated[$component]);
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
/**
* Factories Configuration file.
*
* Provides overriding directives for how
* Factories should handle discovery and
* instantiation of specific components.
* Each property should correspond to the
* lowercase, plural component name.
*/
class Factory extends BaseConfig
{
/**
* Supplies a default set of options to merge for
* all unspecified factory components.
*
* @var array
*/
public static $default = [
'component' => null,
'path' => null,
'instanceOf' => null,
'getShared' => true,
'preferApp' => true,
];
/**
* Specifies that Models should always favor child
* classes to allow easy extension of module Models.
*
* @var array
*/
public $models = [
'preferApp' => true,
];
}
+1
View File
@@ -0,0 +1 @@
<?php
+117
View File
@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
/**
* Describes foreign characters for transliteration with the text helper.
*/
class ForeignCharacters
{
/**
* The list of foreign characters.
*
* @var array<string, string>
*/
public $characterList = [
'/ä|æ|ǽ/' => 'ae',
'/ö|œ/' => 'oe',
'/ü/' => 'ue',
'/Ä/' => 'Ae',
'/Ü/' => 'Ue',
'/Ö/' => 'Oe',
'/À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ|Α|Ά|Ả|Ạ|Ầ|Ẫ|Ẩ|Ậ|Ằ|Ắ|Ẵ|Ẳ|Ặ|А/' => 'A',
'/à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª|α|ά|ả|ạ|ầ|ấ|ẫ|ẩ|ậ|ằ|ắ|ẵ|ẳ|ặ|а/' => 'a',
'/Б/' => 'B',
'/б/' => 'b',
'/Ç|Ć|Ĉ|Ċ|Č/' => 'C',
'/ç|ć|ĉ|ċ|č/' => 'c',
'/Д/' => 'D',
'/д/' => 'd',
'/Ð|Ď|Đ|Δ/' => 'Dj',
'/ð|ď|đ|δ/' => 'dj',
'/È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě|Ε|Έ|Ẽ|Ẻ|Ẹ|Ề|Ế|Ễ|Ể|Ệ|Е|Э/' => 'E',
'/è|é|ê|ë|ē|ĕ|ė|ę|ě|έ|ε|ẽ|ẻ|ẹ|ề|ế|ễ|ể|ệ|е|э/' => 'e',
'/Ф/' => 'F',
'/ф/' => 'f',
'/Ĝ|Ğ|Ġ|Ģ|Γ|Г|Ґ/' => 'G',
'/ĝ|ğ|ġ|ģ|γ|г|ґ/' => 'g',
'/Ĥ|Ħ/' => 'H',
'/ĥ|ħ/' => 'h',
'/Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ|Η|Ή|Ί|Ι|Ϊ|Ỉ|Ị|И|Ы/' => 'I',
'/ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı|η|ή|ί|ι|ϊ|ỉ|ị|и|ы|ї/' => 'i',
'/Ĵ/' => 'J',
'/ĵ/' => 'j',
'/Ķ|Κ|К/' => 'K',
'/ķ|κ|к/' => 'k',
'/Ĺ|Ļ|Ľ|Ŀ|Ł|Λ|Л/' => 'L',
'/ĺ|ļ|ľ|ŀ|ł|λ|л/' => 'l',
'/М/' => 'M',
'/м/' => 'm',
'/Ñ|Ń|Ņ|Ň|Ν|Н/' => 'N',
'/ñ|ń|ņ|ň|ʼn|ν|н/' => 'n',
'/Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ|Ο|Ό|Ω|Ώ|Ỏ|Ọ|Ồ|Ố|Ỗ|Ổ|Ộ|Ờ|Ớ|Ỡ|Ở|Ợ|О/' => 'O',
'/ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º|ο|ό|ω|ώ|ỏ|ọ|ồ|ố|ỗ|ổ|ộ|ờ|ớ|ỡ|ở|ợ|о/' => 'o',
'/П/' => 'P',
'/п/' => 'p',
'/Ŕ|Ŗ|Ř|Ρ|Р/' => 'R',
'/ŕ|ŗ|ř|ρ|р/' => 'r',
'/Ś|Ŝ|Ş|Ș|Š|Σ|С/' => 'S',
'/ś|ŝ|ş|ș|š|ſ|σ|ς|с/' => 's',
'/Ț|Ţ|Ť|Ŧ|τ|Т/' => 'T',
'/ț|ţ|ť|ŧ|т/' => 't',
'/Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ|Ũ|Ủ|Ụ|Ừ|Ứ|Ữ|Ử|Ự|У/' => 'U',
'/ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ|υ|ύ|ϋ|ủ|ụ|ừ|ứ|ữ|ử|ự|у/' => 'u',
'/Ƴ|Ɏ|Ỵ|Ẏ|Ӳ|Ӯ|Ў|Ý|Ÿ|Ŷ|Υ|Ύ|Ϋ|Ỳ|Ỹ|Ỷ|Ỵ|Й/' => 'Y',
'/ẙ|ʏ|ƴ|ɏ|ỵ|ẏ|ӳ|ӯ|ў|ý|ÿ|ŷ|ỳ|ỹ|ỷ|ỵ|й/' => 'y',
'/В/' => 'V',
'/в/' => 'v',
'/Ŵ/' => 'W',
'/ŵ/' => 'w',
'/Ź|Ż|Ž|Ζ|З/' => 'Z',
'/ź|ż|ž|ζ|з/' => 'z',
'/Æ|Ǽ/' => 'AE',
'/ß/' => 'ss',
'/IJ/' => 'IJ',
'/ij/' => 'ij',
'/Œ/' => 'OE',
'/ƒ/' => 'f',
'/ξ/' => 'ks',
'/π/' => 'p',
'/β/' => 'v',
'/μ/' => 'm',
'/ψ/' => 'ps',
'/Ё/' => 'Yo',
'/ё/' => 'yo',
'/Є/' => 'Ye',
'/є/' => 'ye',
'/Ї/' => 'Yi',
'/Ж/' => 'Zh',
'/ж/' => 'zh',
'/Х/' => 'Kh',
'/х/' => 'kh',
'/Ц/' => 'Ts',
'/ц/' => 'ts',
'/Ч/' => 'Ch',
'/ч/' => 'ch',
'/Ш/' => 'Sh',
'/ш/' => 'sh',
'/Щ/' => 'Shch',
'/щ/' => 'shch',
'/Ъ|ъ|Ь|ь/' => '',
'/Ю/' => 'Yu',
'/ю/' => 'yu',
'/Я/' => 'Ya',
'/я/' => 'ya',
];
}
+44
View File
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
/**
* Publisher Configuration
*
* Defines basic security restrictions for the Publisher class
* to prevent abuse by injecting malicious files into a project.
*/
class Publisher extends BaseConfig
{
/**
* A list of allowed destinations with a (pseudo-)regex
* of allowed files for each destination.
* Attempts to publish to directories not in this list will
* result in a PublisherException. Files that do no fit the
* pattern will cause copy/merge to fail.
*
* @var array<string, string>
*/
public $restrictions = [
ROOTPATH => '*',
FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
];
/**
* Disables Registrars to prevent modules from altering the restrictions.
*/
final protected function registerProperties(): void
{
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Config;
use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Debug\Exceptions;
use Config\Cache;
use Config\Exceptions as ExceptionsConfig;
/**
* Services Configuration file.
*
* Services are simply other classes/libraries that the system uses
* to do its job. This is used by CodeIgniter to allow the core of the
* framework to be swapped out easily without affecting the usage within
* the rest of your application.
*
* This is used in place of a Dependency Injection container primarily
* due to its simplicity, which allows a better long-term maintenance
* of the applications built on top of CodeIgniter. A bonus side-effect
* is that IDEs are able to determine what class you are calling
* whereas with DI Containers there usually isn't a way for them to do this.
*
* @see http://blog.ircmaxell.com/2015/11/simple-easy-risk-and-change.html
* @see http://www.infoq.com/presentations/Simple-Made-Easy
* @see \CodeIgniter\Config\ServicesTest
*/
class Services extends BaseService
{
/**
* The cache class provides a simple way to store and retrieve
* complex data for later.
*
* @return CacheInterface
*/
public static function cache(?Cache $config = null, bool $getShared = true)
{
if ($getShared) {
return static::getSharedInstance('cache', $config);
}
$config ??= config(Cache::class);
return CacheFactory::getHandler($config);
}
/**
* The Exceptions class holds the methods that handle:
*
* - set_exception_handler
* - set_error_handler
* - register_shutdown_function
*
* @return Exceptions
*/
public static function exceptions(
?ExceptionsConfig $config = null,
bool $getShared = true,
) {
if ($getShared) {
return static::getSharedInstance('exceptions', $config);
}
$config ??= config(ExceptionsConfig::class);
return new Exceptions($config);
}
}
+122
View File
@@ -0,0 +1,122 @@
<?php namespace Config;
/**
* {{www.xunruicms.com}}
* {{迅睿内容管理框架系统}}
* 本文件是框架系统文件,二次开发时不可以修改本文件
**/
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Debug\Toolbar\Collectors\Database;
use CodeIgniter\Debug\Toolbar\Collectors\Files;
use CodeIgniter\Debug\Toolbar\Collectors\Routes;
use CodeIgniter\Debug\Toolbar\Collectors\Views;
/**
* --------------------------------------------------------------------------
* Debug Toolbar
* --------------------------------------------------------------------------
*
* The Debug Toolbar provides a way to see information about the performance
* and state of your application during that page display. By default it will
* NOT be displayed under production environments, and will only display if
* `CI_DEBUG` is true, since if it's not, there's not much to display anyway.
*/
class Toolbar extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Toolbar Collectors
* --------------------------------------------------------------------------
*
* List of toolbar collectors that will be called when Debug Toolbar
* fires up and collects data from.
*
* @var string[]
*/
public $collectors = [
Database::class,
Views::class,
Files::class,
Routes::class
];
/**
* --------------------------------------------------------------------------
* Max History
* --------------------------------------------------------------------------
*
* `$maxHistory` sets a limit on the number of past requests that are stored,
* helping to conserve file space used to store them. You can set it to
* 0 (zero) to not have any history stored, or -1 for unlimited history.
*
* @var integer
*/
public $maxHistory = 20;
/**
* --------------------------------------------------------------------------
* Toolbar Views Path
* --------------------------------------------------------------------------
*
* The full path to the the views that are used by the toolbar.
* This MUST have a trailing slash.
*
* @var string
*/
public $viewsPath = FRAMEPATH . 'Debug/Toolbar/Views/';
/**
* --------------------------------------------------------------------------
* Max Queries
* --------------------------------------------------------------------------
*
* If the Database Collector is enabled, it will log every query that the
* the system generates so they can be displayed on the toolbar's timeline
* and in the query log. This can lead to memory issues in some instances
* with hundreds of queries.
*
* `$maxQueries` defines the maximum amount of queries that will be stored.
*
* @var integer
*/
public $maxQueries = 100;
/**
* --------------------------------------------------------------------------
* Collect Var Data
* --------------------------------------------------------------------------
*
* If set to false var data from the views will not be colleted. Usefull to
* avoid high memory usage when there are lots of data passed to the view.
*
* @var bool
*/
public $collectVarData = true;
/**
* --------------------------------------------------------------------------
* Watched Directories
* --------------------------------------------------------------------------
*
* Contains an array of directories that will be watched for changes and
* used to determine if the hot-reload feature should reload the page or not.
* We restrict the values to keep performance as high as possible.
*
* NOTE: The ROOTPATH will be prepended to all values.
*/
public $watchedDirectories = [
'app',
];
/**
* --------------------------------------------------------------------------
* Watched File Extensions
* --------------------------------------------------------------------------
*
* Contains an array of file extensions that will be watched for changes and
* used to determine if the hot-reload feature should reload the page or not.
*/
public $watchedExtensions = [
'php', 'css', 'js', 'html', 'svg', 'json', 'env',
];
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use ArgumentCountError;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\BadMethodCallException;
use ErrorException;
/**
* @template TConnection
* @template TStatement
* @template TResult
*
* @implements PreparedQueryInterface<TConnection, TStatement, TResult>
*/
abstract class BasePreparedQuery implements PreparedQueryInterface
{
/**
* The prepared statement itself.
*
* @var TStatement|null
*/
protected $statement;
/**
* The error code, if any.
*
* @var int
*/
protected $errorCode;
/**
* The error message, if any.
*
* @var string
*/
protected $errorString;
/**
* Holds the prepared query object
* that is cloned during execute.
*
* @var Query
*/
protected $query;
/**
* A reference to the db connection to use.
*
* @var BaseConnection<TConnection, TResult>
*/
protected $db;
public function __construct(BaseConnection $db)
{
$this->db = $db;
}
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* NOTE: This version is based on SQL code. Child classes should
* override this method.
*
* @return $this
*/
public function prepare(string $sql, array $options = [], string $queryClass = Query::class)
{
// We only supports positional placeholders (?)
// in order to work with the execute method below, so we
// need to replace our named placeholders (:name)
$sql = preg_replace('/:[^\s,)]+/', '?', $sql);
/** @var Query $query */
$query = new $queryClass($this->db);
$query->setQuery($sql);
if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) {
$query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
}
$this->query = $query;
return $this->_prepare($query->getOriginalQuery(), $options);
}
/**
* The database-dependent portion of the prepare statement.
*
* @return $this
*/
abstract public function _prepare(string $sql, array $options = []);
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*
* @return bool|ResultInterface<TConnection, TResult>
*
* @throws DatabaseException
*/
public function execute(...$data)
{
// Execute the Query.
$startTime = microtime(true);
try {
$exception = null;
$result = $this->_execute($data);
} catch (ArgumentCountError|ErrorException $exception) {
$result = false;
}
// Update our query object
$query = clone $this->query;
$query->setBinds($data);
if ($result === false) {
$query->setDuration($startTime, $startTime);
// This will trigger a rollback if transactions are being used
$this->db->handleTransStatus();
if ($this->db->DBDebug) {
// We call this function in order to roll-back queries
// if transactions are enabled. If we don't call this here
// the error message will trigger an exit, causing the
// transactions to remain in limbo.
while ($this->db->transDepth !== 0) {
$transDepth = $this->db->transDepth;
$this->db->transComplete();
if ($transDepth === $this->db->transDepth) {
log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
break;
}
}
// Let others do something with this query.
Events::trigger('DBQuery', $query);
if ($exception !== null) {
throw new DatabaseException($exception->getMessage(), $exception->getCode(), $exception);
}
return false;
}
// Let others do something with this query.
Events::trigger('DBQuery', $query);
return false;
}
$query->setDuration($startTime);
// Let others do something with this query
Events::trigger('DBQuery', $query);
if ($this->db->isWriteType((string) $query)) {
return true;
}
// Return a result object
$resultClass = str_replace('PreparedQuery', 'Result', static::class);
$resultID = $this->_getResult();
return new $resultClass($this->db->connID, $resultID);
}
/**
* The database dependant version of the execute method.
*/
abstract public function _execute(array $data): bool;
/**
* Returns the result object for the prepared query.
*
* @return object|resource|null
*/
abstract public function _getResult();
/**
* Explicitly closes the prepared statement.
*
* @throws BadMethodCallException
*/
public function close(): bool
{
if (! isset($this->statement)) {
throw new BadMethodCallException('Cannot call close on a non-existing prepared statement.');
}
try {
return $this->_close();
} finally {
$this->statement = null;
}
}
/**
* The database-dependent version of the close method.
*/
abstract protected function _close(): bool;
/**
* Returns the SQL that has been prepared.
*/
public function getQueryString(): string
{
if (! $this->query instanceof QueryInterface) {
throw new BadMethodCallException('Cannot call getQueryString on a prepared query until after the query has been prepared.');
}
return $this->query->getQuery();
}
/**
* A helper to determine if any error exists.
*/
public function hasError(): bool
{
return ! empty($this->errorString);
}
/**
* Returns the error code created while executing this statement.
*/
public function getErrorCode(): int
{
return $this->errorCode;
}
/**
* Returns the error message created while executing this statement.
*/
public function getErrorMessage(): string
{
return $this->errorString;
}
/**
* Whether the input contain binary data.
*/
protected function isBinary(string $input): bool
{
return mb_detect_encoding($input, 'UTF-8', true) === false;
}
}
+537
View File
@@ -0,0 +1,537 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Entity\Entity;
use stdClass;
/**
* @template TConnection
* @template TResult
*
* @implements ResultInterface<TConnection, TResult>
*/
abstract class BaseResult implements ResultInterface
{
/**
* Connection ID
*
* @var TConnection
*/
public $connID;
/**
* Result ID
*
* @var false|TResult
*/
public $resultID;
/**
* Result Array
*
* @var list<array>
*/
public $resultArray = [];
/**
* Result Object
*
* @var list<object>
*/
public $resultObject = [];
/**
* Custom Result Object
*
* @var array
*/
public $customResultObject = [];
/**
* Current Row index
*
* @var int
*/
public $currentRow = 0;
/**
* The number of records in the query result
*
* @var int|null
*/
protected $numRows;
/**
* Row data
*
* @var array|null
*/
public $rowData;
/**
* Constructor
*
* @param TConnection $connID
* @param TResult $resultID
*/
public function __construct(&$connID, &$resultID)
{
$this->connID = $connID;
$this->resultID = $resultID;
}
/**
* Retrieve the results of the query. Typically an array of
* individual data rows, which can be either an 'array', an
* 'object', or a custom class name.
*
* @param string $type The row type. Either 'array', 'object', or a class name to use
*/
public function getResult(string $type = 'object'): array
{
if ($type === 'array') {
return $this->getResultArray();
}
if ($type === 'object') {
return $this->getResultObject();
}
return $this->getCustomResultObject($type);
}
/**
* Returns the results as an array of custom objects.
*
* @param class-string $className
*
* @return array
*/
public function getCustomResultObject(string $className)
{
if (isset($this->customResultObject[$className])) {
return $this->customResultObject[$className];
}
if (! $this->isValidResultId()) {
return [];
}
// Don't fetch the result set again if we already have it
$_data = null;
if (($c = count($this->resultArray)) > 0) {
$_data = 'resultArray';
} elseif (($c = count($this->resultObject)) > 0) {
$_data = 'resultObject';
}
if ($_data !== null) {
for ($i = 0; $i < $c; $i++) {
$this->customResultObject[$className][$i] = new $className();
foreach ($this->{$_data}[$i] as $key => $value) {
$this->customResultObject[$className][$i]->{$key} = $value;
}
}
return $this->customResultObject[$className];
}
if ($this->rowData !== null) {
$this->dataSeek();
}
$this->customResultObject[$className] = [];
while ($row = $this->fetchObject($className)) {
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
$row->syncOriginal();
}
$this->customResultObject[$className][] = $row;
}
return $this->customResultObject[$className];
}
/**
* Returns the results as an array of arrays.
*
* If no results, an empty array is returned.
*/
public function getResultArray(): array
{
if ($this->resultArray !== []) {
return $this->resultArray;
}
// In the event that query caching is on, the result_id variable
// will not be a valid resource so we'll simply return an empty
// array.
if (! $this->isValidResultId()) {
return [];
}
if ($this->resultObject !== []) {
foreach ($this->resultObject as $row) {
$this->resultArray[] = (array) $row;
}
return $this->resultArray;
}
if ($this->rowData !== null) {
$this->dataSeek();
}
while ($row = $this->fetchAssoc()) {
$this->resultArray[] = $row;
}
return $this->resultArray;
}
/**
* Returns the results as an array of objects.
*
* If no results, an empty array is returned.
*
* @return list<stdClass>
*/
public function getResultObject(): array
{
if ($this->resultObject !== []) {
return $this->resultObject;
}
// In the event that query caching is on, the result_id variable
// will not be a valid resource so we'll simply return an empty
// array.
if (! $this->isValidResultId()) {
return [];
}
if ($this->resultArray !== []) {
foreach ($this->resultArray as $row) {
$this->resultObject[] = (object) $row;
}
return $this->resultObject;
}
if ($this->rowData !== null) {
$this->dataSeek();
}
while ($row = $this->fetchObject()) {
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
$row->syncOriginal();
}
$this->resultObject[] = $row;
}
return $this->resultObject;
}
/**
* Wrapper object to return a row as either an array, an object, or
* a custom class.
*
* If the row doesn't exist, returns null.
*
* @template T of object
*
* @param int|string $n The index of the results to return, or column name.
* @param 'array'|'object'|class-string<T> $type The type of result object. 'array', 'object' or class name.
*
* @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)))
*/
public function getRow($n = 0, string $type = 'object')
{
// $n is a column name.
if (! is_numeric($n)) {
// We cache the row data for subsequent uses
if (! is_array($this->rowData)) {
$this->rowData = $this->getRowArray();
}
// array_key_exists() instead of isset() to allow for NULL values
if (empty($this->rowData) || ! array_key_exists($n, $this->rowData)) {
return null;
}
return $this->rowData[$n];
}
if ($type === 'object') {
return $this->getRowObject($n);
}
if ($type === 'array') {
return $this->getRowArray($n);
}
return $this->getCustomRowObject($n, $type);
}
/**
* Returns a row as a custom class instance.
*
* If the row doesn't exist, returns null.
*
* @template T of object
*
* @param int $n The index of the results to return.
* @param class-string<T> $className
*
* @return T|null
*/
public function getCustomRowObject(int $n, string $className)
{
if (! isset($this->customResultObject[$className])) {
$this->getCustomResultObject($className);
}
if (empty($this->customResultObject[$className])) {
return null;
}
if ($n !== $this->currentRow && isset($this->customResultObject[$className][$n])) {
$this->currentRow = $n;
}
return $this->customResultObject[$className][$this->currentRow];
}
/**
* Returns a single row from the results as an array.
*
* If row doesn't exist, returns null.
*
* @return array|null
*/
public function getRowArray(int $n = 0)
{
$result = $this->getResultArray();
if ($result === []) {
return null;
}
if ($n !== $this->currentRow && isset($result[$n])) {
$this->currentRow = $n;
}
return $result[$this->currentRow];
}
/**
* Returns a single row from the results as an object.
*
* If row doesn't exist, returns null.
*
* @return object|stdClass|null
*/
public function getRowObject(int $n = 0)
{
$result = $this->getResultObject();
if ($result === []) {
return null;
}
if ($n !== $this->customResultObject && isset($result[$n])) {
$this->currentRow = $n;
}
return $result[$this->currentRow];
}
/**
* Assigns an item into a particular column slot.
*
* @param array|string $key
* @param array|object|stdClass|null $value
*
* @return void
*/
public function setRow($key, $value = null)
{
// We cache the row data for subsequent uses
if (! is_array($this->rowData)) {
$this->rowData = $this->getRowArray();
}
if (is_array($key)) {
foreach ($key as $k => $v) {
$this->rowData[$k] = $v;
}
return;
}
if ($key !== '' && $value !== null) {
$this->rowData[$key] = $value;
}
}
/**
* Returns the "first" row of the current results.
*
* @return array|object|null
*/
public function getFirstRow(string $type = 'object')
{
$result = $this->getResult($type);
return ($result === []) ? null : $result[0];
}
/**
* Returns the "last" row of the current results.
*
* @return array|object|null
*/
public function getLastRow(string $type = 'object')
{
$result = $this->getResult($type);
return ($result === []) ? null : $result[count($result) - 1];
}
/**
* Returns the "next" row of the current results.
*
* @return array|object|null
*/
public function getNextRow(string $type = 'object')
{
$result = $this->getResult($type);
if ($result === []) {
return null;
}
return isset($result[$this->currentRow + 1]) ? $result[++$this->currentRow] : null;
}
/**
* Returns the "previous" row of the current results.
*
* @return array|object|null
*/
public function getPreviousRow(string $type = 'object')
{
$result = $this->getResult($type);
if ($result === []) {
return null;
}
if (isset($result[$this->currentRow - 1])) {
$this->currentRow--;
}
return $result[$this->currentRow];
}
/**
* Returns an unbuffered row and move the pointer to the next row.
*
* @return array|object|null
*/
public function getUnbufferedRow(string $type = 'object')
{
if ($type === 'array') {
return $this->fetchAssoc();
}
if ($type === 'object') {
return $this->fetchObject();
}
return $this->fetchObject($type);
}
/**
* Number of rows in the result set; checks for previous count, falls
* back on counting resultArray or resultObject, finally fetching resultArray
* if nothing was previously fetched
*/
public function getNumRows(): int
{
if (is_int($this->numRows)) {
return $this->numRows;
}
if ($this->resultArray !== []) {
return $this->numRows = count($this->resultArray);
}
if ($this->resultObject !== []) {
return $this->numRows = count($this->resultObject);
}
return $this->numRows = count($this->getResultArray());
}
private function isValidResultId(): bool
{
return is_resource($this->resultID) || is_object($this->resultID);
}
/**
* Gets the number of fields in the result set.
*/
abstract public function getFieldCount(): int;
/**
* Generates an array of column names in the result set.
*/
abstract public function getFieldNames(): array;
/**
* Generates an array of objects representing field meta-data.
*/
abstract public function getFieldData(): array;
/**
* Frees the current result.
*
* @return void
*/
abstract public function freeResult();
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return bool
*/
abstract public function dataSeek(int $n = 0);
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return array|false|null
*/
abstract protected function fetchAssoc();
/**
* Returns the result set as an object.
*
* @param class-string $className
*
* @return false|object
*/
abstract protected function fetchObject(string $className = stdClass::class);
}
+328
View File
@@ -0,0 +1,328 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Class BaseUtils
*/
abstract class BaseUtils
{
/**
* Database object
*
* @var object
*/
protected $db;
/**
* List databases statement
*
* @var bool|string
*/
protected $listDatabases = false;
/**
* OPTIMIZE TABLE statement
*
* @var bool|string
*/
protected $optimizeTable = false;
/**
* REPAIR TABLE statement
*
* @var bool|string
*/
protected $repairTable = false;
/**
* Class constructor
*/
public function __construct(ConnectionInterface $db)
{
$this->db = $db;
}
/**
* List databases
*
* @return array|bool
*
* @throws DatabaseException
*/
public function listDatabases()
{
// Is there a cached result?
if (isset($this->db->dataCache['db_names'])) {
return $this->db->dataCache['db_names'];
}
if ($this->listDatabases === false) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
return false;
}
$this->db->dataCache['db_names'] = [];
$query = $this->db->query($this->listDatabases);
if ($query === false) {
return $this->db->dataCache['db_names'];
}
for ($i = 0, $query = $query->getResultArray(), $c = count($query); $i < $c; $i++) {
$this->db->dataCache['db_names'][] = current($query[$i]);
}
return $this->db->dataCache['db_names'];
}
/**
* Determine if a particular database exists
*/
public function databaseExists(string $databaseName): bool
{
return in_array($databaseName, $this->listDatabases(), true);
}
/**
* Optimize Table
*
* @return bool
*
* @throws DatabaseException
*/
public function optimizeTable(string $tableName)
{
if ($this->optimizeTable === false) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
return false;
}
$query = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName)));
return $query !== false;
}
/**
* Optimize Database
*
* @return mixed
*
* @throws DatabaseException
*/
public function optimizeDatabase()
{
if ($this->optimizeTable === false) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
return false;
}
$result = [];
foreach ($this->db->listTables() as $tableName) {
$res = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName)));
if (is_bool($res)) {
return $res;
}
// Build the result array...
$res = $res->getResultArray();
// Postgre & SQLite3 returns empty array
if (empty($res)) {
$key = $tableName;
} else {
$res = current($res);
$key = str_replace($this->db->database . '.', '', current($res));
$keys = array_keys($res);
unset($res[$keys[0]]);
}
$result[$key] = $res;
}
return $result;
}
/**
* Repair Table
*
* @return mixed
*
* @throws DatabaseException
*/
public function repairTable(string $tableName)
{
if ($this->repairTable === false) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
return false;
}
$query = $this->db->query(sprintf($this->repairTable, $this->db->escapeIdentifiers($tableName)));
if (is_bool($query)) {
return $query;
}
$query = $query->getResultArray();
return current($query);
}
/**
* Generate CSV from a query result object
*
* @return string
*/
public function getCSVFromResult(ResultInterface $query, string $delim = ',', string $newline = "\n", string $enclosure = '"')
{
$out = '';
foreach ($query->getFieldNames() as $name) {
$out .= $enclosure . str_replace($enclosure, $enclosure . $enclosure, $name) . $enclosure . $delim;
}
$out = substr($out, 0, -strlen($delim)) . $newline;
// Next blast through the result array and build out the rows
while ($row = $query->getUnbufferedRow('array')) {
$line = [];
foreach ($row as $item) {
$line[] = $enclosure . str_replace(
$enclosure,
$enclosure . $enclosure,
(string) $item,
) . $enclosure;
}
$out .= implode($delim, $line) . $newline;
}
return $out;
}
/**
* Generate XML data from a query result object
*/
public function getXMLFromResult(ResultInterface $query, array $params = []): string
{
foreach (['root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t"] as $key => $val) {
if (! isset($params[$key])) {
$params[$key] = $val;
}
}
$root = $params['root'];
$newline = $params['newline'];
$tab = $params['tab'];
$element = $params['element'];
helper('xml');
$xml = '<' . $root . '>' . $newline;
while ($row = $query->getUnbufferedRow()) {
$xml .= $tab . '<' . $element . '>' . $newline;
foreach ($row as $key => $val) {
$val = (! empty($val)) ? xml_convert((string) $val) : '';
$xml .= $tab . $tab . '<' . $key . '>' . $val . '</' . $key . '>' . $newline;
}
$xml .= $tab . '</' . $element . '>' . $newline;
}
return $xml . '</' . $root . '>' . $newline;
}
/**
* Database Backup
*
* @param array|string $params
*
* @return false|never|string
*
* @throws DatabaseException
*/
public function backup($params = [])
{
if (is_string($params)) {
$params = ['tables' => $params];
}
$prefs = [
'tables' => [],
'ignore' => [],
'filename' => '',
'format' => 'gzip', // gzip, txt
'add_drop' => true,
'add_insert' => true,
'newline' => "\n",
'foreign_key_checks' => true,
];
if (! empty($params)) {
foreach (array_keys($prefs) as $key) {
if (isset($params[$key])) {
$prefs[$key] = $params[$key];
}
}
}
if (empty($prefs['tables'])) {
$prefs['tables'] = $this->db->listTables();
}
if (! in_array($prefs['format'], ['gzip', 'txt'], true)) {
$prefs['format'] = 'txt';
}
if ($prefs['format'] === 'gzip' && ! function_exists('gzencode')) {
if ($this->db->DBDebug) {
throw new DatabaseException('The file compression format you chose is not supported by your server.');
}
$prefs['format'] = 'txt';
}
if ($prefs['format'] === 'txt') {
return $this->_backup($prefs);
}
// @TODO gzencode() requires `ext-zlib`, but _backup() is not implemented in all databases.
return gzencode($this->_backup($prefs));
}
/**
* Platform dependent version of the backup function.
*
* @return false|never|string
*/
abstract public function _backup(?array $prefs = null);
}
+153
View File
@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Exceptions\InvalidArgumentException;
use Config\Database as DbConfig;
/**
* @see \CodeIgniter\Database\ConfigTest
*/
class Config extends BaseConfig
{
/**
* Cache for instance of any connections that
* have been requested as a "shared" instance.
*
* @var array
*/
protected static $instances = [];
/**
* The main instance used to manage all of
* our open database connections.
*
* @var Database|null
*/
protected static $factory;
/**
* Returns the database connection
*
* @param array|BaseConnection|non-empty-string|null $group The name of the connection group to use,
* or an array of configuration settings.
* @param bool $getShared Whether to return a shared instance of the connection.
*
* @return BaseConnection
*/
public static function connect($group = null, bool $getShared = true)
{
// If a DB connection is passed in, just pass it back
if ($group instanceof BaseConnection) {
return $group;
}
if (is_array($group)) {
$config = $group;
$group = 'custom-' . md5(json_encode($config));
} else {
$dbConfig = config(DbConfig::class);
if ($group === null) {
$group = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup;
}
assert(is_string($group));
if (! isset($dbConfig->{$group})) {
throw new InvalidArgumentException('"' . $group . '" is not a valid database connection group.');
}
$config = $dbConfig->{$group};
}
if ($getShared && isset(static::$instances[$group])) {
return static::$instances[$group];
}
static::ensureFactory();
$connection = static::$factory->load($config, $group);
static::$instances[$group] = $connection;
return $connection;
}
/**
* Returns an array of all db connections currently made.
*/
public static function getConnections(): array
{
return static::$instances;
}
/**
* Loads and returns an instance of the Forge for the specified
* database group, and loads the group if it hasn't been loaded yet.
*
* @param array|ConnectionInterface|string|null $group
*
* @return Forge
*/
public static function forge($group = null)
{
$db = static::connect($group);
return static::$factory->loadForge($db);
}
/**
* Returns a new instance of the Database Utilities class.
*
* @param array|string|null $group
*
* @return BaseUtils
*/
public static function utils($group = null)
{
$db = static::connect($group);
return static::$factory->loadUtils($db);
}
/**
* Returns a new instance of the Database Seeder.
*
* @param non-empty-string|null $group
*
* @return Seeder
*/
public static function seeder(?string $group = null)
{
$config = config(DbConfig::class);
return new Seeder($config, static::connect($group));
}
/**
* Ensures the database Connection Manager/Factory is loaded and ready to use.
*
* @return void
*/
protected static function ensureFactory()
{
if (static::$factory instanceof Database) {
return;
}
static::$factory = new Database();
}
}
@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
/**
* @template TConnection
* @template TResult
*
* @property false|object|resource $connID
* @property-read string $DBDriver
*/
interface ConnectionInterface
{
/**
* Initializes the database connection/settings.
*
* @return void
*/
public function initialize();
/**
* Connect to the database.
*
* @return false|TConnection
*/
public function connect(bool $persistent = false);
/**
* Create a persistent database connection.
*
* @return false|TConnection
*/
public function persistentConnect();
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*
* @return void
*/
public function reconnect();
/**
* Returns the actual connection object. If both a 'read' and 'write'
* connection has been specified, you can pass either term in to
* get that connection. If you pass either alias in and only a single
* connection is present, it must return the sole connection.
*
* @return false|TConnection
*/
public function getConnection(?string $alias = null);
/**
* Select a specific database table to use.
*
* @return bool
*/
public function setDatabase(string $databaseName);
/**
* Returns the name of the current database being used.
*/
public function getDatabase(): string;
/**
* Returns the last error encountered by this connection.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string, int|string>
*/
public function error(): array;
/**
* The name of the platform in use (MySQLi, mssql, etc)
*/
public function getPlatform(): string;
/**
* Returns a string containing the version of the database being used.
*/
public function getVersion(): string;
/**
* 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 array|string|null $binds
*
* @return BaseResult<TConnection, TResult>|bool|Query
*/
public function query(string $sql, $binds = null);
/**
* Performs a basic query against the database. No binding or caching
* is performed, nor are transactions handled. Simply takes a raw
* query string and returns the database-specific result id.
*
* @return false|TResult
*/
public function simpleQuery(string $sql);
/**
* Returns an instance of the query builder for this connection.
*
* @param array|string $tableName Table name.
*
* @return BaseBuilder Builder.
*/
public function table($tableName);
/**
* Returns the last query's statement object.
*
* @return Query
*/
public function getLastQuery();
/**
* "Smart" Escaping
*
* Escapes data based on type.
* Sets boolean and null types.
*
* @param array|bool|float|int|object|string|null $str
*
* @return ($str is array ? array : float|int|string)
*/
public function escape($str);
/**
* Allows for custom calls to the database engine that are not
* supported through our database layer.
*
* @param array ...$params
*
* @return array|bool|float|int|object|resource|string|null
*/
public function callFunction(string $functionName, ...$params);
/**
* Determines if the statement is a write-type query or not.
*
* @param string $sql
*/
public function isWriteType($sql): bool;
}
+184
View File
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Exceptions\ConfigException;
use CodeIgniter\Exceptions\CriticalError;
use CodeIgniter\Exceptions\InvalidArgumentException;
/**
* Database Connection Factory
*
* Creates and returns an instance of the appropriate Database Connection.
*/
class Database
{
/**
* Maintains an array of the instances of all connections that have
* been created.
*
* Helps to keep track of all open connections for performance
* monitoring, logging, etc.
*
* @var array
*/
protected $connections = [];
/**
* Parses the connection binds and creates a Database Connection instance.
*
* @return BaseConnection
*
* @throws InvalidArgumentException
*/
public function load(array $params = [], string $alias = '')
{
if ($alias === '') {
throw new InvalidArgumentException('You must supply the parameter: alias.');
}
if (! empty($params['DSN']) && str_contains($params['DSN'], '://')) {
$params = $this->parseDSN($params);
}
if (empty($params['DBDriver'])) {
throw new InvalidArgumentException('You have not selected a database type to connect to.');
}
assert($this->checkDbExtension($params['DBDriver']));
$this->connections[$alias] = $this->initDriver($params['DBDriver'], 'Connection', $params);
return $this->connections[$alias];
}
/**
* Creates a Forge instance for the current database type.
*/
public function loadForge(ConnectionInterface $db): Forge
{
if (! $db->connID) {
$db->initialize();
}
return $this->initDriver($db->DBDriver, 'Forge', $db);
}
/**
* Creates an instance of Utils for the current database type.
*/
public function loadUtils(ConnectionInterface $db): BaseUtils
{
if (! $db->connID) {
$db->initialize();
}
return $this->initDriver($db->DBDriver, 'Utils', $db);
}
/**
* Parses universal DSN string
*
* @throws InvalidArgumentException
*/
protected function parseDSN(array $params): array
{
$dsn = parse_url($params['DSN']);
if ($dsn === 0 || $dsn === '' || $dsn === '0' || $dsn === [] || $dsn === false || $dsn === null) {
throw new InvalidArgumentException('Your DSN connection string is invalid.');
}
$dsnParams = [
'DSN' => '',
'DBDriver' => $dsn['scheme'],
'hostname' => isset($dsn['host']) ? rawurldecode($dsn['host']) : '',
'port' => isset($dsn['port']) ? rawurldecode((string) $dsn['port']) : '',
'username' => isset($dsn['user']) ? rawurldecode($dsn['user']) : '',
'password' => isset($dsn['pass']) ? rawurldecode($dsn['pass']) : '',
'database' => isset($dsn['path']) ? rawurldecode(substr($dsn['path'], 1)) : '',
];
if (isset($dsn['query']) && ($dsn['query'] !== '')) {
parse_str($dsn['query'], $extra);
foreach ($extra as $key => $val) {
if (is_string($val) && in_array(strtolower($val), ['true', 'false', 'null'], true)) {
$val = $val === 'null' ? null : filter_var($val, FILTER_VALIDATE_BOOLEAN);
}
$dsnParams[$key] = $val;
}
}
return array_merge($params, $dsnParams);
}
/**
* Creates a database object.
*
* @param string $driver Driver name. FQCN can be used.
* @param string $class 'Connection'|'Forge'|'Utils'
* @param array|ConnectionInterface $argument The constructor parameter or DB connection
*
* @return BaseConnection|BaseUtils|Forge
*/
protected function initDriver(string $driver, string $class, $argument): object
{
$classname = (! str_contains($driver, '\\'))
? "CodeIgniter\\Database\\{$driver}\\{$class}"
: $driver . '\\' . $class;
return new $classname($argument);
}
/**
* Check the PHP database extension is loaded.
*
* @param string $driver DB driver or FQCN for custom driver
*/
private function checkDbExtension(string $driver): bool
{
if (str_contains($driver, '\\')) {
// Cannot check a fully qualified classname for a custom driver.
return true;
}
$extensionMap = [
// DBDriver => PHP extension
'MySQLi' => 'mysqli',
'SQLite3' => 'sqlite3',
'Postgre' => 'pgsql',
'SQLSRV' => 'sqlsrv',
'OCI8' => 'oci8',
];
$extension = $extensionMap[$driver] ?? '';
if ($extension === '') {
$message = 'Invalid DBDriver name: "' . $driver . '"';
throw new ConfigException($message);
}
if (extension_loaded($extension)) {
return true;
}
$message = 'The required PHP extension "' . $extension . '" is not loaded.'
. ' Install and enable it to use "' . $driver . '" driver.';
throw new CriticalError($message);
}
}
@@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Exceptions;
use CodeIgniter\Exceptions\DebugTraceableTrait;
use CodeIgniter\Exceptions\RuntimeException;
class DataException extends RuntimeException implements ExceptionInterface
{
use DebugTraceableTrait;
/**
* Used by the Model's trigger() method when the callback cannot be found.
*
* @return DataException
*/
public static function forInvalidMethodTriggered(string $method)
{
return new static(lang('Database.invalidEvent', [$method]));
}
/**
* Used by Model's insert/update methods when there isn't
* any data to actually work with.
*
* @return DataException
*/
public static function forEmptyDataset(string $mode)
{
return new static(lang('Database.emptyDataset', [$mode]));
}
/**
* Used by Model's insert/update methods when there is no
* primary key defined and Model has option `useAutoIncrement`
* set to false.
*
* @return DataException
*/
public static function forEmptyPrimaryKey(string $mode)
{
return new static(lang('Database.emptyPrimaryKey', [$mode]));
}
/**
* Thrown when an argument for one of the Model's methods
* were empty or otherwise invalid, and they could not be
* to work correctly for that method.
*
* @return DataException
*/
public static function forInvalidArgument(string $argument)
{
return new static(lang('Database.invalidArgument', [$argument]));
}
/**
* @return DataException
*/
public static function forInvalidAllowedFields(string $model)
{
return new static(lang('Database.invalidAllowedFields', [$model]));
}
/**
* @return DataException
*/
public static function forTableNotFound(string $table)
{
return new static(lang('Database.tableNotFound', [$table]));
}
/**
* @return DataException
*/
public static function forEmptyInputGiven(string $argument)
{
return new static(lang('Database.forEmptyInputGiven', [$argument]));
}
/**
* @return DataException
*/
public static function forFindColumnHaveMultipleColumns()
{
return new static(lang('Database.forFindColumnHaveMultipleColumns'));
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Exceptions;
use CodeIgniter\Exceptions\HasExitCodeInterface;
use CodeIgniter\Exceptions\RuntimeException;
class DatabaseException extends RuntimeException implements ExceptionInterface, HasExitCodeInterface
{
public function getExitCode(): int
{
return EXIT_DATABASE;
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Exceptions;
/**
* Provides a domain-level interface for broad capture
* of all database-related exceptions.
*
* catch (\CodeIgniter\Database\Exceptions\ExceptionInterface) { ... }
*/
interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface
{
}
File diff suppressed because it is too large Load Diff
+78
View File
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use Config\Database;
/**
* Class Migration
*/
abstract class Migration
{
/**
* The name of the database group to use.
*
* @var string|null
*/
protected $DBGroup;
/**
* Database Connection instance
*
* @var ConnectionInterface
*/
protected $db;
/**
* Database Forge instance.
*
* @var Forge
*/
protected $forge;
public function __construct(?Forge $forge = null)
{
if (isset($this->DBGroup)) {
$this->forge = Database::forge($this->DBGroup);
} elseif ($forge instanceof Forge) {
$this->forge = $forge;
} else {
$this->forge = Database::forge(config(Database::class)->defaultGroup);
}
$this->db = $this->forge->getConnection();
}
/**
* Returns the database group name this migration uses.
*/
public function getDBGroup(): ?string
{
return $this->DBGroup;
}
/**
* Perform a migration step.
*
* @return void
*/
abstract public function up();
/**
* Revert a migration step.
*
* @return void
*/
abstract public function down();
}
+881
View File
@@ -0,0 +1,881 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\ConfigException;
use CodeIgniter\Exceptions\RuntimeException;
use CodeIgniter\I18n\Time;
use Config\Database;
use Config\Migrations as MigrationsConfig;
use stdClass;
/**
* Class MigrationRunner
*/
class MigrationRunner
{
/**
* Whether or not migrations are allowed to run.
*
* @var bool
*/
protected $enabled = false;
/**
* Name of table to store meta information
*
* @var string
*/
protected $table;
/**
* The Namespace where migrations can be found.
* `null` is all namespaces.
*
* @var string|null
*/
protected $namespace;
/**
* The database Group to migrate.
*
* @var string
*/
protected $group;
/**
* The migration name.
*
* @var string
*/
protected $name;
/**
* The pattern used to locate migration file versions.
*
* @var string
*/
protected $regex = '/\A(\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6})_(\w+)\z/';
/**
* The main database connection. Used to store
* migration information in.
*
* @var BaseConnection
*/
protected $db;
/**
* If true, will continue instead of throwing
* exceptions.
*
* @var bool
*/
protected $silent = false;
/**
* used to return messages for CLI.
*
* @var array
*/
protected $cliMessages = [];
/**
* Tracks whether we have already ensured
* the table exists or not.
*
* @var bool
*/
protected $tableChecked = false;
/**
* The full path to locate migration files.
*
* @var string
*/
protected $path;
/**
* The database Group filter.
*
* @var string|null
*/
protected $groupFilter;
/**
* Used to skip current migration.
*
* @var bool
*/
protected $groupSkip = false;
/**
* The migration can manage multiple databases. So it should always use the
* default DB group so that it creates the `migrations` table in the default
* DB group. Therefore, passing $db is for testing purposes only.
*
* @param array|ConnectionInterface|string|null $db DB group. For testing purposes only.
*
* @throws ConfigException
*/
public function __construct(MigrationsConfig $config, $db = null)
{
$this->enabled = $config->enabled ?? false;
$this->table = $config->table ?? 'migrations';
$this->namespace = APP_NAMESPACE;
// Even if a DB connection is passed, since it is a test,
// it is assumed to use the default group name
$this->group = is_string($db) ? $db : config(Database::class)->defaultGroup;
$this->db = db_connect($db);
}
/**
* Locate and run all new migrations
*
* @return bool
*
* @throws ConfigException
* @throws RuntimeException
*/
public function latest(?string $group = null)
{
if (! $this->enabled) {
throw ConfigException::forDisabledMigrations();
}
$this->ensureTable();
if ($group !== null) {
$this->groupFilter = $group;
$this->setGroup($group);
}
$migrations = $this->findMigrations();
if ($migrations === []) {
return true;
}
foreach ($this->getHistory((string) $group) as $history) {
unset($migrations[$this->getObjectUid($history)]);
}
$batch = $this->getLastBatch() + 1;
foreach ($migrations as $migration) {
if ($this->migrate('up', $migration)) {
if ($this->groupSkip === true) {
$this->groupSkip = false;
continue;
}
$this->addHistory($migration, $batch);
} else {
$this->regress(-1);
$message = lang('Migrations.generalFault');
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
}
$data = get_object_vars($this);
$data['method'] = 'latest';
Events::trigger('migrate', $data);
return true;
}
/**
* Migrate down to a previous batch
*
* Calls each migration step required to get to the provided batch
*
* @param int $targetBatch Target batch number, or negative for a relative batch, 0 for all
* @param string|null $group Deprecated. The designation has no effect.
*
* @return bool True on success, FALSE on failure or no migrations are found
*
* @throws ConfigException
* @throws RuntimeException
*/
public function regress(int $targetBatch = 0, ?string $group = null)
{
if (! $this->enabled) {
throw ConfigException::forDisabledMigrations();
}
$this->ensureTable();
$batches = $this->getBatches();
if ($targetBatch < 0) {
$targetBatch = $batches[count($batches) - 1 + $targetBatch] ?? 0;
}
if ($batches === [] && $targetBatch === 0) {
return true;
}
if ($targetBatch !== 0 && ! in_array($targetBatch, $batches, true)) {
$message = lang('Migrations.batchNotFound') . $targetBatch;
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$tmpNamespace = $this->namespace;
$this->namespace = null;
$allMigrations = $this->findMigrations();
$migrations = [];
while ($batch = array_pop($batches)) {
if ($batch <= $targetBatch) {
break;
}
foreach ($this->getBatchHistory($batch, 'desc') as $history) {
$uid = $this->getObjectUid($history);
if (! isset($allMigrations[$uid])) {
$message = lang('Migrations.gap') . ' ' . $history->version;
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$migration = $allMigrations[$uid];
$migration->history = $history;
$migrations[] = $migration;
}
}
foreach ($migrations as $migration) {
if ($this->migrate('down', $migration)) {
$this->removeHistory($migration->history);
} else {
$message = lang('Migrations.generalFault');
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
}
$data = get_object_vars($this);
$data['method'] = 'regress';
Events::trigger('migrate', $data);
$this->namespace = $tmpNamespace;
return true;
}
/**
* Migrate a single file regardless of order or batches.
* Method "up" or "down" determined by presence in history.
* NOTE: This is not recommended and provided mostly for testing.
*
* @param string $path Full path to a valid migration file
* @param string $path Namespace of the target migration
*
* @return bool
*/
public function force(string $path, string $namespace, ?string $group = null)
{
if (! $this->enabled) {
throw ConfigException::forDisabledMigrations();
}
$this->ensureTable();
if ($group !== null) {
$this->groupFilter = $group;
$this->setGroup($group);
}
$migration = $this->migrationFromFile($path, $namespace);
if (empty($migration)) {
$message = lang('Migrations.notFound');
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$method = 'up';
$this->setNamespace($migration->namespace);
foreach ($this->getHistory($this->group) as $history) {
if ($this->getObjectUid($history) === $migration->uid) {
$method = 'down';
$migration->history = $history;
break;
}
}
if ($method === 'up') {
$batch = $this->getLastBatch() + 1;
if ($this->migrate('up', $migration) && $this->groupSkip === false) {
$this->addHistory($migration, $batch);
return true;
}
$this->groupSkip = false;
} elseif ($this->migrate('down', $migration)) {
$this->removeHistory($migration->history);
return true;
}
$message = lang('Migrations.generalFault');
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
/**
* Retrieves list of available migration scripts
*
* @return array List of all located migrations by their UID
*/
public function findMigrations(): array
{
$namespaces = $this->namespace !== null ? [$this->namespace] : array_keys(service('autoloader')->getNamespace());
$migrations = [];
foreach ($namespaces as $namespace) {
if (ENVIRONMENT !== 'testing' && $namespace === 'Tests\Support') {
continue;
}
foreach ($this->findNamespaceMigrations($namespace) as $migration) {
$migrations[$migration->uid] = $migration;
}
}
// Sort migrations ascending by their UID (version)
ksort($migrations);
return $migrations;
}
/**
* Retrieves a list of available migration scripts for one namespace
*/
public function findNamespaceMigrations(string $namespace): array
{
$migrations = [];
$locator = service('locator', true);
if (! empty($this->path)) {
helper('filesystem');
$dir = rtrim($this->path, DIRECTORY_SEPARATOR) . '/';
$files = get_filenames($dir, true, false, false);
} else {
$files = $locator->listNamespaceFiles($namespace, '/Database/Migrations/');
}
foreach ($files as $file) {
$file = empty($this->path) ? $file : $this->path . str_replace($this->path, '', $file);
if ($migration = $this->migrationFromFile($file, $namespace)) {
$migrations[] = $migration;
}
}
return $migrations;
}
/**
* Create a migration object from a file path.
*
* @param string $path Full path to a valid migration file.
*
* @return false|object Returns the migration object, or false on failure
*/
protected function migrationFromFile(string $path, string $namespace)
{
if (! str_ends_with($path, '.php')) {
return false;
}
$filename = basename($path, '.php');
if (preg_match($this->regex, $filename) !== 1) {
return false;
}
$locator = service('locator', true);
$migration = new stdClass();
$migration->version = $this->getMigrationNumber($filename);
$migration->name = $this->getMigrationName($filename);
$migration->path = $path;
$migration->class = $locator->getClassname($path);
$migration->namespace = $namespace;
$migration->uid = $this->getObjectUid($migration);
return $migration;
}
/**
* Allows other scripts to modify on the fly as needed.
*
* @return MigrationRunner
*/
public function setNamespace(?string $namespace)
{
$this->namespace = $namespace;
return $this;
}
/**
* Allows other scripts to modify on the fly as needed.
*
* @return MigrationRunner
*/
public function setGroup(string $group)
{
$this->group = $group;
return $this;
}
/**
* @return MigrationRunner
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* If $silent == true, then will not throw exceptions and will
* attempt to continue gracefully.
*
* @return MigrationRunner
*/
public function setSilent(bool $silent)
{
$this->silent = $silent;
return $this;
}
/**
* Extracts the migration number from a filename
*
* @param string $migration A migration filename w/o path.
*/
protected function getMigrationNumber(string $migration): string
{
preg_match($this->regex, $migration, $matches);
return $matches !== [] ? $matches[1] : '0';
}
/**
* Extracts the migration name from a filename
*
* Note: The migration name should be the classname, but maybe they are
* different.
*
* @param string $migration A migration filename w/o path.
*/
protected function getMigrationName(string $migration): string
{
preg_match($this->regex, $migration, $matches);
return $matches !== [] ? $matches[2] : '';
}
/**
* Uses the non-repeatable portions of a migration or history
* to create a sortable unique key
*
* @param object $object migration or $history
*/
public function getObjectUid($object): string
{
return preg_replace('/[^0-9]/', '', $object->version) . $object->class;
}
/**
* Retrieves messages formatted for CLI output
*/
public function getCliMessages(): array
{
return $this->cliMessages;
}
/**
* Clears any CLI messages.
*
* @return MigrationRunner
*/
public function clearCliMessages()
{
$this->cliMessages = [];
return $this;
}
/**
* Truncates the history table.
*
* @return void
*/
public function clearHistory()
{
if ($this->db->tableExists($this->table)) {
$this->db->table($this->table)->truncate();
}
}
/**
* Add a history to the table.
*
* @param object $migration
*
* @return void
*/
protected function addHistory($migration, int $batch)
{
$this->db->table($this->table)->insert([
'version' => $migration->version,
'class' => $migration->class,
'group' => $this->group,
'namespace' => $migration->namespace,
'time' => Time::now()->getTimestamp(),
'batch' => $batch,
]);
if (is_cli()) {
$this->cliMessages[] = sprintf(
"\t%s(%s) %s_%s",
CLI::color(lang('Migrations.added'), 'yellow'),
$migration->namespace,
$migration->version,
$migration->class,
);
}
}
/**
* Removes a single history
*
* @param object $history
*
* @return void
*/
protected function removeHistory($history)
{
$this->db->table($this->table)->where('id', $history->id)->delete();
if (is_cli()) {
$this->cliMessages[] = sprintf(
"\t%s(%s) %s_%s",
CLI::color(lang('Migrations.removed'), 'yellow'),
$history->namespace,
$history->version,
$history->class,
);
}
}
/**
* Grabs the full migration history from the database for a group
*/
public function getHistory(string $group = 'default'): array
{
$this->ensureTable();
$builder = $this->db->table($this->table);
// If group was specified then use it
if ($group !== '') {
$builder->where('group', $group);
}
// If a namespace was specified then use it
if ($this->namespace !== null) {
$builder->where('namespace', $this->namespace);
}
$query = $builder->orderBy('id', 'ASC')->get();
return ! empty($query) ? $query->getResultObject() : [];
}
/**
* Returns the migration history for a single batch.
*
* @param string $order
*/
public function getBatchHistory(int $batch, $order = 'asc'): array
{
$this->ensureTable();
$query = $this->db->table($this->table)
->where('batch', $batch)
->orderBy('id', $order)
->get();
return ! empty($query) ? $query->getResultObject() : [];
}
/**
* Returns all the batches from the database history in order
*/
public function getBatches(): array
{
$this->ensureTable();
$batches = $this->db->table($this->table)
->select('batch')
->distinct()
->orderBy('batch', 'asc')
->get()
->getResultArray();
return array_map('intval', array_column($batches, 'batch'));
//return array_map(intval(...), array_column($batches, 'batch'));
}
/**
* Returns the value of the last batch in the database.
*/
public function getLastBatch(): int
{
$this->ensureTable();
$batch = $this->db->table($this->table)
->selectMax('batch')
->get()
->getResultObject();
$batch = is_array($batch) && $batch !== []
? end($batch)->batch
: 0;
return (int) $batch;
}
/**
* Returns the version number of the first migration for a batch.
* Mostly just for tests.
*/
public function getBatchStart(int $batch): string
{
if ($batch < 0) {
$batches = $this->getBatches();
$batch = $batches[count($batches) - 1] ?? 0;
}
$migration = $this->db->table($this->table)
->where('batch', $batch)
->orderBy('id', 'asc')
->limit(1)
->get()
->getResultObject();
return $migration !== [] ? $migration[0]->version : '0';
}
/**
* Returns the version number of the last migration for a batch.
* Mostly just for tests.
*/
public function getBatchEnd(int $batch): string
{
if ($batch < 0) {
$batches = $this->getBatches();
$batch = $batches[count($batches) - 1] ?? 0;
}
$migration = $this->db->table($this->table)
->where('batch', $batch)
->orderBy('id', 'desc')
->limit(1)
->get()
->getResultObject();
return $migration === [] ? '0' : $migration[0]->version;
}
/**
* Ensures that we have created our migrations table
* in the database.
*
* @return void
*/
public function ensureTable()
{
if ($this->tableChecked || $this->db->tableExists($this->table)) {
return;
}
$forge = Database::forge($this->db);
$forge->addField([
'id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
'auto_increment' => true,
],
'version' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'class' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'group' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'namespace' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'time' => [
'type' => 'INT',
'constraint' => 11,
'null' => false,
],
'batch' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
]);
$forge->addPrimaryKey('id');
$forge->createTable($this->table, true);
$this->tableChecked = true;
}
/**
* Handles the actual running of a migration.
*
* @param string $direction "up" or "down"
* @param object $migration The migration to run
*/
protected function migrate($direction, $migration): bool
{
include_once $migration->path;
$class = $migration->class;
$this->setName($migration->name);
// Validate the migration file structure
if (! class_exists($class, false)) {
$message = sprintf(lang('Migrations.classNotFound'), $class);
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
/** @var Migration $instance */
$instance = new $class(Database::forge($this->db));
$group = $instance->getDBGroup() ?? $this->group;
if (ENVIRONMENT !== 'testing' && $group === 'tests' && $this->groupFilter !== 'tests') {
// @codeCoverageIgnoreStart
$this->groupSkip = true;
return true;
// @codeCoverageIgnoreEnd
}
if ($direction === 'up' && $this->groupFilter !== null && $this->groupFilter !== $group) {
$this->groupSkip = true;
return true;
}
if (! is_callable([$instance, $direction])) {
$message = sprintf(lang('Migrations.missingMethod'), $direction);
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$instance->{$direction}();
return true;
}
}
+147
View File
@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\RawSql;
/**
* Builder for MySQLi
*/
class Builder extends BaseBuilder
{
/**
* Identifier escape character
*
* @var string
*/
protected $escapeChar = '`';
/**
* Specifies which sql statements
* support the ignore option.
*
* @var array<string, string>
*/
protected $supportedIgnoreStatements = [
'update' => 'IGNORE',
'insert' => 'IGNORE',
'delete' => 'IGNORE',
];
/**
* FROM tables
*
* Groups tables in FROM clauses if needed, so there is no confusion
* about operator precedence.
*
* Note: This is only used (and overridden) by MySQL.
*/
protected function _fromTables(): string
{
if ($this->QBJoin !== [] && count($this->QBFrom) > 1) {
return '(' . implode(', ', $this->QBFrom) . ')';
}
return implode(', ', $this->QBFrom);
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _updateBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
}
return ''; // @codeCoverageIgnore
}
$updateFields = $this->QBOptions['updateFields'] ??
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
[];
$alias = $this->QBOptions['alias'] ?? '`_u`';
$sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n";
$sql .= "INNER JOIN (\n{:_table_:}";
$sql .= ') ' . $alias . "\n";
$sql .= 'ON ' . implode(
' AND ',
array_map(
static fn ($key, $value) => (
($value instanceof RawSql && is_string($key))
?
$table . '.' . $key . ' = ' . $value
:
(
$value instanceof RawSql
?
$value
:
$table . '.' . $value . ' = ' . $alias . '.' . $value
)
),
array_keys($constraints),
$constraints,
),
) . "\n";
$sql .= "SET\n";
$sql .= implode(
",\n",
array_map(
static fn ($key, $value): string => $table . '.' . $key . ($value instanceof RawSql ?
' = ' . $value :
' = ' . $alias . '.' . $value),
array_keys($updateFields),
$updateFields,
),
);
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" UNION ALL\n",
array_map(
static fn ($value): string => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index): string => $index . ' ' . $key,
$keys,
$value,
)),
$values,
),
) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
}
File diff suppressed because it is too large Load Diff
+265
View File
@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\Forge as BaseForge;
/**
* Forge for MySQLi
*/
class Forge extends BaseForge
{
/**
* CREATE DATABASE statement
*
* @var string
*/
protected $createDatabaseStr = 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s';
/**
* CREATE DATABASE IF statement
*
* @var string
*/
protected $createDatabaseIfStr = 'CREATE DATABASE IF NOT EXISTS %s CHARACTER SET %s COLLATE %s';
/**
* DROP CONSTRAINT statement
*
* @var string
*/
protected $dropConstraintStr = 'ALTER TABLE %s DROP FOREIGN KEY %s';
/**
* CREATE TABLE keys flag
*
* Whether table keys are created from within the
* CREATE TABLE statement.
*
* @var bool
*/
protected $createTableKeys = true;
/**
* UNSIGNED support
*
* @var array
*/
protected $_unsigned = [
'TINYINT',
'SMALLINT',
'MEDIUMINT',
'INT',
'INTEGER',
'BIGINT',
'REAL',
'DOUBLE',
'DOUBLE PRECISION',
'FLOAT',
'DECIMAL',
'NUMERIC',
];
/**
* Table Options list which required to be quoted
*
* @var array
*/
protected $_quoted_table_options = [
'COMMENT',
'COMPRESSION',
'CONNECTION',
'DATA DIRECTORY',
'INDEX DIRECTORY',
'ENCRYPTION',
'PASSWORD',
];
/**
* NULL value representation in CREATE/ALTER TABLE statements
*
* @var string
*
* @internal
*/
protected $null = 'NULL';
/**
* CREATE TABLE attributes
*
* @param array $attributes Associative array of table attributes
*/
protected function _createTableAttributes(array $attributes): string
{
$sql = '';
foreach (array_keys($attributes) as $key) {
if (is_string($key)) {
$sql .= ' ' . strtoupper($key) . ' = ';
if (in_array(strtoupper($key), $this->_quoted_table_options, true)) {
$sql .= $this->db->escape($attributes[$key]);
} else {
$sql .= $this->db->escapeString($attributes[$key]);
}
}
}
if ($this->db->charset !== '' && ! str_contains($sql, 'CHARACTER SET') && ! str_contains($sql, 'CHARSET')) {
$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
}
if ($this->db->DBCollat !== '' && ! str_contains($sql, 'COLLATE')) {
$sql .= ' COLLATE = ' . $this->db->escapeString($this->db->DBCollat);
}
return $sql;
}
/**
* ALTER TABLE
*
* @param string $alterType ALTER type
* @param string $table Table name
* @param array|string $processedFields Processed column definitions
* or column names to DROP
*
* @return ($alterType is 'DROP' ? string : list<string>)
*/
protected function _alterTable(string $alterType, string $table, $processedFields)
{
if ($alterType === 'DROP') {
return parent::_alterTable($alterType, $table, $processedFields);
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
foreach ($processedFields as $i => $field) {
if ($field['_literal'] !== false) {
$processedFields[$i] = ($alterType === 'ADD') ? "\n\tADD " . $field['_literal'] : "\n\tMODIFY " . $field['_literal'];
} else {
if ($alterType === 'ADD') {
$processedFields[$i]['_literal'] = "\n\tADD ";
} else {
$processedFields[$i]['_literal'] = empty($field['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
}
$processedFields[$i] = $processedFields[$i]['_literal'] . $this->_processColumn($processedFields[$i]);
}
}
return [$sql . implode(',', $processedFields)];
}
/**
* Process column
*/
protected function _processColumn(array $processedField): string
{
$extraClause = isset($processedField['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($processedField['after']) : '';
if (empty($extraClause) && isset($processedField['first']) && $processedField['first'] === true) {
$extraClause = ' FIRST';
}
return $this->db->escapeIdentifiers($processedField['name'])
. (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name']))
. ' ' . $processedField['type'] . $processedField['length']
. $processedField['unsigned']
. $processedField['null']
. $processedField['default']
. $processedField['auto_increment']
. $processedField['unique']
. (empty($processedField['comment']) ? '' : ' COMMENT ' . $processedField['comment'])
. $extraClause;
}
/**
* Generates SQL to add indexes
*
* @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE
*/
protected function _processIndexes(string $table, bool $asQuery = false): array
{
$sqls = [''];
$index = 0;
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
$index = $i;
if ($asQuery === false) {
$index = 0;
}
if (isset($this->keys[$i]['fields'])) {
for ($i2 = 0, $c2 = count($this->keys[$i]['fields']); $i2 < $c2; $i2++) {
if (! isset($this->fields[$this->keys[$i]['fields'][$i2]])) {
unset($this->keys[$i]['fields'][$i2]);
continue;
}
}
}
if (! is_array($this->keys[$i]['fields'])) {
$this->keys[$i]['fields'] = [$this->keys[$i]['fields']];
}
$unique = in_array($i, $this->uniqueKeys, true) ? 'UNIQUE ' : '';
$keyName = $this->db->escapeIdentifiers(($this->keys[$i]['keyName'] === '') ?
implode('_', $this->keys[$i]['fields']) :
$this->keys[$i]['keyName']);
if ($asQuery) {
$sqls[$index] = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table) . " ADD {$unique}KEY "
. $keyName
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
} else {
$sqls[$index] .= ",\n\t{$unique}KEY " . $keyName
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
}
}
$this->keys = [];
return $sqls;
}
/**
* Drop Key
*/
public function dropKey(string $table, string $keyName, bool $prefixKeyName = true): bool
{
$sql = sprintf(
$this->dropIndexStr,
$this->db->escapeIdentifiers($keyName),
$this->db->escapeIdentifiers($this->db->DBPrefix . $table),
);
return $this->db->query($sql);
}
/**
* Drop Primary Key
*/
public function dropPrimaryKey(string $table, string $keyName = ''): bool
{
$sql = sprintf(
'ALTER TABLE %s DROP PRIMARY KEY',
$this->db->escapeIdentifiers($this->db->DBPrefix . $table),
);
return $this->db->query($sql);
}
}
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BasePreparedQuery;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Exceptions\BadMethodCallException;
use mysqli;
use mysqli_result;
use mysqli_sql_exception;
use mysqli_stmt;
/**
* Prepared query for MySQLi
*
* @extends BasePreparedQuery<mysqli, mysqli_stmt, mysqli_result>
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* NOTE: This version is based on SQL code. Child classes should
* override this method.
*
* @param array $options Passed to the connection's prepare statement.
* Unused in the MySQLi driver.
*/
public function _prepare(string $sql, array $options = []): PreparedQuery
{
// Mysqli driver doesn't like statements
// with terminating semicolons.
$sql = rtrim($sql, ';');
if (! $this->statement = $this->db->mysqli->prepare($sql)) {
$this->errorCode = $this->db->mysqli->errno;
$this->errorString = $this->db->mysqli->error;
if ($this->db->DBDebug) {
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
}
}
return $this;
}
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*/
public function _execute(array $data): bool
{
if (! isset($this->statement)) {
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
}
// First off - bind the parameters
$bindTypes = '';
$binaryData = [];
// Determine the type string
foreach ($data as $key => $item) {
if (is_int($item)) {
$bindTypes .= 'i';
} elseif (is_numeric($item)) {
$bindTypes .= 'd';
} elseif (is_string($item) && $this->isBinary($item)) {
$bindTypes .= 'b';
$binaryData[$key] = $item;
} else {
$bindTypes .= 's';
}
}
// Bind it
$this->statement->bind_param($bindTypes, ...$data);
// Stream binary data
foreach ($binaryData as $key => $value) {
$this->statement->send_long_data($key, $value);
}
try {
return $this->statement->execute();
} catch (mysqli_sql_exception $e) {
if ($this->db->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
return false;
}
}
/**
* Returns the result object for the prepared query or false on failure.
*
* @return false|mysqli_result
*/
public function _getResult()
{
return $this->statement->get_result();
}
/**
* Deallocate prepared statements.
*/
protected function _close(): bool
{
return $this->statement->close();
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Entity\Entity;
use mysqli;
use mysqli_result;
use stdClass;
/**
* Result for MySQLi
*
* @extends BaseResult<mysqli, mysqli_result>
*/
class Result extends BaseResult
{
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int
{
return $this->resultID->field_count;
}
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array
{
$fieldNames = [];
$this->resultID->field_seek(0);
while ($field = $this->resultID->fetch_field()) {
$fieldNames[] = $field->name;
}
return $fieldNames;
}
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array
{
static $dataTypes = [
MYSQLI_TYPE_DECIMAL => 'decimal',
MYSQLI_TYPE_NEWDECIMAL => 'newdecimal',
MYSQLI_TYPE_FLOAT => 'float',
MYSQLI_TYPE_DOUBLE => 'double',
MYSQLI_TYPE_BIT => 'bit',
MYSQLI_TYPE_SHORT => 'short',
MYSQLI_TYPE_LONG => 'long',
MYSQLI_TYPE_LONGLONG => 'longlong',
MYSQLI_TYPE_INT24 => 'int24',
MYSQLI_TYPE_YEAR => 'year',
MYSQLI_TYPE_TIMESTAMP => 'timestamp',
MYSQLI_TYPE_DATE => 'date',
MYSQLI_TYPE_TIME => 'time',
MYSQLI_TYPE_DATETIME => 'datetime',
MYSQLI_TYPE_NEWDATE => 'newdate',
MYSQLI_TYPE_SET => 'set',
MYSQLI_TYPE_VAR_STRING => 'var_string',
MYSQLI_TYPE_STRING => 'string',
MYSQLI_TYPE_GEOMETRY => 'geometry',
MYSQLI_TYPE_TINY_BLOB => 'tiny_blob',
MYSQLI_TYPE_MEDIUM_BLOB => 'medium_blob',
MYSQLI_TYPE_LONG_BLOB => 'long_blob',
MYSQLI_TYPE_BLOB => 'blob',
];
$retVal = [];
$fieldData = $this->resultID->fetch_fields();
foreach ($fieldData as $i => $data) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $data->name;
$retVal[$i]->type = $data->type;
$retVal[$i]->type_name = in_array($data->type, [1, 247], true) ? 'char' : ($dataTypes[$data->type] ?? null);
$retVal[$i]->max_length = $data->max_length;
$retVal[$i]->primary_key = $data->flags & 2;
$retVal[$i]->length = $data->length;
$retVal[$i]->default = $data->def;
}
return $retVal;
}
/**
* Frees the current result.
*
* @return void
*/
public function freeResult()
{
if (is_object($this->resultID)) {
$this->resultID->free();
$this->resultID = false;
}
}
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return bool
*/
public function dataSeek(int $n = 0)
{
return $this->resultID->data_seek($n);
}
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return array|false|null
*/
protected function fetchAssoc()
{
return $this->resultID->fetch_assoc();
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return Entity|false|object|stdClass
*/
protected function fetchObject(string $className = 'stdClass')
{
if (is_subclass_of($className, Entity::class)) {
return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($data);
}
return $this->resultID->fetch_object($className);
}
/**
* Returns the number of rows in the resultID (i.e., mysqli_result object)
*/
public function getNumRows(): int
{
if (! is_int($this->numRows)) {
$this->numRows = $this->resultID->num_rows;
}
return $this->numRows;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseUtils;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Utils for MySQLi
*/
class Utils extends BaseUtils
{
/**
* List databases statement
*
* @var string
*/
protected $listDatabases = 'SHOW DATABASES';
/**
* OPTIMIZE TABLE statement
*
* @var string
*/
protected $optimizeTable = 'OPTIMIZE TABLE %s';
/**
* Platform dependent version of the backup function.
*
* @return never
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Exceptions\BadMethodCallException;
/**
* @template TConnection
* @template TStatement
* @template TResult
*/
interface PreparedQueryInterface
{
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*
* @return bool|ResultInterface<TConnection, TResult>
*/
public function execute(...$data);
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* @return $this
*/
public function prepare(string $sql, array $options = []);
/**
* Explicity closes the statement.
*
* @throws BadMethodCallException
*/
public function close(): bool;
/**
* Returns the SQL that has been prepared.
*/
public function getQueryString(): string;
/**
* Returns the error code created while executing this statement.
*/
public function getErrorCode(): int;
/**
* Returns the error message created while executing this statement.
*/
public function getErrorMessage(): string;
}
+433
View File
@@ -0,0 +1,433 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use Stringable;
/**
* Query builder
*/
class Query implements QueryInterface, Stringable
{
/**
* The query string, as provided by the user.
*
* @var string
*/
protected $originalQueryString;
/**
* The query string if table prefix has been swapped.
*
* @var string|null
*/
protected $swappedQueryString;
/**
* The final query string after binding, etc.
*
* @var string|null
*/
protected $finalQueryString;
/**
* The binds and their values used for binding.
*
* @var array
*/
protected $binds = [];
/**
* Bind marker
*
* Character used to identify values in a prepared statement.
*
* @var string
*/
protected $bindMarker = '?';
/**
* The start time in seconds with microseconds
* for when this query was executed.
*
* @var float|string
*/
protected $startTime;
/**
* The end time in seconds with microseconds
* for when this query was executed.
*
* @var float
*/
protected $endTime;
/**
* The error code, if any.
*
* @var int
*/
protected $errorCode;
/**
* The error message, if any.
*
* @var string
*/
protected $errorString;
/**
* Pointer to database connection.
* Mainly for escaping features.
*
* @var ConnectionInterface
*/
public $db;
public function __construct(ConnectionInterface $db)
{
$this->db = $db;
}
/**
* Sets the raw query string to use for this statement.
*
* @param mixed $binds
*
* @return $this
*/
public function setQuery(string $sql, $binds = null, bool $setEscape = true)
{
$this->originalQueryString = $sql;
unset($this->swappedQueryString);
if ($binds !== null) {
if (! is_array($binds)) {
$binds = [$binds];
}
if ($setEscape) {
array_walk($binds, static function (&$item): void {
$item = [
$item,
true,
];
});
}
$this->binds = $binds;
}
unset($this->finalQueryString);
return $this;
}
/**
* Will store the variables to bind into the query later.
*
* @return $this
*/
public function setBinds(array $binds, bool $setEscape = true)
{
if ($setEscape) {
array_walk($binds, static function (&$item): void {
$item = [$item, true];
});
}
$this->binds = $binds;
unset($this->finalQueryString);
return $this;
}
/**
* Returns the final, processed query string after binding, etal
* has been performed.
*/
public function getQuery(): string
{
if (empty($this->finalQueryString)) {
$this->compileBinds();
}
return $this->finalQueryString;
}
/**
* Records the execution time of the statement using microtime(true)
* for it's start and end values. If no end value is present, will
* use the current time to determine total duration.
*
* @return $this
*/
public function setDuration(float $start, ?float $end = null)
{
$this->startTime = $start;
if ($end === null) {
$end = microtime(true);
}
$this->endTime = $end;
return $this;
}
/**
* Returns the start time in seconds with microseconds.
*
* @return float|string
*/
public function getStartTime(bool $returnRaw = false, int $decimals = 6)
{
if ($returnRaw) {
return $this->startTime;
}
return number_format($this->startTime, $decimals);
}
/**
* Returns the duration of this query during execution, or null if
* the query has not been executed yet.
*
* @param int $decimals The accuracy of the returned time.
*/
public function getDuration(int $decimals = 6): string
{
return number_format(($this->endTime - $this->startTime), $decimals);
}
/**
* Stores the error description that happened for this query.
*
* @return $this
*/
public function setError(int $code, string $error)
{
$this->errorCode = $code;
$this->errorString = $error;
return $this;
}
/**
* Reports whether this statement created an error not.
*/
public function hasError(): bool
{
return ! empty($this->errorString);
}
/**
* Returns the error code created while executing this statement.
*/
public function getErrorCode(): int
{
return $this->errorCode;
}
/**
* Returns the error message created while executing this statement.
*/
public function getErrorMessage(): string
{
return $this->errorString;
}
/**
* Determines if the statement is a write-type query or not.
*/
public function isWriteType(): bool
{
return $this->db->isWriteType($this->originalQueryString);
}
/**
* Swaps out one table prefix for a new one.
*
* @return $this
*/
public function swapPrefix(string $orig, string $swap)
{
$sql = $this->swappedQueryString ?? $this->originalQueryString;
$from = '/(\W)' . $orig . '(\S)/';
$to = '\\1' . $swap . '\\2';
$this->swappedQueryString = preg_replace($from, $to, $sql);
unset($this->finalQueryString);
return $this;
}
/**
* Returns the original SQL that was passed into the system.
*/
public function getOriginalQuery(): string
{
return $this->originalQueryString;
}
/**
* Escapes and inserts any binds into the finalQueryString property.
*
* @see https://regex101.com/r/EUEhay/5
*
* @return void
*/
protected function compileBinds()
{
$sql = $this->swappedQueryString ?? $this->originalQueryString;
$binds = $this->binds;
if (empty($binds)) {
$this->finalQueryString = $sql;
return;
}
if (is_int(array_key_first($binds))) {
$bindCount = count($binds);
$ml = strlen($this->bindMarker);
$this->finalQueryString = $this->matchSimpleBinds($sql, $binds, $bindCount, $ml);
} else {
// Reverse the binds so that duplicate named binds
// will be processed prior to the original binds.
$binds = array_reverse($binds);
$this->finalQueryString = $this->matchNamedBinds($sql, $binds);
}
}
/**
* Match bindings
*/
protected function matchNamedBinds(string $sql, array $binds): string
{
$replacers = [];
foreach ($binds as $placeholder => $value) {
// $value[1] contains the boolean whether should be escaped or not
$escapedValue = $value[1] ? $this->db->escape($value[0]) : $value[0];
// In order to correctly handle backlashes in saved strings
// we will need to preg_quote, so remove the wrapping escape characters
// otherwise it will get escaped.
if (is_array($value[0])) {
$escapedValue = '(' . implode(',', $escapedValue) . ')';
}
$replacers[":{$placeholder}:"] = $escapedValue;
}
return strtr($sql, $replacers);
}
/**
* Match bindings
*/
protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, int $ml): string
{
if ($c = preg_match_all("/'[^']*'/", $sql, $matches) >= 1) {
$c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', str_replace($matches[0], str_replace($this->bindMarker, str_repeat(' ', $ml), $matches[0]), $sql, $c), $matches, PREG_OFFSET_CAPTURE);
// Bind values' count must match the count of markers in the query
if ($bindCount !== $c) {
return $sql;
}
} elseif (($c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bindCount) {
return $sql;
}
do {
$c--;
$escapedValue = $binds[$c][1] ? $this->db->escape($binds[$c][0]) : $binds[$c][0];
if (is_array($escapedValue)) {
$escapedValue = '(' . implode(',', $escapedValue) . ')';
}
$sql = substr_replace($sql, (string) $escapedValue, $matches[0][$c][1], $ml);
} while ($c !== 0);
return $sql;
}
/**
* Returns string to display in debug toolbar
*/
public function debugToolbarDisplay(): string
{
// Key words we want bolded
static $highlight = [
'AND',
'AS',
'ASC',
'AVG',
'BY',
'COUNT',
'DESC',
'DISTINCT',
'FROM',
'GROUP',
'HAVING',
'IN',
'INNER',
'INSERT',
'INTO',
'IS',
'JOIN',
'LEFT',
'LIKE',
'LIMIT',
'MAX',
'MIN',
'NOT',
'NULL',
'OFFSET',
'ON',
'OR',
'ORDER',
'RIGHT',
'SELECT',
'SUM',
'UPDATE',
'VALUES',
'WHERE',
];
$sql = esc($this->getQuery());
/**
* @see https://stackoverflow.com/a/20767160
* @see https://regex101.com/r/hUlrGN/4
*/
$search = '/\b(?:' . implode('|', $highlight) . ')\b(?![^(&#039;)]*&#039;(?:(?:[^(&#039;)]*&#039;){2})*[^(&#039;)]*$)/';
return preg_replace_callback($search, static fn ($matches): string => '<strong>' . str_replace(' ', '&nbsp;', $matches[0]) . '</strong>', $sql);
}
/**
* Return text representation of the query
*/
public function __toString(): string
{
return $this->getQuery();
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
/**
* Interface QueryInterface
*
* Represents a single statement that can be executed against the database.
* Statements are platform-specific and can handle binding of binds.
*/
interface QueryInterface
{
/**
* Sets the raw query string to use for this statement.
*
* @param mixed $binds
*
* @return $this
*/
public function setQuery(string $sql, $binds = null, bool $setEscape = true);
/**
* Returns the final, processed query string after binding, etal
* has been performed.
*
* @return string
*/
public function getQuery();
/**
* Records the execution time of the statement using microtime(true)
* for it's start and end values. If no end value is present, will
* use the current time to determine total duration.
*
* @return $this
*/
public function setDuration(float $start, ?float $end = null);
/**
* Returns the duration of this query during execution, or null if
* the query has not been executed yet.
*
* @param int $decimals The accuracy of the returned time.
*/
public function getDuration(int $decimals = 6): string;
/**
* Stores the error description that happened for this query.
*
* @return $this
*/
public function setError(int $code, string $error);
/**
* Reports whether this statement created an error not.
*/
public function hasError(): bool;
/**
* Returns the error code created while executing this statement.
*/
public function getErrorCode(): int;
/**
* Returns the error message created while executing this statement.
*/
public function getErrorMessage(): string;
/**
* Determines if the statement is a write-type query or not.
*/
public function isWriteType(): bool;
/**
* Swaps out one table prefix for a new one.
*
* @return $this
*/
public function swapPrefix(string $orig, string $swap);
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use Stringable;
/**
* @see \CodeIgniter\Database\RawSqlTest
*/
class RawSql implements Stringable
{
/**
* @var string Raw SQL string
*/
private string $string;
public function __construct(string $sqlString)
{
$this->string = $sqlString;
}
public function __toString(): string
{
return $this->string;
}
/**
* Create new instance with new SQL string
*/
public function with(string $newSqlString): self
{
$new = clone $this;
$new->string = $newSqlString;
return $new;
}
/**
* Returns unique id for binding key
*/
public function getBindingKey(): string
{
return 'RawSql' . spl_object_id($this);
}
}
+183
View File
@@ -0,0 +1,183 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use stdClass;
/**
* @template TConnection
* @template TResult
*/
interface ResultInterface
{
/**
* Retrieve the results of the query. Typically an array of
* individual data rows, which can be either an 'array', an
* 'object', or a custom class name.
*
* @param string $type The row type. Either 'array', 'object', or a class name to use
*/
public function getResult(string $type = 'object'): array;
/**
* Returns the results as an array of custom objects.
*
* @param string $className The name of the class to use.
*
* @return array
*/
public function getCustomResultObject(string $className);
/**
* Returns the results as an array of arrays.
*
* If no results, an empty array is returned.
*/
public function getResultArray(): array;
/**
* Returns the results as an array of objects.
*
* If no results, an empty array is returned.
*/
public function getResultObject(): array;
/**
* Wrapper object to return a row as either an array, an object, or
* a custom class.
*
* If the row doesn't exist, returns null.
*
* @template T of object
*
* @param int|string $n The index of the results to return, or column name.
* @param 'array'|'object'|class-string<T> $type The type of result object. 'array', 'object' or class name.
*
* @return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)))
*/
public function getRow($n = 0, string $type = 'object');
/**
* Returns a row as a custom class instance.
*
* If the row doesn't exist, returns null.
*
* @template T of object
*
* @param int $n The index of the results to return.
* @param class-string<T> $className
*
* @return T|null
*/
public function getCustomRowObject(int $n, string $className);
/**
* Returns a single row from the results as an array.
*
* If row doesn't exist, returns null.
*
* @return array|null
*/
public function getRowArray(int $n = 0);
/**
* Returns a single row from the results as an object.
*
* If row doesn't exist, returns null.
*
* @return object|stdClass|null
*/
public function getRowObject(int $n = 0);
/**
* Assigns an item into a particular column slot.
*
* @param array|string $key
* @param array|object|stdClass|null $value
*
* @return void
*/
public function setRow($key, $value = null);
/**
* Returns the "first" row of the current results.
*
* @return array|object|null
*/
public function getFirstRow(string $type = 'object');
/**
* Returns the "last" row of the current results.
*
* @return array|object|null
*/
public function getLastRow(string $type = 'object');
/**
* Returns the "next" row of the current results.
*
* @return array|object|null
*/
public function getNextRow(string $type = 'object');
/**
* Returns the "previous" row of the current results.
*
* @return array|object|null
*/
public function getPreviousRow(string $type = 'object');
/**
* Returns number of rows in the result set.
*/
public function getNumRows(): int;
/**
* Returns an unbuffered row and move the pointer to the next row.
*
* @return array|object|null
*/
public function getUnbufferedRow(string $type = 'object');
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int;
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array;
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array;
/**
* Frees the current result.
*
* @return void
*/
public function freeResult();
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return bool
*/
public function dataSeek(int $n = 0);
}
+280
View File
@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\RawSql;
use CodeIgniter\Exceptions\InvalidArgumentException;
/**
* Builder for SQLite3
*/
class Builder extends BaseBuilder
{
/**
* Default installs of SQLite typically do not
* support limiting delete clauses.
*
* @var bool
*/
protected $canLimitDeletes = false;
/**
* Default installs of SQLite do no support
* limiting update queries in combo with WHERE.
*
* @var bool
*/
protected $canLimitWhereUpdates = false;
/**
* ORDER BY random keyword
*
* @var array
*/
protected $randomKeyword = [
'RANDOM()',
];
/**
* @var array<string, string>
*/
protected $supportedIgnoreStatements = [
'insert' => 'OR IGNORE',
];
/**
* Replace statement
*
* Generates a platform-specific replace string from the supplied data
*/
protected function _replace(string $table, array $keys, array $values): string
{
return 'INSERT OR ' . parent::_replace($table, $keys, $values);
}
/**
* Generates a platform-specific truncate string from the supplied data
*
* If the database does not support the TRUNCATE statement,
* then this method maps to 'DELETE FROM table'
*/
protected function _truncate(string $table): string
{
return 'DELETE FROM ' . $table;
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _updateBatch(string $table, array $keys, array $values): string
{
if (version_compare($this->db->getVersion(), '3.33.0') >= 0) {
return parent::_updateBatch($table, $keys, $values);
}
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch updates.');
}
return ''; // @codeCoverageIgnore
}
if (count($constraints) > 1 || isset($this->QBOptions['setQueryAsData']) || (current($constraints) instanceof RawSql)) {
throw new DatabaseException('You are trying to use a feature which requires SQLite version 3.33 or higher.');
}
$index = current($constraints);
$ids = [];
$final = [];
foreach ($values as $val) {
$val = array_combine($keys, $val);
$ids[] = $val[$index];
foreach (array_keys($val) as $field) {
if ($field !== $index) {
$final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field];
}
}
}
$cases = '';
foreach ($final as $k => $v) {
$cases .= $k . " = CASE \n"
. implode("\n", $v) . "\n"
. 'ELSE ' . $k . ' END, ';
}
$this->where($index . ' IN(' . implode(',', $ids) . ')', null, false);
return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
}
/**
* Generates a platform-specific upsertBatch string from the supplied data
*
* @throws DatabaseException
*/
protected function _upsertBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if (empty($constraints)) {
$fieldNames = array_map(static fn ($columnName): string => trim($columnName, '`'), $keys);
$allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool {
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields;
});
foreach ($allIndexes as $index) {
$constraints = $index->fields;
break;
}
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
}
if (empty($constraints)) {
if ($this->db->DBDebug) {
throw new DatabaseException('No constraint found for upsert.');
}
return ''; // @codeCoverageIgnore
}
$alias = $this->QBOptions['alias'] ?? '`excluded`';
if (strtolower($alias) !== '`excluded`') {
throw new InvalidArgumentException('SQLite alias is always named "excluded". A custom alias cannot be used.');
}
$updateFields = $this->QBOptions['updateFields'] ??
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
[];
$sql = 'INSERT INTO ' . $table . ' (';
$sql .= implode(', ', array_map(static fn ($columnName): string => $columnName, $keys));
$sql .= ")\n";
$sql .= '{:_table_:}';
$sql .= 'ON CONFLICT(' . implode(',', $constraints) . ")\n";
$sql .= "DO UPDATE SET\n";
$sql .= implode(
",\n",
array_map(
static fn ($key, $value): string => $key . ($value instanceof RawSql ?
" = {$value}" :
" = {$alias}.{$value}"),
array_keys($updateFields),
$updateFields,
),
);
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$hasWhere = stripos($this->QBOptions['setQueryAsData'], 'WHERE') > 0;
$data = $this->QBOptions['setQueryAsData'] . ($hasWhere ? '' : "\nWHERE 1 = 1\n");
} else {
$data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _deleteBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
}
return ''; // @codeCoverageIgnore
}
$sql = 'DELETE FROM ' . $table . "\n";
if (current($constraints) instanceof RawSql && $this->db->DBDebug) {
throw new DatabaseException('You cannot use RawSql for constraint in SQLite.');
// @codeCoverageIgnore
}
if (is_string(current(array_keys($constraints)))) {
$concat1 = implode(' || ', array_keys($constraints));
$concat2 = implode(' || ', array_values($constraints));
} else {
$concat1 = implode(' || ', $constraints);
$concat2 = $concat1;
}
$sql .= "WHERE {$concat1} IN (SELECT {$concat2} FROM (\n{:_table_:}))";
// where is not supported
if ($this->QBWhere !== [] && $this->db->DBDebug) {
throw new DatabaseException('You cannot use WHERE with SQLite.');
// @codeCoverageIgnore
}
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" UNION ALL\n",
array_map(
static fn ($value): string => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index): string => $index . ' ' . $key,
$keys,
$value,
)),
$values,
),
) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
}
File diff suppressed because it is too large Load Diff
+337
View File
@@ -0,0 +1,337 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Forge as BaseForge;
/**
* Forge for SQLite3
*/
class Forge extends BaseForge
{
/**
* DROP INDEX statement
*
* @var string
*/
protected $dropIndexStr = 'DROP INDEX %s';
/**
* @var Connection
*/
protected $db;
/**
* UNSIGNED support
*
* @var array|bool
*/
protected $_unsigned = false;
/**
* NULL value representation in CREATE/ALTER TABLE statements
*
* @var string
*
* @internal
*/
protected $null = 'NULL';
/**
* Constructor.
*/
public function __construct(BaseConnection $db)
{
parent::__construct($db);
if (version_compare($this->db->getVersion(), '3.3', '<')) {
$this->dropTableIfStr = false;
}
}
/**
* Create database
*
* @param bool $ifNotExists Whether to add IF NOT EXISTS condition
*/
public function createDatabase(string $dbName, bool $ifNotExists = false): bool
{
// In SQLite, a database is created when you connect to the database.
// We'll return TRUE so that an error isn't generated.
return true;
}
/**
* Drop database
*
* @throws DatabaseException
*/
public function dropDatabase(string $dbName): bool
{
// In SQLite, a database is dropped when we delete a file
if (! is_file($dbName)) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unable to drop the specified database.');
}
return false;
}
// We need to close the pseudo-connection first
$this->db->close();
if (! @unlink($dbName)) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unable to drop the specified database.');
}
return false;
}
if (! empty($this->db->dataCache['db_names'])) {
$key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true);
if ($key !== false) {
unset($this->db->dataCache['db_names'][$key]);
}
}
return true;
}
/**
* @param list<string>|string $columnNames
*
* @throws DatabaseException
*/
public function dropColumn(string $table, $columnNames): bool
{
//$columns = is_array($columnNames) ? $columnNames : array_map(trim(...), explode(',', $columnNames));
$columns = is_array($columnNames) ? $columnNames : array_map('trim', explode(',', $columnNames));
$result = (new Table($this->db, $this))
->fromTable($this->db->DBPrefix . $table)
->dropColumn($columns)
->run();
if (! $result && $this->db->DBDebug) {
throw new DatabaseException(sprintf(
'Failed to drop column%s "%s" on "%s" table.',
count($columns) > 1 ? 's' : '',
implode('", "', $columns),
$table,
));
}
return $result;
}
/**
* @param array|string $processedFields Processed column definitions
* or column names to DROP
*
* @return ($alterType is 'DROP' ? string : list<string>|null)
*/
protected function _alterTable(string $alterType, string $table, $processedFields)
{
switch ($alterType) {
case 'CHANGE':
$fieldsToModify = [];
foreach ($processedFields as $processedField) {
$name = $processedField['name'];
$newName = $processedField['new_name'];
$field = $this->fields[$name];
$field['name'] = $name;
$field['new_name'] = $newName;
// Unlike when creating a table, if `null` is not specified,
// the column will be `NULL`, not `NOT NULL`.
if ($processedField['null'] === '') {
$field['null'] = true;
}
$fieldsToModify[] = $field;
}
(new Table($this->db, $this))
->fromTable($table)
->modifyColumn($fieldsToModify)
->run();
return null; // Why null?
default:
return parent::_alterTable($alterType, $table, $processedFields);
}
}
/**
* Process column
*/
protected function _processColumn(array $processedField): string
{
if ($processedField['type'] === 'TEXT' && str_starts_with($processedField['length'], "('")) {
$processedField['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($processedField['name'])
. ' IN ' . $processedField['length'] . ')';
}
return $this->db->escapeIdentifiers($processedField['name'])
. ' ' . $processedField['type']
. $processedField['auto_increment']
. $processedField['null']
. $processedField['unique']
. $processedField['default'];
}
/**
* Field attribute TYPE
*
* Performs a data type mapping between different databases.
*/
protected function _attributeType(array &$attributes)
{
switch (strtoupper($attributes['TYPE'])) {
case 'ENUM':
case 'SET':
$attributes['TYPE'] = 'TEXT';
break;
case 'BOOLEAN':
$attributes['TYPE'] = 'INT';
break;
default:
break;
}
}
/**
* Field attribute AUTO_INCREMENT
*/
protected function _attributeAutoIncrement(array &$attributes, array &$field)
{
if (
! empty($attributes['AUTO_INCREMENT'])
&& $attributes['AUTO_INCREMENT'] === true
&& str_contains(strtolower($field['type']), 'int')
) {
$field['type'] = 'INTEGER PRIMARY KEY';
$field['default'] = '';
$field['null'] = '';
$field['unique'] = '';
$field['auto_increment'] = ' AUTOINCREMENT';
$this->primaryKeys = [];
}
}
/**
* Foreign Key Drop
*
* @throws DatabaseException
*/
public function dropForeignKey(string $table, string $foreignName): bool
{
// 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();
}
/**
* Drop Primary Key
*/
public function dropPrimaryKey(string $table, string $keyName = ''): bool
{
$sqlTable = new Table($this->db, $this);
return $sqlTable->fromTable($this->db->DBPrefix . $table)
->dropPrimaryKey()
->run();
}
public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '', string $fkName = ''): BaseForge
{
if ($fkName === '') {
return parent::addForeignKey($fieldName, $tableName, $tableField, $onUpdate, $onDelete, $fkName);
}
throw new DatabaseException('SQLite does not support foreign key names. CodeIgniter will refer to them in the format: prefix_table_column_referencecolumn_foreign');
}
/**
* Generates SQL to add primary key
*
* @param bool $asQuery When true recreates table with key, else partial SQL used with CREATE TABLE
*/
protected function _processPrimaryKeys(string $table, bool $asQuery = false): string
{
if ($asQuery === false) {
return parent::_processPrimaryKeys($table, $asQuery);
}
$sqlTable = new Table($this->db, $this);
$sqlTable->fromTable($this->db->DBPrefix . $table)
->addPrimaryKey($this->primaryKeys)
->run();
return '';
}
/**
* Generates SQL to add foreign keys
*
* @param bool $asQuery When true recreates table with key, else partial SQL used with CREATE TABLE
*/
protected function _processForeignKeys(string $table, bool $asQuery = false): array
{
if ($asQuery === false) {
return parent::_processForeignKeys($table, $asQuery);
}
$errorNames = [];
foreach ($this->foreignKeys as $name) {
foreach ($name['field'] as $f) {
if (! isset($this->fields[$f])) {
$errorNames[] = $f;
}
}
}
if ($errorNames !== []) {
$errorNames = [implode(', ', $errorNames)];
throw new DatabaseException(lang('Database.fieldNotExists', $errorNames));
}
$sqlTable = new Table($this->db, $this);
$sqlTable->fromTable($this->db->DBPrefix . $table)
->addForeignKey($this->foreignKeys)
->run();
return [];
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\BasePreparedQuery;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Exceptions\BadMethodCallException;
use Exception;
use SQLite3;
use SQLite3Result;
use SQLite3Stmt;
/**
* Prepared query for SQLite3
*
* @extends BasePreparedQuery<SQLite3, SQLite3Stmt, SQLite3Result>
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* The SQLite3Result resource, or false.
*
* @var false|SQLite3Result
*/
protected $result;
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* NOTE: This version is based on SQL code. Child classes should
* override this method.
*
* @param array $options Passed to the connection's prepare statement.
* Unused in the MySQLi driver.
*/
public function _prepare(string $sql, array $options = []): PreparedQuery
{
if (! ($this->statement = $this->db->connID->prepare($sql))) {
$this->errorCode = $this->db->connID->lastErrorCode();
$this->errorString = $this->db->connID->lastErrorMsg();
if ($this->db->DBDebug) {
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
}
}
return $this;
}
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*/
public function _execute(array $data): bool
{
if (! isset($this->statement)) {
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
}
foreach ($data as $key => $item) {
// Determine the type string
if (is_int($item)) {
$bindType = SQLITE3_INTEGER;
} elseif (is_float($item)) {
$bindType = SQLITE3_FLOAT;
} elseif (is_string($item) && $this->isBinary($item)) {
$bindType = SQLITE3_BLOB;
} else {
$bindType = SQLITE3_TEXT;
}
// Bind it
$this->statement->bindValue($key + 1, $item, $bindType);
}
try {
$this->result = $this->statement->execute();
} catch (Exception $e) {
if ($this->db->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
return false;
}
return $this->result !== false;
}
/**
* Returns the result object for the prepared query or false on failure.
*
* @return false|SQLite3Result
*/
public function _getResult()
{
return $this->result;
}
/**
* Deallocate prepared statements.
*/
protected function _close(): bool
{
return $this->statement->close();
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use Closure;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Entity\Entity;
use SQLite3;
use SQLite3Result;
use stdClass;
/**
* Result for SQLite3
*
* @extends BaseResult<SQLite3, SQLite3Result>
*/
class Result extends BaseResult
{
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int
{
return $this->resultID->numColumns();
}
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array
{
$fieldNames = [];
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
$fieldNames[] = $this->resultID->columnName($i);
}
return $fieldNames;
}
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array
{
static $dataTypes = [
SQLITE3_INTEGER => 'integer',
SQLITE3_FLOAT => 'float',
SQLITE3_TEXT => 'text',
SQLITE3_BLOB => 'blob',
SQLITE3_NULL => 'null',
];
$retVal = [];
$this->resultID->fetchArray(SQLITE3_NUM);
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $this->resultID->columnName($i);
$type = $this->resultID->columnType($i);
$retVal[$i]->type = $type;
$retVal[$i]->type_name = $dataTypes[$type] ?? null;
$retVal[$i]->max_length = null;
$retVal[$i]->length = null;
}
$this->resultID->reset();
return $retVal;
}
/**
* Frees the current result.
*
* @return void
*/
public function freeResult()
{
if (is_object($this->resultID)) {
$this->resultID->finalize();
$this->resultID = false;
}
}
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return bool
*
* @throws DatabaseException
*/
public function dataSeek(int $n = 0)
{
if ($n !== 0) {
throw new DatabaseException('SQLite3 doesn\'t support seeking to other offset.');
}
return $this->resultID->reset();
}
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return array|false
*/
protected function fetchAssoc()
{
return $this->resultID->fetchArray(SQLITE3_ASSOC);
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return Entity|false|object|stdClass
*/
protected function fetchObject(string $className = 'stdClass')
{
// No native support for fetching rows as objects
if (($row = $this->fetchAssoc()) === false) {
return false;
}
if ($className === 'stdClass') {
return (object) $row;
}
$classObj = new $className();
if (is_subclass_of($className, Entity::class)) {
return $classObj->injectRawData($row);
}
$classSet = Closure::bind(function ($key, $value): void {
$this->{$key} = $value;
}, $classObj, $className);
foreach (array_keys($row) as $key) {
$classSet($key, $row[$key]);
}
return $classObj;
}
}
+517
View File
@@ -0,0 +1,517 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\Exceptions\DataException;
use stdClass;
/**
* Provides missing features for altering tables that are common
* in other supported databases, but are missing from SQLite.
* These are needed in order to support migrations during testing
* when another database is used as the primary engine, but
* SQLite in memory databases are used for faster test execution.
*/
class Table
{
/**
* All of the fields this table represents.
*
* @var array<string, array<string, bool|int|string|null>> [name => attributes]
*/
protected $fields = [];
/**
* All of the unique/primary keys in the table.
*
* @var array
*/
protected $keys = [];
/**
* All of the foreign keys in the table.
*
* @var array
*/
protected $foreignKeys = [];
/**
* The name of the table we're working with.
*
* @var string
*/
protected $tableName;
/**
* The name of the table, with database prefix
*
* @var string
*/
protected $prefixedTableName;
/**
* Database connection.
*
* @var Connection
*/
protected $db;
/**
* Handle to our forge.
*
* @var Forge
*/
protected $forge;
/**
* Table constructor.
*/
public function __construct(Connection $db, Forge $forge)
{
$this->db = $db;
$this->forge = $forge;
}
/**
* Reads an existing database table and
* collects all of the information needed to
* recreate this table.
*
* @return Table
*/
public function fromTable(string $table)
{
$this->prefixedTableName = $table;
$prefix = $this->db->DBPrefix;
if (! empty($prefix) && str_starts_with($table, $prefix)) {
$table = substr($table, strlen($prefix));
}
if (! $this->db->tableExists($this->prefixedTableName)) {
throw DataException::forTableNotFound($this->prefixedTableName);
}
$this->tableName = $table;
$this->fields = $this->formatFields($this->db->getFieldData($table));
$indexData = $this->db->getIndexData($table);
$this->keys = array_merge($this->keys, $this->formatKeys(is_array($indexData) ? $indexData : []));
// if primary key index exists twice then remove psuedo index name 'primary'.
$primaryIndexes = array_filter($this->keys, static fn ($index): bool => $index['type'] === 'primary');
if ($primaryIndexes !== [] && count($primaryIndexes) > 1 && array_key_exists('primary', $this->keys)) {
unset($this->keys['primary']);
}
$fkData = $this->db->getForeignKeyData($table);
$this->foreignKeys = is_array($fkData) ? $fkData : [];
return $this;
}
/**
* Called after `fromTable` and any actions, like `dropColumn`, etc,
* to finalize the action. It creates a temp table, creates the new
* table with modifications, and copies the data over to the new table.
* Resets the connection dataCache to be sure changes are collected.
*/
public function run(): bool
{
// 重建表前确保等待锁,避免立刻 database is locked
if (method_exists($this->db, 'connID') || isset($this->db->connID)) {
$conn = $this->db->connID ?? null;
if ($conn instanceof \SQLite3) {
$conn->busyTimeout(10000);
@$conn->exec('PRAGMA busy_timeout = 10000');
}
}
$this->db->query('PRAGMA foreign_keys = OFF');
$this->db->transStart();
$success = false;
try {
$this->forge->renameTable($this->tableName, "temp_{$this->tableName}");
$this->forge->reset();
$this->createTable();
$this->copyData();
$this->forge->dropTable("temp_{$this->tableName}");
$success = $this->db->transComplete();
} catch (\Throwable $e) {
// 必须回滚,否则同请求后续操作会一直 database is locked
$this->db->transRollback();
// 尽量恢复外键后再抛出
try {
$this->db->query('PRAGMA foreign_keys = ON');
} catch (\Throwable $ignore) {
}
$this->db->resetDataCache();
throw $e;
}
$this->db->query('PRAGMA foreign_keys = ON');
$this->db->resetDataCache();
return $success;
}
/**
* Drops columns from the table.
*
* @param list<string>|string $columns Column names to drop.
*
* @return Table
*/
public function dropColumn($columns)
{
if (is_string($columns)) {
$columns = explode(',', $columns);
}
foreach ($columns as $column) {
$column = trim($column);
if (isset($this->fields[$column])) {
unset($this->fields[$column]);
}
}
return $this;
}
/**
* Modifies a field, including changing data type, renaming, etc.
*
* @param list<array<string, bool|int|string|null>> $fieldsToModify
*
* @return Table
*/
public function modifyColumn(array $fieldsToModify)
{
foreach ($fieldsToModify as $field) {
$oldName = $field['name'];
unset($field['name']);
$this->fields[$oldName] = $field;
}
return $this;
}
/**
* Drops the primary key
*/
public function dropPrimaryKey(): Table
{
$primaryIndexes = array_filter($this->keys, static fn ($index): bool => strtolower($index['type']) === 'primary');
foreach (array_keys($primaryIndexes) as $key) {
unset($this->keys[$key]);
}
return $this;
}
/**
* Drops a foreign key from this table so that
* it won't be recreated in the future.
*
* @return Table
*/
public function dropForeignKey(string $foreignName)
{
if (empty($this->foreignKeys)) {
return $this;
}
if (isset($this->foreignKeys[$foreignName])) {
unset($this->foreignKeys[$foreignName]);
}
return $this;
}
/**
* Adds primary key
*/
public function addPrimaryKey(array $fields): Table
{
$primaryIndexes = array_filter($this->keys, static fn ($index): bool => strtolower($index['type']) === 'primary');
// if primary key already exists we can't add another one
if ($primaryIndexes !== []) {
return $this;
}
// add array to keys of fields
$pk = [
'fields' => $fields['fields'],
'type' => 'primary',
];
$this->keys['primary'] = $pk;
return $this;
}
/**
* Add a foreign key
*
* @return $this
*/
public function addForeignKey(array $foreignKeys)
{
$fk = [];
// convert to object
foreach ($foreignKeys as $row) {
$obj = new stdClass();
$obj->column_name = $row['field'];
$obj->foreign_table_name = $row['referenceTable'];
$obj->foreign_column_name = $row['referenceField'];
$obj->on_delete = $row['onDelete'];
$obj->on_update = $row['onUpdate'];
$fk[] = $obj;
}
$this->foreignKeys = array_merge($this->foreignKeys, $fk);
return $this;
}
/**
* Creates the new table based on our current fields.
*
* @return bool
*/
protected function createTable()
{
$this->dropIndexes();
$this->db->resetDataCache();
// Handle any modified columns.
$fields = [];
foreach ($this->fields as $name => $field) {
if (isset($field['new_name'])) {
$fields[$field['new_name']] = $field;
continue;
}
$fields[$name] = $field;
}
$this->forge->addField($fields);
$fieldNames = array_keys($fields);
$this->keys = array_filter(
$this->keys,
static fn ($index): bool => count(array_intersect($index['fields'], $fieldNames)) === count($index['fields']),
);
// Unique/Index keys
if (is_array($this->keys)) {
foreach ($this->keys as $keyName => $key) {
switch ($key['type']) {
case 'primary':
$this->forge->addPrimaryKey($key['fields']);
break;
case 'unique':
$this->forge->addUniqueKey($key['fields'], $keyName);
break;
case 'index':
$this->forge->addKey($key['fields'], false, false, $keyName);
break;
}
}
}
foreach ($this->foreignKeys as $foreignKey) {
$this->forge->addForeignKey(
$foreignKey->column_name,
trim($foreignKey->foreign_table_name, $this->db->DBPrefix),
$foreignKey->foreign_column_name,
);
}
return $this->forge->createTable($this->tableName);
}
/**
* Copies data from our old table to the new one,
* taking care map data correctly based on any columns
* that have been renamed.
*
* @return void
*/
protected function copyData()
{
$exFields = [];
$newFields = [];
foreach ($this->fields as $name => $details) {
$newFields[] = $details['new_name'] ?? $name;
$exFields[] = $name;
}
$exFields = implode(
', ',
array_map(fn ($item) => $this->db->protectIdentifiers($item), $exFields),
);
$newFields = implode(
', ',
array_map(fn ($item) => $this->db->protectIdentifiers($item), $newFields),
);
$this->db->query(
"INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}",
);
}
/**
* Converts fields retrieved from the database to
* the format needed for creating fields with Forge.
*
* @param array|bool $fields
*
* @return ($fields is array ? array : mixed)
*/
protected function formatFields($fields)
{
if (! is_array($fields)) {
return $fields;
}
$return = [];
foreach ($fields as $field) {
$return[$field->name] = [
'type' => $field->type,
'default' => $field->default,
'null' => $field->nullable,
];
if ($field->default === null) {
// `null` means that the default value is not defined.
unset($return[$field->name]['default']);
} elseif ($field->default === 'NULL') {
// 'NULL' means that the default value is NULL.
$return[$field->name]['default'] = null;
} else {
$default = trim($field->default, "'");
if ($this->isIntegerType($field->type)) {
$default = (int) $default;
} elseif ($this->isNumericType($field->type)) {
$default = (float) $default;
}
$return[$field->name]['default'] = $default;
}
if ($field->primary_key) {
$this->keys['primary'] = [
'fields' => [$field->name],
'type' => 'primary',
];
}
}
return $return;
}
/**
* Is INTEGER type?
*
* @param string $type SQLite data type (case-insensitive)
*
* @see https://www.sqlite.org/datatype3.html
*/
private function isIntegerType(string $type): bool
{
return str_contains(strtoupper($type), 'INT');
}
/**
* Is NUMERIC type?
*
* @param string $type SQLite data type (case-insensitive)
*
* @see https://www.sqlite.org/datatype3.html
*/
private function isNumericType(string $type): bool
{
return in_array(strtoupper($type), ['NUMERIC', 'DECIMAL'], true);
}
/**
* Converts keys retrieved from the database to
* the format needed to create later.
*
* @param array<string, stdClass> $keys
*
* @return array<string, array{fields: string, type: string}>
*/
protected function formatKeys($keys)
{
$return = [];
foreach ($keys as $name => $key) {
$return[strtolower($name)] = [
'fields' => $key->fields,
'type' => strtolower($key->type),
];
}
return $return;
}
/**
* Attempts to drop all indexes and constraints
* from the database for this table.
*
* @return void
*/
protected function dropIndexes()
{
if (! is_array($this->keys) || $this->keys === []) {
return;
}
foreach (array_keys($this->keys) as $name) {
if ($name === 'primary') {
continue;
}
$this->db->query("DROP INDEX IF EXISTS '{$name}'");
}
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\BaseUtils;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Utils for SQLite3
*/
class Utils extends BaseUtils
{
/**
* OPTIMIZE TABLE statement
*
* @var string
*/
protected $optimizeTable = 'REINDEX %s';
/**
* Platform dependent version of the backup function.
*
* @return never
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}
+195
View File
@@ -0,0 +1,195 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Exceptions\InvalidArgumentException;
use Config\Database;
use Faker\Factory;
use Faker\Generator;
/**
* Class Seeder
*/
class Seeder
{
/**
* The name of the database group to use.
*
* @var non-empty-string
*/
protected $DBGroup;
/**
* Where we can find the Seed files.
*
* @var string
*/
protected $seedPath;
/**
* An instance of the main Database configuration
*
* @var Database
*/
protected $config;
/**
* Database Connection instance
*
* @var BaseConnection
*/
protected $db;
/**
* Database Forge instance.
*
* @var Forge
*/
protected $forge;
/**
* If true, will not display CLI messages.
*
* @var bool
*/
protected $silent = false;
/**
* Faker Generator instance.
*
* @deprecated
*/
private static ?Generator $faker = null;
/**
* Seeder constructor.
*/
public function __construct(Database $config, ?BaseConnection $db = null)
{
$this->seedPath = $config->filesPath ?? APPPATH . 'Database/';
if ($this->seedPath === '') {
throw new InvalidArgumentException('Invalid filesPath set in the Config\Database.');
}
$this->seedPath = rtrim($this->seedPath, '\\/') . '/Seeds/';
if (! is_dir($this->seedPath)) {
throw new InvalidArgumentException('Unable to locate the seeds directory. Please check Config\Database::filesPath');
}
$this->config = &$config;
$db ??= Database::connect($this->DBGroup);
$this->db = $db;
$this->forge = Database::forge($this->DBGroup);
}
/**
* Gets the Faker Generator instance.
*
* @deprecated
*/
public static function faker(): ?Generator
{
if (! self::$faker instanceof Generator && class_exists(Factory::class)) {
self::$faker = Factory::create();
}
return self::$faker;
}
/**
* Loads the specified seeder and runs it.
*
* @return void
*
* @throws InvalidArgumentException
*/
public function call(string $class)
{
$class = trim($class);
if ($class === '') {
throw new InvalidArgumentException('No seeder was specified.');
}
if (! str_contains($class, '\\')) {
$path = $this->seedPath . str_replace('.php', '', $class) . '.php';
if (! is_file($path)) {
throw new InvalidArgumentException('The specified seeder is not a valid file: ' . $path);
}
// Assume the class has the correct namespace
// @codeCoverageIgnoreStart
$class = APP_NAMESPACE . '\Database\Seeds\\' . $class;
if (! class_exists($class, false)) {
require_once $path;
}
// @codeCoverageIgnoreEnd
}
/** @var Seeder $seeder */
$seeder = new $class($this->config);
$seeder->setSilent($this->silent)->run();
unset($seeder);
if (is_cli() && ! $this->silent) {
CLI::write("Seeded: {$class}", 'green');
}
}
/**
* Sets the location of the directory that seed files can be located in.
*
* @return $this
*/
public function setPath(string $path)
{
$this->seedPath = rtrim($path, '\\/') . '/';
return $this;
}
/**
* Sets the silent treatment.
*
* @return $this
*/
public function setSilent(bool $silent)
{
$this->silent = $silent;
return $this;
}
/**
* Run the database seeds. This is where the magic happens.
*
* Child classes must implement this method and take care
* of inserting their data here.
*
* @return void
*
* @codeCoverageIgnore
*/
public function run()
{
}
}
+135
View File
@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
/**
* Represents a table name in SQL.
*
* @interal
*
* @see \CodeIgniter\Database\TableNameTest
*/
class TableName
{
/**
* @param string $actualTable Actual table name
* @param string $logicalTable Logical table name (w/o DB prefix)
* @param string $schema Schema name
* @param string $database Database name
* @param string $alias Alias name
*/
protected function __construct(
private readonly string $actualTable,
private readonly string $logicalTable = '',
private readonly string $schema = '',
private readonly string $database = '',
private readonly string $alias = '',
) {
}
/**
* Creates a new instance.
*
* @param string $table Table name (w/o DB prefix)
* @param string $alias Alias name
*/
public static function create(string $dbPrefix, string $table, string $alias = ''): self
{
return new self(
$dbPrefix . $table,
$table,
'',
'',
$alias,
);
}
/**
* Creates a new instance from an actual table name.
*
* @param string $actualTable Actual table name with DB prefix
* @param string $alias Alias name
*/
public static function fromActualName(string $dbPrefix, string $actualTable, string $alias = ''): self
{
$prefix = $dbPrefix;
$logicalTable = '';
if (str_starts_with($actualTable, $prefix)) {
$logicalTable = substr($actualTable, strlen($prefix));
}
return new self(
$actualTable,
$logicalTable,
'',
$alias,
);
}
/**
* Creates a new instance from full name.
*
* @param string $table Table name (w/o DB prefix)
* @param string $schema Schema name
* @param string $database Database name
* @param string $alias Alias name
*/
public static function fromFullName(
string $dbPrefix,
string $table,
string $schema = '',
string $database = '',
string $alias = '',
): self {
return new self(
$dbPrefix . $table,
$table,
$schema,
$database,
$alias,
);
}
/**
* Returns the single segment table name w/o DB prefix.
*/
public function getTableName(): string
{
return $this->logicalTable;
}
/**
* Returns the actual single segment table name w/z DB prefix.
*/
public function getActualTableName(): string
{
return $this->actualTable;
}
public function getAlias(): string
{
return $this->alias;
}
public function getSchema(): string
{
return $this->schema;
}
public function getDatabase(): string
{
return $this->database;
}
}
@@ -0,0 +1,273 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Exceptions as ExceptionsConfig;
use Throwable;
/**
* Provides common functions for exception handlers,
* especially around displaying the output.
*/
abstract class BaseExceptionHandler
{
/**
* Config for debug exceptions.
*/
protected ExceptionsConfig $config;
/**
* Nesting level of the output buffering mechanism
*/
protected int $obLevel;
/**
* The path to the directory containing the
* cli and html error view directories.
*/
protected ?string $viewPath = null;
public function __construct(ExceptionsConfig $config)
{
$this->config = $config;
$this->obLevel = ob_get_level();
if ($this->viewPath === null) {
$this->viewPath = rtrim($this->config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
}
}
/**
* The main entry point into the handler.
*
* @return void
*/
abstract public function handle(
Throwable $exception,
RequestInterface $request,
ResponseInterface $response,
int $statusCode,
int $exitCode,
);
/**
* Gathers the variables that will be made available to the view.
*/
protected function collectVars(Throwable $exception, int $statusCode): array
{
// Get the first exception.
$firstException = $exception;
while ($prevException = $firstException->getPrevious()) {
$firstException = $prevException;
}
$trace = $firstException->getTrace();
if ($this->config->sensitiveDataInTrace !== []) {
$trace = $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace);
}
return [
'title' => $exception::class,
'type' => $exception::class,
'code' => $statusCode,
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $trace,
];
}
/**
* Mask sensitive data in the trace.
*/
protected function maskSensitiveData(array $trace, array $keysToMask, string $path = ''): array
{
foreach ($trace as $i => $line) {
$trace[$i]['args'] = $this->maskData($line['args'], $keysToMask);
}
return $trace;
}
/**
* @param array|object $args
*
* @return array|object
*/
private function maskData($args, array $keysToMask, string $path = '')
{
foreach ($keysToMask as $keyToMask) {
$explode = explode('/', $keyToMask);
$index = end($explode);
if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) {
if (is_array($args) && array_key_exists($index, $args)) {
$args[$index] = '******************';
} elseif (
is_object($args) && property_exists($args, $index)
&& isset($args->{$index}) && is_scalar($args->{$index})
) {
$args->{$index} = '******************';
}
}
}
if (is_array($args)) {
foreach ($args as $pathKey => $subarray) {
$args[$pathKey] = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
}
} elseif (is_object($args)) {
foreach ($args as $pathKey => $subarray) {
$args->{$pathKey} = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
}
}
return $args;
}
/**
* Describes memory usage in real-world units. Intended for use
* with memory_get_usage, etc.
*
* @used-by app/Views/errors/html/error_exception.php
*/
protected static function describeMemory(int $bytes): string
{
helper('number');
return number_to_size($bytes, 2);
}
/**
* Creates a syntax-highlighted version of a PHP file.
*
* @used-by app/Views/errors/html/error_exception.php
*
* @return bool|string
*/
protected static function highlightFile(string $file, int $lineNumber, int $lines = 15)
{
if ($file === '' || ! is_readable($file)) {
return false;
}
// Set our highlight colors:
if (function_exists('ini_set')) {
ini_set('highlight.comment', '#767a7e; font-style: italic');
ini_set('highlight.default', '#c7c7c7');
ini_set('highlight.html', '#06B');
ini_set('highlight.keyword', '#f1ce61;');
ini_set('highlight.string', '#869d6a');
}
try {
$source = file_get_contents($file);
} catch (Throwable) {
return false;
}
$source = str_replace(["\r\n", "\r"], "\n", $source);
$source = explode("\n", highlight_string($source, true));
if (PHP_VERSION_ID < 80300) {
$source = str_replace('<br />', "\n", $source[1]);
$source = explode("\n", str_replace("\r\n", "\n", $source));
} else {
// We have to remove these tags since we're preparing the result
// ourselves and these tags are added manually at the end.
$source = str_replace(['<pre><code>', '</code></pre>'], '', $source);
}
// Get just the part to show
$start = max($lineNumber - (int) round($lines / 2), 0);
// Get just the lines we need to display, while keeping line numbers...
$source = array_splice($source, $start, $lines, true);
// Used to format the line number in the source
$format = '% ' . strlen((string) ($start + $lines)) . 'd';
$out = '';
// Because the highlighting may have an uneven number
// of open and close span tags on one line, we need
// to ensure we can close them all to get the lines
// showing correctly.
$spans = 0;
foreach ($source as $n => $row) {
$spans += substr_count($row, '<span') - substr_count($row, '</span');
$row = str_replace(["\r", "\n"], ['', ''], $row);
if (($n + $start + 1) === $lineNumber) {
preg_match_all('#<[^>]+>#', $row, $tags);
$out .= sprintf(
"<span class='line highlight'><span class='number'>{$format}</span> %s\n</span>%s",
$n + $start + 1,
strip_tags($row),
implode('', $tags[0]),
);
} else {
$out .= sprintf('<span class="line"><span class="number">' . $format . '</span> %s', $n + $start + 1, $row) . "\n";
// We're closing only one span tag we added manually line before,
// so we have to increment $spans count to close this tag later.
$spans++;
}
}
if ($spans > 0) {
$out .= str_repeat('</span>', $spans);
}
return '<pre><code>' . $out . '</code></pre>';
}
/**
* Given an exception and status code will display the error to the client.
*
* @param string|null $viewFile
*/
protected function render(Throwable $exception, int $statusCode, $viewFile = null): void
{
if ($viewFile === null) {
echo 'The error view file was not specified. Cannot display error view.';
exit(1);
}
if (! is_file($viewFile)) {
echo 'The error view file "' . $viewFile . '" was not found. Cannot display error view.';
exit(1);
}
echo (function () use ($exception, $statusCode, $viewFile): string {
$vars = $this->collectVars($exception, $statusCode);
extract($vars, EXTR_SKIP);
// CLI error views output to STDERR/STDOUT, so ob_start() does not work.
ob_clean(); //屏蔽之前输出
ob_start();
include $viewFile;
return ob_get_clean();
})();
}
}
+107
View File
@@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug;
use CodeIgniter\Exceptions\PageNotFoundException;
use Throwable;
/**
* @see \CodeIgniter\Debug\ExceptionHandlerTest
*/
final class ExceptionHandler extends BaseExceptionHandler implements ExceptionHandlerInterface
{
/**
* ResponseTrait needs this.
*/
private $request = null;
/**
* ResponseTrait needs this.
*/
private $response = null;
/**
* Determines the correct way to display the error.
*/
public function handle(
$exception,
$request,
$response,
int $statusCode,
int $exitCode,
): void {
// ResponseTrait needs these properties.
// Determine possible directories of error views
$addPath = (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
$path = $this->viewPath . $addPath;
// Determine the views
$view = $this->determineView($exception, $path, $statusCode);
// Check if the view exists
$viewFile = null;
if (is_file($path . $view)) {
$viewFile = $path . $view;
}
// Displays the HTML or CLI error code.
$this->render($exception, $statusCode, $viewFile);
exit($exitCode);
}
/**
* Determines the view to display based on the exception thrown, HTTP status
* code, whether an HTTP or CLI request, etc.
*
* @return string The filename of the view file to use
*/
protected function determineView(
Throwable $exception,
string $templatePath,
int $statusCode = 500,
): string {
// Production environments should have a custom exception file.
$view = 'production.php';
if ($this->isDisplayErrorsEnabled()) {
// If display_errors is enabled, shows the error details.
$view = 'error_exception.php';
}
// 404 Errors
if ($exception instanceof PageNotFoundException) {
return 'error_404.php';
}
$templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;
// Allow for custom views based upon the status code
if (is_file($templatePath . 'error_' . $statusCode . '.php')) {
return 'error_' . $statusCode . '.php';
}
return $view;
}
private function isDisplayErrorsEnabled(): bool
{
return in_array(
strtolower(ini_get('display_errors')),
['1', 'true', 'on', 'yes'],
true,
);
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Throwable;
interface ExceptionHandlerInterface
{
/**
* Determines the correct way to display the error.
*/
public function handle(
Throwable $exception,
$request,
$response,
int $statusCode,
int $exitCode,
): void;
}
+776
View File
@@ -0,0 +1,776 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug;
use CodeIgniter\Exceptions\HasExitCodeInterface;
use CodeIgniter\Exceptions\HTTPExceptionInterface;
use CodeIgniter\Exceptions\PageNotFoundException;
use Config\Exceptions as ExceptionsConfig;
use ErrorException;
use Throwable;
/**
* Exceptions manager
*
* @see \CodeIgniter\Debug\ExceptionsTest
*/
class Exceptions
{
/**
* Nesting level of the output buffering mechanism
*
* @var int
*
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
*/
public $ob_level;
/**
* The path to the directory containing the
* cli and html error view directories.
*
* @var string
*
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
*/
protected $viewPath;
/**
* Config for debug exceptions.
*
* @var ExceptionsConfig
*/
protected $config;
/**
* The request.
*
* @var RequestInterface|null
*/
protected $request;
/**
* The outgoing response.
*
* @var ResponseInterface
*/
protected $response;
private ?Throwable $exceptionCaughtByExceptionHandler = null;
public function __construct(ExceptionsConfig $config)
{
// For backward compatibility
$this->ob_level = ob_get_level();
$this->viewPath = rtrim($config->errorViewPath, '\\/ ') . DIRECTORY_SEPARATOR;
$this->config = $config;
// workaround for upgraded users
// This causes "Deprecated: Creation of dynamic property" in PHP 8.2.
// @TODO remove this after dropping PHP 8.1 support.
if (! isset($this->config->sensitiveDataInTrace)) {
$this->config->sensitiveDataInTrace = [];
}
if (! isset($this->config->logDeprecations, $this->config->deprecationLogLevel)) {
$this->config->logDeprecations = false;
$this->config->deprecationLogLevel = 'warning';
}
}
/**
* Responsible for registering the error, exception and shutdown
* handling of our application.
*
* @codeCoverageIgnore
*
* @return void
*/
public function initialize()
{
set_exception_handler([$this, 'exceptionHandler']);
set_error_handler([$this, 'errorHandler']);
//set_exception_handler($this->exceptionHandler(...));
//set_error_handler($this->errorHandler(...));
register_shutdown_function([$this, 'shutdownHandler']);
}
/**
* 中文翻译输出的错误信息
*/
private function _cn_msg($message) {
if (!$message) {
return $message;
}
if (strpos($message, 'Unable to connect to the database') !== false) {
$message.= '<br>无法连接到数据库,检查数据库是否启动或者数据库配置文件不对,config/database.php';
} elseif (strpos($message, 'Unclosed \'{\'') !== false) {
$message.= '<br>循环体或者if语句,缺少结束语句,{ }没有成对出现';
} elseif (strpos($message, 'Cannot access offset of type string on string') !== false) {
$message.= '<br>此变量是字符串,不能使用数组的方式调用他,检查下代码语法';
} elseif (strpos($message, 'Call to undefined function') !== false) {
$message.= '<br>'.str_replace('Call to undefined function', '函数没有定义', $message);
} elseif (strpos($message, 'open_basedir restriction in effect') !== false) {
$message.= '<br>目录被限制读取,需要设置.users.ini文件中的目录白名单';
} elseif (strpos($message, 'Undefined constant') !== false) {
$message.= '<br>'.str_replace('Undefined constant', '变量或者常量没有定义', $message);
} elseif (preg_match("/Table '(.+)' doesn't exist/", $message, $mt)) {
$message.= '<br>数据库表'.$mt[1].'不存在,表丢失或者表没有创建成功';
} elseif (preg_match("/Unknown column '(.+)' in 'field list'/", $message, $mt)) {
$message.= '<br>表中没有字段'.$mt[1].',字段没有被创建';
} elseif (preg_match("/Access level to (.+) must be protected \(as in class (.+)\) or weaker/U", $message, $mt)) {
$message.= '<br>'.$mt[1].'在类'.$mt[2].'中已经被定义过更高级别的权限,请删除本文件的定义代码';
} elseif (preg_match("/Creation of dynamic property (.+) is deprecated/", $message, $mt)) {
$message.= '<br>动态属性被废除'.$mt[1].',请预先定义';
} elseif (preg_match("/Failed opening required '(.+)'/", $message, $mt)) {
$message.= '<br>文件'.$mt[1].'不存在,文件丢失或者文件没有创建成功';
} elseif (preg_match("/syntax error, unexpected token (.+)/", $message, $mt)) {
$message.= '<br>PHP语法错误 或者 模板标签语法错误,检查上下行代码是否写对';
} elseif (preg_match("/Cannot declare class (.+), because the name is already in use/", $message, $mt)) {
$message.= '<br>类名'.$mt[1].'重复,全文搜索下哪个地方被重复命名了';
} elseif (preg_match("/Controller method is not found: (.+)/", $message, $mt)) {
$message.= '<br>检查此文件中是否有'.$mt[1].'方法名:'.$this->_get_file();
} elseif (preg_match("/Controller or its method is not found:(.+)/", $message, $mt)) {
$message.= '<br>检查此文件是否存在:'.$this->_get_file().',检查地址是否正确,注意控制器文件首字母要大写';
} elseif (preg_match("/count\(\): Argument #1 \((.+)\) must be of type Countable\|array/", $message, $mt)) {
$message.= '<br>需要将count函数改为dr_count';
} elseif (IS_XRDEV) {
}
return $message;
}
/**
* 替换模板文件显示完整路径
*/
private function _rp_file($file) {
if (strpos((string)$file, '.cache.php') !== false && strpos((string)$file, '_DS_') !== false) {
$file = str_replace([WRITEPATH.'template/', '_DS_', '.cache.php'], ['', '/', ''], $file);
}
return $file;
}
/**
* 获取控制器地址
*/
private function _get_file() {
$file = APPPATH;
if ($file == FRAMEPATH) {
$file = CMSPATH.'Control';
} else {
$file.= 'Controllers';
}
if (IS_ADMIN) {
$file.= '/Admin';
} elseif (IS_MEMBER) {
$file.= '/Member';
} elseif (IS_API) {
$file.= '/Api';
}
return $file.'/'.ucfirst(\Phpcmf\Service::L('Router')->class).'.php';
}
/**
* Catches any uncaught errors and exceptions, including most Fatal errors
* (Yay PHP7!). Will log the error, display it if display_errors is on,
* and fire an event that allows custom actions to be taken at this point.
*
* @return void
*/
public function exceptionHandler(Throwable $exception)
{
$message = $this->_cn_msg($exception->getMessage());
// ajax 返回
if (IS_AJAX || IS_API) {
// 调试模式不屏蔽敏感信息
$file = $exception->getFile();
if (strpos($file, WRITEPATH.'template') !== false) {
$file = $this->_rp_file($file);
$message = '模板标签写法错误:'.$message;
$arr = \Phpcmf\Service::V()->get_view_files();
if ($arr) {
$one = current($arr);
$message.= ''.CI_DEBUG ? $one['path'] : basename($one['path']).'';
}
}
if (CI_DEBUG) {
$message.= '<br>错误文件:'.$file.''.$exception->getLine().'';
$message.= '<br>访问地址:'.\Phpcmf\Service::V()->now_php_url();
$trace = $exception->getTrace();
if ($trace) {
foreach ($trace as $t) {
if ($t['file'] && strpos((string)$t['file'], FRAMEPATH) === false) {
$message.= '<br>'.$t['function'].''.$t['file'].''.$t['line'].'';
}
}
}
} else {
$message = str_replace([FCPATH, WEBPATH], ['/', '/'], $message);
}
dr_exit_msg(0, $message);
}
$this->exceptionCaughtByExceptionHandler = null;
$this->exceptionCaughtByExceptionHandler = $exception;
[$statusCode, $exitCode] = $this->determineCodes($exception);
if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) {
$uri = '';
$routeInfo = '';
log_message('critical', $exception::class . ": {message}\n{routeInfo}\nin {exFile} on line {exLine}.\n{trace}", [
'message' => $exception->getMessage(),
'routeInfo' => $routeInfo,
'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file
'exLine' => $exception->getLine(), // {line} refers to THIS line
'trace' => self::renderBacktrace($exception->getTrace()),
]);
// Get the first exception.
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
log_message('critical', '[Caused by] ' . $prevException::class . ": {message}\nin {exFile} on line {exLine}.\n{trace}", [
'message' => $prevException->getMessage(),
'exFile' => clean_path($prevException->getFile()), // {file} refers to THIS file
'exLine' => $prevException->getLine(), // {line} refers to THIS line
'trace' => self::renderBacktrace($prevException->getTrace()),
]);
}
}
if (method_exists($this->config, 'handler')) {
// Use new ExceptionHandler
$handler = $this->config->handler($statusCode, $exception);
$handler->handle(
$exception,
null,
null,
$statusCode,
$exitCode,
);
return;
} else {
exit($exception->getMessage());
}
}
/**
* The callback to be registered to `set_error_handler()`.
*
* @return bool
*
* @throws ErrorException
*
* @codeCoverageIgnore
*/
public function errorHandler(int $severity, string $message, ?string $file = null, ?int $line = null)
{
if ($this->isDeprecationError($severity)) {
if ($this->isSessionSidDeprecationError($message, $file, $line)) {
return true;
}
if ($this->isImplicitNullableDeprecationError($message, $file, $line)) {
return true;
}
if (! $this->config->logDeprecations || (bool) env('CODEIGNITER_SCREAM_DEPRECATIONS')) {
throw new ErrorException($message, 0, $severity, $file, $line);
}
return $this->handleDeprecationError($message, $file, $line);
}
if ((error_reporting() & $severity) !== 0) {
throw new ErrorException($message, 0, $severity, $file, $line);
}
return false; // return false to propagate the error to PHP standard error handler
}
/**
* Handles session.sid_length and session.sid_bits_per_character deprecations
* in PHP 8.4.
*/
private function isSessionSidDeprecationError(string $message, ?string $file = null, ?int $line = null): bool
{
if (
PHP_VERSION_ID >= 80400
&& str_contains($message, 'session.sid_')
) {
log_message(
'wranting',
'[DEPRECATED] {message} in {errFile} on line {errLine}.',
[
'message' => $message,
'errFile' => clean_path($file ?? ''),
'errLine' => $line ?? 0,
],
);
return true;
}
return false;
}
/**
* Workaround to implicit nullable deprecation errors in PHP 8.4.
*
* "Implicitly marking parameter $xxx as nullable is deprecated,
* the explicit nullable type must be used instead"
*
* @TODO remove this before v4.6.0 release
*/
private function isImplicitNullableDeprecationError(string $message, ?string $file = null, ?int $line = null): bool
{
if (
PHP_VERSION_ID >= 80400
&& str_contains($message, 'the explicit nullable type must be used instead')
// Only Kint and Faker, which cause this error, are logged.
&& (str_starts_with($message, 'Kint\\') || str_starts_with($message, 'Faker\\'))
) {
log_message(
'wranting',
'[DEPRECATED] {message} in {errFile} on line {errLine}.',
[
'message' => $message,
'errFile' => clean_path($file ?? ''),
'errLine' => $line ?? 0,
],
);
return true;
}
return false;
}
/**
* Checks to see if any errors have happened during shutdown that
* need to be caught and handle them.
*
* @codeCoverageIgnore
*
* @return void
*/
public function shutdownHandler()
{
$error = error_get_last();
if ($error === null) {
return;
}
['type' => $type, 'message' => $message, 'file' => $file, 'line' => $line] = $error;
if ($this->exceptionCaughtByExceptionHandler instanceof Throwable) {
$message .= "\n【Previous Exception】\n"
. $this->exceptionCaughtByExceptionHandler::class . "\n"
. $this->exceptionCaughtByExceptionHandler->getMessage() . "\n"
. $this->exceptionCaughtByExceptionHandler->getTraceAsString();
}
if (in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE], true)) {
$this->exceptionHandler(new ErrorException($message, 0, $type, $file, $line));
}
}
/**
* Determines the view to display based on the exception thrown,
* whether an HTTP or CLI request, etc.
*
* @return string The path and filename of the view file to use
*
* @deprecated 4.4.0 No longer used. Moved to ExceptionHandler.
*/
protected function determineView(Throwable $exception, string $templatePath): string
{
// Production environments should have a custom exception file.
$view = 'production.php';
$templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;
if (
in_array(
strtolower(ini_get('display_errors')),
['1', 'true', 'on', 'yes'],
true,
)
) {
$view = 'error_exception.php';
}
// 404 Errors
if ($exception instanceof PageNotFoundException) {
return 'error_404.php';
}
// Allow for custom views based upon the status code
if (is_file($templatePath . 'error_' . $exception->getCode() . '.php')) {
return 'error_' . $exception->getCode() . '.php';
}
return $view;
}
/**
* Given an exception and status code will display the error to the client.
*
* @return void
*
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
*/
protected function render(Throwable $exception, int $statusCode)
{
// Determine possible directories of error views
$path = $this->viewPath;
$altPath = rtrim((new Paths())->viewDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR;
$path .= (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
$altPath .= (is_cli() ? 'cli' : 'html') . DIRECTORY_SEPARATOR;
// Determine the views
$view = $this->determineView($exception, $path);
$altView = $this->determineView($exception, $altPath);
// Check if the view exists
if (is_file($path . $view)) {
$viewFile = $path . $view;
} elseif (is_file($altPath . $altView)) {
$viewFile = $altPath . $altView;
}
if (! isset($viewFile)) {
echo 'The error view files were not found. Cannot render exception trace.';
exit(1);
}
echo (function () use ($exception, $statusCode, $viewFile): string {
$vars = $this->collectVars($exception, $statusCode);
extract($vars, EXTR_SKIP);
ob_start();
include $viewFile;
return ob_get_clean();
})();
}
/**
* Gathers the variables that will be made available to the view.
*
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
*/
protected function collectVars(Throwable $exception, int $statusCode): array
{
// Get the first exception.
$firstException = $exception;
while ($prevException = $firstException->getPrevious()) {
$firstException = $prevException;
}
$trace = $firstException->getTrace();
if ($this->config->sensitiveDataInTrace !== []) {
$trace = $this->maskSensitiveData($trace, $this->config->sensitiveDataInTrace);
}
return [
'title' => $exception::class,
'type' => $exception::class,
'code' => $statusCode,
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $trace,
];
}
/**
* Mask sensitive data in the trace.
*
* @param array $trace
*
* @return array
*
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
*/
protected function maskSensitiveData($trace, array $keysToMask, string $path = '')
{
foreach ($trace as $i => $line) {
$trace[$i]['args'] = $this->maskData($line['args'], $keysToMask);
}
return $trace;
}
/**
* @param array|object $args
*
* @return array|object
*
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
*/
private function maskData($args, array $keysToMask, string $path = '')
{
foreach ($keysToMask as $keyToMask) {
$explode = explode('/', $keyToMask);
$index = end($explode);
if (str_starts_with(strrev($path . '/' . $index), strrev($keyToMask))) {
if (is_array($args) && array_key_exists($index, $args)) {
$args[$index] = '******************';
} elseif (
is_object($args) && property_exists($args, $index)
&& isset($args->{$index}) && is_scalar($args->{$index})
) {
$args->{$index} = '******************';
}
}
}
if (is_array($args)) {
foreach ($args as $pathKey => $subarray) {
$args[$pathKey] = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
}
} elseif (is_object($args)) {
foreach ($args as $pathKey => $subarray) {
$args->{$pathKey} = $this->maskData($subarray, $keysToMask, $path . '/' . $pathKey);
}
}
return $args;
}
/**
* Determines the HTTP status code and the exit status code for this request.
*/
protected function determineCodes(Throwable $exception): array
{
$statusCode = 500;
$exitStatus = EXIT_ERROR;
if ($exception instanceof HTTPExceptionInterface) {
$statusCode = $exception->getCode();
}
if ($exception instanceof HasExitCodeInterface) {
$exitStatus = $exception->getExitCode();
}
return [$statusCode, $exitStatus];
}
private function isDeprecationError(int $error): bool
{
$deprecations = E_DEPRECATED | E_USER_DEPRECATED;
return ($error & $deprecations) !== 0;
}
/**
* @return true
*/
private function handleDeprecationError(string $message, ?string $file = null, ?int $line = null): bool
{
// Remove the trace of the error handler.
$trace = array_slice(debug_backtrace(), 2);
log_message(
$this->config->deprecationLogLevel,
"[DEPRECATED] {message} in {errFile} on line {errLine}.\n{trace}",
[
'message' => $message,
'errFile' => clean_path($file ?? ''),
'errLine' => $line ?? 0,
'trace' => self::renderBacktrace($trace),
],
);
return true;
}
// --------------------------------------------------------------------
// Display Methods
// --------------------------------------------------------------------
/**
* This makes nicer looking paths for the error output.
*
* @deprecated Use dedicated `clean_path()` function.
*/
public static function cleanPath(string $file): string
{
return CI_DEBUG ? $file : match (true) {
str_starts_with($file, APPPATH) => 'APPPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(APPPATH)),
str_starts_with($file, SYSTEMPATH) => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(SYSTEMPATH)),
str_starts_with($file, FCPATH) => 'FCPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(FCPATH)),
defined('VENDORPATH') && str_starts_with($file, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($file, strlen(VENDORPATH)),
default => $file,
};
}
/**
* Describes memory usage in real-world units. Intended for use
* with memory_get_usage, etc.
*
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
*/
public static function describeMemory(int $bytes): string
{
if ($bytes < 1024) {
return $bytes . 'B';
}
if ($bytes < 1_048_576) {
return round($bytes / 1024, 2) . 'KB';
}
return round($bytes / 1_048_576, 2) . 'MB';
}
/**
* Creates a syntax-highlighted version of a PHP file.
*
* @return bool|string
*
* @deprecated 4.4.0 No longer used. Moved to BaseExceptionHandler.
*/
public static function highlightFile(string $file, int $lineNumber, int $lines = 15)
{
if ($file === '' || ! is_readable($file)) {
return false;
}
// Set our highlight colors:
if (function_exists('ini_set')) {
ini_set('highlight.comment', '#767a7e; font-style: italic');
ini_set('highlight.default', '#c7c7c7');
ini_set('highlight.html', '#06B');
ini_set('highlight.keyword', '#f1ce61;');
ini_set('highlight.string', '#869d6a');
}
try {
$source = file_get_contents($file);
} catch (Throwable) {
return false;
}
$source = str_replace(["\r\n", "\r"], "\n", $source);
$source = explode("\n", highlight_string($source, true));
$source = str_replace('<br />', "\n", $source[1]);
$source = explode("\n", str_replace("\r\n", "\n", $source));
// Get just the part to show
$start = max($lineNumber - (int) round($lines / 2), 0);
// Get just the lines we need to display, while keeping line numbers...
$source = array_splice($source, $start, $lines, true);
// Used to format the line number in the source
$format = '% ' . strlen((string) ($start + $lines)) . 'd';
$out = '';
// Because the highlighting may have an uneven number
// of open and close span tags on one line, we need
// to ensure we can close them all to get the lines
// showing correctly.
$spans = 1;
foreach ($source as $n => $row) {
$spans += substr_count($row, '<span') - substr_count($row, '</span');
$row = str_replace(["\r", "\n"], ['', ''], $row);
if (($n + $start + 1) === $lineNumber) {
preg_match_all('#<[^>]+>#', $row, $tags);
$out .= sprintf(
"<span class='line highlight'><span class='number'>{$format}</span> %s\n</span>%s",
$n + $start + 1,
strip_tags($row),
implode('', $tags[0]),
);
} else {
$out .= sprintf('<span class="line"><span class="number">' . $format . '</span> %s', $n + $start + 1, $row) . "\n";
}
}
if ($spans > 0) {
$out .= str_repeat('</span>', $spans);
}
return '<pre><code>' . $out . '</code></pre>';
}
private static function renderBacktrace(array $backtrace): string
{
$backtraces = [];
foreach ($backtrace as $index => $trace) {
$frame = $trace + ['file' => '[internal function]', 'line' => '', 'class' => '', 'type' => '', 'args' => []];
if ($frame['file'] !== '[internal function]') {
$frame['file'] = sprintf('%s(%s)', $frame['file'], $frame['line']);
}
unset($frame['line']);
$idx = $index;
$idx = str_pad((string) ++$idx, 2, ' ', STR_PAD_LEFT);
$args = implode(', ', array_map(static fn ($value): string => match (true) {
is_object($value) => sprintf('Object(%s)', $value::class),
is_array($value) => $value !== [] ? '[...]' : '[]',
$value === null => 'null',
is_resource($value) => sprintf('resource (%s)', get_resource_type($value)),
default => var_export($value, true),
}, $frame['args']));
$backtraces[] = sprintf(
'%s %s: %s%s%s(%s)',
$idx,
clean_path($frame['file']),
$frame['class'],
$frame['type'],
$frame['function'],
$args,
);
}
return implode("\n", $backtraces);
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug;
use Closure;
/**
* Iterator for debugging.
*/
class Iterator
{
/**
* Stores the tests that we are to run.
*
* @var array
*/
protected $tests = [];
/**
* Stores the results of each of the tests.
*
* @var array
*/
protected $results = [];
/**
* Adds a test to run.
*
* Tests are simply closures that the user can define any sequence of
* things to happen during the test.
*
* @param Closure(): mixed $closure
*
* @return $this
*/
public function add(string $name, Closure $closure)
{
$name = strtolower($name);
$this->tests[$name] = $closure;
return $this;
}
/**
* Runs through all of the tests that have been added, recording
* time to execute the desired number of iterations, and the approximate
* memory usage used during those iterations.
*
* @return string|null
*/
public function run(int $iterations = 1000, bool $output = true)
{
foreach ($this->tests as $name => $test) {
// clear memory before start
gc_collect_cycles();
$start = microtime(true);
$startMem = $maxMemory = memory_get_usage(true);
for ($i = 0; $i < $iterations; $i++) {
$result = $test();
$maxMemory = max($maxMemory, memory_get_usage(true));
unset($result);
}
$this->results[$name] = [
'time' => microtime(true) - $start,
'memory' => $maxMemory - $startMem,
'n' => $iterations,
];
}
if ($output) {
return $this->getReport();
}
return null;
}
/**
* Get results.
*/
public function getReport(): string
{
if ($this->results === []) {
return 'No results to display.';
}
helper('number');
// Template
$tpl = '<table>
<thead>
<tr>
<td>Test</td>
<td>Time</td>
<td>Memory</td>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>';
$rows = '';
foreach ($this->results as $name => $result) {
$memory = number_to_size($result['memory'], 4);
$rows .= "<tr>
<td>{$name}</td>
<td>" . number_format($result['time'], 4) . "</td>
<td>{$memory}</td>
</tr>";
}
$tpl = str_replace('{rows}', $rows, $tpl);
return $tpl . '<br/>';
}
}
+151
View File
@@ -0,0 +1,151 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug;
use CodeIgniter\Exceptions\RuntimeException;
/**
* Class Timer
*
* Provides a simple way to measure the amount of time
* that elapses between two points.
*
* @see \CodeIgniter\Debug\TimerTest
*/
class Timer
{
/**
* List of all timers.
*
* @var array
*/
protected $timers = [];
/**
* Starts a timer running.
*
* Multiple calls can be made to this method so that several
* execution points can be measured.
*
* @param string $name The name of this timer.
* @param float|null $time Allows user to provide time.
*
* @return Timer
*/
public function start(string $name, ?float $time = null)
{
$this->timers[strtolower($name)] = [
'start' => ! empty($time) ? $time : microtime(true),
'end' => null,
];
return $this;
}
/**
* Stops a running timer.
*
* If the timer is not stopped before the timers() method is called,
* it will be automatically stopped at that point.
*
* @param string $name The name of this timer.
*
* @return Timer
*/
public function stop(string $name)
{
$name = strtolower($name);
if (empty($this->timers[$name])) {
throw new RuntimeException('Cannot stop timer: invalid name given.');
}
$this->timers[$name]['end'] = microtime(true);
return $this;
}
/**
* Returns the duration of a recorded timer.
*
* @param string $name The name of the timer.
* @param int $decimals Number of decimal places.
*
* @return float|null Returns null if timer does not exist by that name.
* Returns a float representing the number of
* seconds elapsed while that timer was running.
*/
public function getElapsedTime(string $name, int $decimals = 4)
{
$name = strtolower($name);
if (empty($this->timers[$name])) {
return null;
}
$timer = $this->timers[$name];
if (empty($timer['end'])) {
$timer['end'] = microtime(true);
}
return (float) number_format($timer['end'] - $timer['start'], $decimals, '.', '');
}
/**
* Returns the array of timers, with the duration pre-calculated for you.
*
* @param int $decimals Number of decimal places
*/
public function getTimers(int $decimals = 4): array
{
$timers = $this->timers;
foreach ($timers as &$timer) {
if (empty($timer['end'])) {
$timer['end'] = microtime(true);
}
$timer['duration'] = (float) number_format($timer['end'] - $timer['start'], $decimals);
}
return $timers;
}
/**
* Checks whether or not a timer with the specified name exists.
*/
public function has(string $name): bool
{
return array_key_exists(strtolower($name), $this->timers);
}
/**
* Executes callable and measures its time.
* Returns its return value if any.
*
* @param string $name The name of the timer
* @param callable(): mixed $callable callable to be executed
*
* @return mixed
*/
public function record(string $name, callable $callable)
{
$this->start($name);
$returnValue = $callable();
$this->stop($name);
return $returnValue;
}
}
+425
View File
@@ -0,0 +1,425 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug;
use CodeIgniter\Debug\Toolbar\Collectors\BaseCollector;
use CodeIgniter\Debug\Toolbar\Collectors\Config;
use CodeIgniter\I18n\Time;
use Config\Toolbar as ToolbarConfig;
/**
* Displays a toolbar with bits of stats to aid a developer in debugging.
*
* Inspiration: http://prophiler.fabfuel.de
*/
class Toolbar
{
/**
* Toolbar configuration settings.
*
* @var ToolbarConfig
*/
protected $config;
/**
* Collectors to be used and displayed.
*
* @var list<BaseCollector>
*/
protected $collectors = [];
public function __construct(ToolbarConfig $config)
{
$this->config = $config;
foreach ($config->collectors as $collector) {
$this->collectors[] = new $collector();
}
}
/**
* Returns all the data required by Debug Bar
*
* @param float $startTime App start time
* @param IncomingRequest $request
*
* @return string JSON encoded data
*/
public function run(float $startTime, float $totalTime): string
{
$data = [];
// Data items used within the view.
$data['url'] = dr_now_url();
$data['method'] = 1;
$data['isAJAX'] = 2;
$data['startTime'] = $startTime;
$data['totalTime'] = $totalTime * 1000;
$data['totalMemory'] = number_format(memory_get_peak_usage() / 1024 / 1024, 3);
$data['segmentDuration'] = $this->roundTo($data['totalTime'] / 7);
$data['segmentCount'] = (int) ceil($data['totalTime'] / $data['segmentDuration']);
$data['CI_VERSION'] = FRAME_VERSION;
$data['collectors'] = [];
foreach ($this->collectors as $collector) {
$data['collectors'][] = $collector->getAsArray();
}
$data['vars']['response'] = [
'headers' => [],
];
$data['config'] = Config::display();
return json_encode($data);
}
/**
* Called within the view to display the timeline itself.
*/
protected function renderTimeline(array $collectors, float $startTime, int $segmentCount, int $segmentDuration, array &$styles): string
{
$rows = $this->collectTimelineData($collectors);
$styleCount = 0;
// Use recursive render function
return $this->renderTimelineRecursive($rows, $startTime, $segmentCount, $segmentDuration, $styles, $styleCount);
}
/**
* Recursively renders timeline elements and their children.
*/
protected function renderTimelineRecursive(array $rows, float $startTime, int $segmentCount, int $segmentDuration, array &$styles, int &$styleCount, int $level = 0, bool $isChild = false): string
{
$displayTime = $segmentCount * $segmentDuration;
$output = '';
foreach ($rows as $row) {
$hasChildren = isset($row['children']) && ! empty($row['children']);
$isQuery = isset($row['query']) && ! empty($row['query']);
// Open controller timeline by default
$open = $row['name'] === 'Controller';
if ($hasChildren || $isQuery) {
$output .= '<tr class="timeline-parent' . ($open ? ' timeline-parent-open' : '') . '" id="timeline-' . $styleCount . '_parent" data-toggle="childrows" data-child="timeline-' . $styleCount . '">';
} else {
$output .= '<tr>';
}
$output .= '<td class="' . ($isChild ? 'debug-bar-width30' : '') . ' debug-bar-level-' . $level . '" >' . ($hasChildren || $isQuery ? '<nav></nav>' : '') . $row['name'] . '</td>';
$output .= '<td class="' . ($isChild ? 'debug-bar-width10' : '') . '">' . $row['component'] . '</td>';
$output .= '<td class="' . ($isChild ? 'debug-bar-width10 ' : '') . 'debug-bar-alignRight">' . number_format($row['duration'] * 1000, 2) . ' ms</td>';
$output .= "<td class='debug-bar-noverflow' colspan='{$segmentCount}'>";
$offset = ((((float) $row['start'] - $startTime) * 1000) / $displayTime) * 100;
$length = (((float) $row['duration'] * 1000) / $displayTime) * 100;
$styles['debug-bar-timeline-' . $styleCount] = "left: {$offset}%; width: {$length}%;";
$output .= "<span class='timer debug-bar-timeline-{$styleCount}' title='" . number_format($length, 2) . "%'></span>";
$output .= '</td>';
$output .= '</tr>';
$styleCount++;
// Add children if any
if ($hasChildren || $isQuery) {
$output .= '<tr class="child-row ' . ($open ? '' : ' debug-bar-ndisplay') . '" id="timeline-' . ($styleCount - 1) . '_children" >';
$output .= '<td colspan="' . ($segmentCount + 3) . '" class="child-container">';
$output .= '<table class="timeline">';
$output .= '<tbody>';
if ($isQuery) {
// Output query string if query
$output .= '<tr>';
$output .= '<td class="query-container debug-bar-level-' . ($level + 1) . '" >' . $row['query'] . '</td>';
$output .= '</tr>';
} else {
// Recursively render children
$output .= $this->renderTimelineRecursive($row['children'], $startTime, $segmentCount, $segmentDuration, $styles, $styleCount, $level + 1, true);
}
$output .= '</tbody>';
$output .= '</table>';
$output .= '</td>';
$output .= '</tr>';
}
}
return $output;
}
/**
* Returns a sorted array of timeline data arrays from the collectors.
*
* @param array $collectors
*/
protected function collectTimelineData($collectors): array
{
$data = [];
// Collect it
foreach ($collectors as $collector) {
if (! $collector['hasTimelineData']) {
continue;
}
$data = array_merge($data, $collector['timelineData']);
}
// Sort it
$sortArray = [
array_column($data, 'start'), SORT_NUMERIC, SORT_ASC,
array_column($data, 'duration'), SORT_NUMERIC, SORT_DESC,
&$data,
];
array_multisort(...$sortArray);
// Add end time to each element
array_walk($data, static function (&$row): void {
$row['end'] = $row['start'] + $row['duration'];
});
// Group it
$data = $this->structureTimelineData($data);
return $data;
}
/**
* Arranges the already sorted timeline data into a parent => child structure.
*/
protected function structureTimelineData(array $elements): array
{
// We define ourselves as the first element of the array
$element = array_shift($elements);
// If we have children behind us, collect and attach them to us
while ($elements !== [] && $elements[array_key_first($elements)]['end'] <= $element['end']) {
$element['children'][] = array_shift($elements);
}
// Make sure our children know whether they have children, too
if (isset($element['children'])) {
$element['children'] = $this->structureTimelineData($element['children']);
}
// If we have no younger siblings, we can return
if ($elements === []) {
return [$element];
}
// Make sure our younger siblings know their relatives, too
return array_merge([$element], $this->structureTimelineData($elements));
}
/**
* Returns an array of data from all of the modules
* that should be displayed in the 'Vars' tab.
*/
protected function collectVarData(): array
{
if (! ($this->config->collectVarData ?? true)) {
return [];
}
$data = [];
foreach ($this->collectors as $collector) {
if (! $collector->hasVarData()) {
continue;
}
$data = array_merge($data, $collector->getVarData());
}
return $data;
}
/**
* Rounds a number to the nearest incremental value.
*/
protected function roundTo(float $number, int $increments = 5): float
{
$increments = 1 / $increments;
return ceil($number * $increments) / $increments;
}
/**
* Prepare for debugging.
*
* @return void
*/
public function prepare($app)
{
if (IS_POST) {
return;
} elseif (IS_API) {
return;
} elseif (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
return;
}
//ajax请求
/**
* @var IncomingRequest|null $request
*/
if (CI_DEBUG && ! is_cli()) {
$stats = $app->getPerformanceStats();
$data = $this->run( $stats['startTime'], $stats['totalTime']);
helper('filesystem');
// Updated to microtime() so we can get history
$time = sprintf('%.6f', Time::now()->format('U.u'));
if (! is_dir(WRITEPATH . 'debugbar')) {
mkdir(WRITEPATH . 'debugbar', 0777);
}
write_file(WRITEPATH . 'debugbar/debugbar_' . $time . '.json', $data, 'w+');
$kintScript = file_get_contents($this->config->viewsPath.'script.js');
$kintScript = substr($kintScript, 0, strpos($kintScript, '</style>') + 8);
$kintScript = ($kintScript === '0') ? '' : $kintScript;
$script = PHP_EOL
. '<script id="debugbar_loader" '
. 'data-time="' . $time . '" '
. 'src="' . WEB_DIR . SELF . '?debugbar"></script>'
. '<script id="debugbar_dynamic_script"></script>'
. '<style id="debugbar_dynamic_style"></style>'
. $kintScript
. PHP_EOL;
echo $script;
return ;
if (str_contains((string) $response->getBody(), '<head>')) {
$response->setBody(
preg_replace(
'/<head>/',
'<head>' . $script,
$response->getBody(),
1
)
);
return;
}
$response->appendBody($script);
}
}
/**
* Inject debug toolbar into the response.
*
* @codeCoverageIgnore
*
* @return void
*/
public function respond()
{
$debugbar = isset($_GET['debugbar']) ? 1 : '';
$debugbar_time = isset($_GET['debugbar_time']) && $_GET['debugbar_time'] ? $_GET['debugbar_time'] : '';
// If the request contains '?debugbar then we're
// simply returning the loading script
if ($debugbar) {
header('Content-Type: application/javascript');
ob_start();
include $this->config->viewsPath . 'toolbarloader.js';
$output = ob_get_clean();
$output = str_replace('{url}', WEB_DIR.SELF, $output);
echo $output;
exit;
}
// Otherwise, if it includes ?debugbar_time, then
// we should return the entire debugbar.
if ($debugbar_time) {
helper('security');
$filename = sanitize_filename('debugbar_' . $debugbar_time);
$filename = WRITEPATH . 'debugbar/' . $filename . '.json';
if (is_file($filename)) {
// Show the toolbar if it exists
echo $this->format(file_get_contents($filename), 'html');
exit;
}
// Filename not found
http_response_code(404);
exit; // Exit here is needed to avoid loading the index page
}
}
/**
* Format output
*/
protected function format(string $data, string $format = 'html'): string
{
$data = json_decode($data, true);
$output = '';
$data['styles'] = [];
extract($data);
ob_start();
include $this->config->viewsPath . 'toolbar.tpl.php';
$output = ob_get_clean();
return $output;
}
public function render($name, $data)
{
extract($data);
ob_start();
include $this->config->viewsPath . $name;
$output = ob_get_clean();
return $output;
}
}
@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug\Toolbar\Collectors;
/**
* Base Toolbar collector
*/
class BaseCollector
{
/**
* Whether this collector has data that can
* be displayed in the Timeline.
*
* @var bool
*/
protected $hasTimeline = false;
/**
* Whether this collector needs to display
* content in a tab or not.
*
* @var bool
*/
protected $hasTabContent = false;
/**
* Whether this collector needs to display
* a label or not.
*
* @var bool
*/
protected $hasLabel = false;
/**
* Whether this collector has data that
* should be shown in the Vars tab.
*
* @var bool
*/
protected $hasVarData = false;
/**
* The 'title' of this Collector.
* Used to name things in the toolbar HTML.
*
* @var string
*/
protected $title = '';
/**
* Gets the Collector's title.
*/
public function getTitle(bool $safe = false): string
{
if ($safe) {
return str_replace(' ', '-', strtolower($this->title));
}
return $this->title;
}
/**
* Returns any information that should be shown next to the title.
*/
public function getTitleDetails(): string
{
return '';
}
/**
* Does this collector need it's own tab?
*/
public function hasTabContent(): bool
{
return (bool) $this->hasTabContent;
}
/**
* Does this collector have a label?
*/
public function hasLabel(): bool
{
return (bool) $this->hasLabel;
}
/**
* Does this collector have information for the timeline?
*/
public function hasTimelineData(): bool
{
return (bool) $this->hasTimeline;
}
/**
* Grabs the data for the timeline, properly formatted,
* or returns an empty array.
*/
public function timelineData(): array
{
if (! $this->hasTimeline) {
return [];
}
return $this->formatTimelineData();
}
/**
* Does this Collector have data that should be shown in the
* 'Vars' tab?
*/
public function hasVarData(): bool
{
return (bool) $this->hasVarData;
}
/**
* Gets a collection of data that should be shown in the 'Vars' tab.
* The format is an array of sections, each with their own array
* of key/value pairs:
*
* $data = [
* 'section 1' => [
* 'foo' => 'bar,
* 'bar' => 'baz'
* ],
* 'section 2' => [
* 'foo' => 'bar,
* 'bar' => 'baz'
* ],
* ];
*
* @return array|null
*/
public function getVarData()
{
return null;
}
/**
* Child classes should implement this to return the timeline data
* formatted for correct usage.
*
* Timeline data should be formatted into arrays that look like:
*
* [
* 'name' => 'Database::Query',
* 'component' => 'Database',
* 'start' => 10 // milliseconds
* 'duration' => 15 // milliseconds
* ]
*/
protected function formatTimelineData(): array
{
return [];
}
/**
* Returns the data of this collector to be formatted in the toolbar
*
* @return array|string
*/
public function display()
{
return [];
}
/**
* This makes nicer looking paths for the error output.
*
* @deprecated Use the dedicated `clean_path()` function.
*/
public function cleanPath(string $file): string
{
return clean_path($file);
}
/**
* Gets the "badge" value for the button.
*
* @return int|null
*/
public function getBadgeValue()
{
return null;
}
/**
* Does this collector have any data collected?
*
* If not, then the toolbar button won't get shown.
*/
public function isEmpty(): bool
{
return false;
}
/**
* Returns the HTML to display the icon. Should either
* be SVG, or a base-64 encoded.
*
* Recommended dimensions are 24px x 24px
*/
public function icon(): string
{
return '';
}
/**
* Return settings as an array.
*/
public function getAsArray(): array
{
return [
'title' => $this->getTitle(),
'titleSafe' => $this->getTitle(true),
'titleDetails' => $this->getTitleDetails(),
'display' => $this->display(),
'badgeValue' => $this->getBadgeValue(),
'isEmpty' => $this->isEmpty(),
'hasTabContent' => $this->hasTabContent(),
'hasLabel' => $this->hasLabel(),
'icon' => $this->icon(),
'hasTimelineData' => $this->hasTimelineData(),
'timelineData' => $this->timelineData(),
];
}
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug\Toolbar\Collectors;
/**
* Debug toolbar configuration
*/
class Config
{
/**
* Return toolbar config values as an array.
*/
public static function display(): array
{
return [
'ciVersion' => FRAME_VERSION,
'phpVersion' => PHP_VERSION,
'phpSAPI' => PHP_SAPI,
'environment' => ENVIRONMENT,
'baseURL' => dr_now_url(),
];
}
}
@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug\Toolbar\Collectors;
use CodeIgniter\Database\Query;
use CodeIgniter\I18n\Time;
use Config\Toolbar;
/**
* Collector for the Database tab of the Debug Toolbar.
*
* @see \CodeIgniter\Debug\Toolbar\Collectors\DatabaseTest
*/
class Database extends BaseCollector
{
/**
* Whether this collector has timeline data.
*
* @var bool
*/
protected $hasTimeline = true;
/**
* Whether this collector should display its own tab.
*
* @var bool
*/
protected $hasTabContent = true;
/**
* Whether this collector has data for the Vars tab.
*
* @var bool
*/
protected $hasVarData = false;
/**
* The name used to reference this collector in the toolbar.
*
* @var string
*/
protected $title = 'Database';
/**
* Array of database connections.
*
* @var array
*/
protected $connections;
/**
* The query instances that have been collected
* through the DBQuery Event.
*
* @var array
*/
protected static $queries = [];
/**
* Constructor
*/
public function __construct()
{
$this->getConnections();
}
/**
* The static method used during Events to collect
* data.
*
* @internal
*
* @return void
*/
public static function collect(Query $query)
{
$config = config(Toolbar::class);
// Provide default in case it's not set
$max = $config->maxQueries ?: 100;
if (count(static::$queries) < $max) {
$queryString = $query->getQuery();
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
if (! is_cli()) {
// when called in the browser, the first two trace arrays
// are from the DB event trigger, which are unneeded
$backtrace = array_slice($backtrace, 2);
}
static::$queries[] = [
'query' => $query,
'string' => $queryString,
'duplicate' => in_array($queryString, array_column(static::$queries, 'string', null), true),
'trace' => $backtrace,
];
}
}
/**
* Returns timeline data formatted for the toolbar.
*
* @return array The formatted data or an empty array.
*/
protected function formatTimelineData(): array
{
$data = [];
foreach ($this->connections as $alias => $connection) {
// Connection Time
$data[] = [
'name' => 'Connecting to Database: "' . $alias . '"',
'component' => 'Database',
'start' => $connection->getConnectStart(),
'duration' => $connection->getConnectDuration(),
];
}
foreach (static::$queries as $query) {
$data[] = [
'name' => 'Query',
'component' => 'Database',
'start' => $query['query']->getStartTime(true),
'duration' => $query['query']->getDuration(),
'query' => $query['query']->debugToolbarDisplay(),
];
}
return $data;
}
/**
* Returns the data of this collector to be formatted in the toolbar
*/
public function display(): array
{
$data = [];
$data['queries'] = array_map(static function (array $query): array {
$isDuplicate = $query['duplicate'] === true;
$firstNonSystemLine = '';
foreach ($query['trace'] as $index => &$line) {
// simplify file and line
if (isset($line['file'])) {
$line['file'] = clean_path($line['file']) . ':' . $line['line'];
unset($line['line']);
} else {
$line['file'] = '[internal function]';
}
// find the first trace line that does not originate from `system/`
if ($firstNonSystemLine === '' && ! str_contains($line['file'], 'SYSTEMPATH')) {
$firstNonSystemLine = $line['file'];
}
// simplify function call
if (isset($line['class'])) {
$line['function'] = $line['class'] . $line['type'] . $line['function'];
unset($line['class'], $line['type']);
}
if (strrpos($line['function'], '{closure}') === false) {
$line['function'] .= '()';
}
$line['function'] = str_repeat(chr(0xC2) . chr(0xA0), 8) . $line['function'];
// add index numbering padded with nonbreaking space
$indexPadded = str_pad(sprintf('%d', $index + 1), 3, ' ', STR_PAD_LEFT);
$indexPadded = preg_replace('/\s/', chr(0xC2) . chr(0xA0), $indexPadded);
$line['index'] = $indexPadded . str_repeat(chr(0xC2) . chr(0xA0), 4);
}
return [
'hover' => $isDuplicate ? 'This query was called more than once.' : '',
'class' => $isDuplicate ? 'duplicate' : '',
'duration' => ((float) $query['query']->getDuration(5) * 1000) . ' ms',
'sql' => $query['query']->debugToolbarDisplay(),
'trace' => $query['trace'],
'trace-file' => $firstNonSystemLine,
'qid' => md5($query['query'] . Time::now()->format('0.u00 U')),
];
}, static::$queries);
return $data;
}
/**
* Gets the "badge" value for the button.
*/
public function getBadgeValue(): int
{
return count(static::$queries);
}
/**
* Information to be displayed next to the title.
*
* @return string The number of queries (in parentheses) or an empty string.
*/
public function getTitleDetails(): string
{
$this->getConnections();
$queryCount = count(static::$queries);
$uniqueCount = count(array_filter(static::$queries, function($query) {
return $query['duplicate'] === false;
}));
$connectionCount = count($this->connections);
return sprintf(
'(%d total Quer%s, %d %s unique across %d Connection%s)',
$queryCount,
$queryCount > 1 ? 'ies' : 'y',
$uniqueCount,
$uniqueCount > 1 ? 'of them' : '',
$connectionCount,
$connectionCount > 1 ? 's' : ''
);
}
/**
* Does this collector have any data collected?
*/
public function isEmpty(): bool
{
return static::$queries === [];
}
/**
* Display the icon.
*
* Icon from https://icons8.com - 1em package
*/
public function icon(): string
{
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADMSURBVEhLY6A3YExLSwsA4nIycQDIDIhRWEBqamo/UNF/SjDQjF6ocZgAKPkRiFeEhoYyQ4WIBiA9QAuWAPEHqBAmgLqgHcolGQD1V4DMgHIxwbCxYD+QBqcKINseKo6eWrBioPrtQBq/BcgY5ht0cUIYbBg2AJKkRxCNWkDQgtFUNJwtABr+F6igE8olGQD114HMgHIxAVDyAhA/AlpSA8RYUwoeXAPVex5qHCbIyMgwBCkAuQJIY00huDBUz/mUlBQDqHGjgBjAwAAACexpph6oHSQAAAAASUVORK5CYII=';
}
/**
* Gets the connections from the database config
*/
private function getConnections(): void
{
$this->connections = \Config\Database::getConnections();
}
}
@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug\Toolbar\Collectors;
/**
* Files collector
*/
class Files extends BaseCollector
{
/**
* Whether this collector has data that can
* be displayed in the Timeline.
*
* @var bool
*/
protected $hasTimeline = false;
/**
* Whether this collector needs to display
* content in a tab or not.
*
* @var bool
*/
protected $hasTabContent = true;
/**
* The 'title' of this Collector.
* Used to name things in the toolbar HTML.
*
* @var string
*/
protected $title = 'Files';
/**
* Returns any information that should be shown next to the title.
*/
public function getTitleDetails(): string
{
return '( ' . count(get_included_files()) . ' )';
}
/**
* Returns the data of this collector to be formatted in the toolbar
*/
public function display(): array
{
$rawFiles = get_included_files();
$coreFiles = [];
$userFiles = [];
foreach ($rawFiles as $file) {
$path = clean_path($file);
if (str_contains($path, 'SYSTEMPATH')) {
$coreFiles[] = [
'path' => $path,
'name' => basename($file),
];
} else {
$userFiles[] = [
'path' => $path,
'name' => basename($file),
];
}
}
sort($userFiles);
sort($coreFiles);
return [
'coreFiles' => $coreFiles,
'userFiles' => $userFiles,
];
}
/**
* Displays the number of included files as a badge in the tab button.
*/
public function getBadgeValue(): int
{
return count(get_included_files());
}
/**
* Display the icon.
*
* Icon from https://icons8.com - 1em package
*/
public function icon(): string
{
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGBSURBVEhL7ZQ9S8NQGIVTBQUncfMfCO4uLgoKbuKQOWg+OkXERRE1IAXrIHbVDrqIDuLiJgj+gro7S3dnpfq88b1FMTE3VZx64HBzzvvZWxKnj15QCcPwCD5HUfSWR+JtzgmtsUcQBEva5IIm9SwSu+95CAWbUuy67qBa32ByZEDpIaZYZSZMjjQuPcQUq8yEyYEb8FSerYeQVGbAFzJkX1PyQWLhgCz0BxTCekC1Wp0hsa6yokzhed4oje6Iz6rlJEkyIKfUEFtITVtQdAibn5rMyaYsMS+a5wTv8qeXMhcU16QZbKgl3hbs+L4/pnpdc87MElZgq10p5DxGdq8I7xrvUWUKvG3NbSK7ubngYzdJwSsF7TiOh9VOgfcEz1UayNe3JUPM1RWC5GXYgTfc75B4NBmXJnAtTfpABX0iPvEd9ezALwkplCFXcr9styiNOKc1RRZpaPM9tcqBwlWzGY1qPL9wjqRBgF5BH6j8HWh2S7MHlX8PrmbK+k/8PzjOOzx1D3i1pKTTAAAAAElFTkSuQmCC';
}
}
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug\Toolbar\Collectors;
use ReflectionException;
use ReflectionFunction;
use ReflectionMethod;
/**
* Routes collector
*/
class Routes extends BaseCollector
{
/**
* Whether this collector has data that can
* be displayed in the Timeline.
*
* @var bool
*/
protected $hasTimeline = false;
/**
* Whether this collector needs to display
* content in a tab or not.
*
* @var bool
*/
protected $hasTabContent = true;
/**
* The 'title' of this Collector.
* Used to name things in the toolbar HTML.
*
* @var string
*/
protected $title = 'Routes';
/**
* Returns the data of this collector to be formatted in the toolbar
*
* @return array{
* matchedRoute: list<array{
* directory: string,
* controller: string,
* method: string,
* paramCount: int,
* truePCount: int,
* params: list<array{
* name: string,
* value: mixed
* }>
* }>,
* routes: list<array{
* method: string,
* route: string,
* handler: string
* }>
* }
*
* @throws ReflectionException
*/
public function display(): array
{
$file = APPPATH;
if ($file == FRAMEPATH) {
$file = CMSPATH.'Control';
} else {
$file.= 'Controllers';
}
if (IS_ADMIN) {
$file.= '/Admin';
} elseif (IS_MEMBER) {
$file.= '/Member';
} elseif (IS_API) {
$file.= '/Api';
}
return [
'matchedRoute' => [
'uri' => \Phpcmf\Service::L('Router')->uri(),
'url' => dr_now_url(),
'app' => APP_DIR ? APP_DIR : '/',
'controller' => \Phpcmf\Service::L('Router')->class,
'method' => \Phpcmf\Service::L('Router')->method,
'file' => $file.'/'.ucfirst(\Phpcmf\Service::L('Router')->class).'.php',
],
'get' => $_GET,
];
}
/**
* Returns a count of all the routes in the system.
*/
public function getBadgeValue(): int
{
return 1;
}
/**
* Display the icon.
*
* Icon from https://icons8.com - 1em package
*/
public function icon(): string
{
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFDSURBVEhL7ZRNSsNQFIUjVXSiOFEcuQIHDpzpxC0IGYeE/BEInbWlCHEDLsSiuANdhKDjgm6ggtSJ+l25ldrmmTwIgtgDh/t37r1J+16cX0dRFMtpmu5pWAkrvYjjOB7AETzStBFW+inxu3KUJMmhludQpoflS1zXban4LYqiO224h6VLTHr8Z+z8EpIHFF9gG78nDVmW7UgTHKjsCyY98QP+pcq+g8Ku2s8G8X3f3/I8b038WZTp+bO38zxfFd+I6YY6sNUvFlSDk9CRhiAI1jX1I9Cfw7GG1UB8LAuwbU0ZwQnbRDeEN5qqBxZMLtE1ti9LtbREnMIuOXnyIf5rGIb7Wq8HmlZgwYBH7ORTcKH5E4mpjeGt9fBZcHE2GCQ3Vt7oTNPNg+FXLHnSsHkw/FR+Gg2bB8Ptzrst/v6C/wrH+QB+duli6MYJdQAAAABJRU5ErkJggg==';
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Debug\Toolbar\Collectors;
/**
* Views collector
*/
class Views extends BaseCollector
{
/**
* Whether this collector has data that can
* be displayed in the Timeline.
*
* @var bool
*/
protected $hasTimeline = true;
/**
* Whether this collector needs to display
* content in a tab or not.
*
* @var bool
*/
protected $hasTabContent = false;
/**
* Whether this collector needs to display
* a label or not.
*
* @var bool
*/
protected $hasLabel = true;
/**
* Whether this collector has data that
* should be shown in the Vars tab.
*
* @var bool
*/
protected $hasVarData = true;
/**
* The 'title' of this Collector.
* Used to name things in the toolbar HTML.
*
* @var string
*/
protected $title = 'Views';
/**
* Instance of the shared Renderer service
*
* @var RendererInterface|null
*/
protected $viewer;
/**
* Views counter
*
* @var array
*/
protected $views = [];
/**
* 把CI模板类改成PHPCMF模板类用于debug.
*/
public function __construct()
{
$this->viewer = \Phpcmf\Service::V();
$this->hasTabContent = true;
}
/**
* Returns the data of this collector to be formatted in the toolbar
*
* @return array
*/
public function display(): array
{
$vars = [];
$tpl_var = $this->viewer->get_data();
if ($tpl_var) {
foreach ($tpl_var as $key => $value) {
if (in_array($key, ['member', 'admin'])) {
continue;
}
$vars[] = [
'name' => $key,
'value' => var_export($value, true),
];
}
}
return [
'vars' => $vars,
'tips' => $this->viewer->get_load_tips(),
'times' => [['tpl' => $this->viewer->get_view_time()]],
'files' => $this->viewer->get_view_files(),
];
}
/**
* Returns any information that should be shown next to the title.
*
* @return string
*/
public function getBadgeValue(): int
{
return dr_count($this->viewer->get_view_files());
}
public function setData($data = [], $context = null)
{
if (! empty($context))
{
foreach ($data as $key => &$value)
{
if (is_array($value))
{
foreach ($value as &$obj)
{
$obj = $this->objectToArray($obj);
}
}
else
{
$value = $this->objectToArray($value);
}
$this->dataContexts[$key] = $context;
}
}
$this->tempData = $this->tempData ?? $this->data;
$this->tempData = array_merge($this->tempData, $data);
return $this;
}
private function initViewer(): void
{
$this->viewer = \Phpcmf\Service::V();
}
/**
* Child classes should implement this to return the timeline data
* formatted for correct usage.
*/
protected function formatTimelineData(): array
{
$this->initViewer();
$data = [];
$rows = $this->viewer->getPerformanceData();
foreach ($rows as $info) {
$data[] = [
'name' => 'View: ' . $info['view'],
'component' => 'Views',
'start' => $info['start'],
'duration' => $info['end'] - $info['start'],
];
}
return $data;
}
/**
* Gets a collection of data that should be shown in the 'Vars' tab.
* The format is an array of sections, each with their own array
* of key/value pairs:
*
* $data = [
* 'section 1' => [
* 'foo' => 'bar,
* 'bar' => 'baz'
* ],
* 'section 2' => [
* 'foo' => 'bar,
* 'bar' => 'baz'
* ],
* ];
*/
public function getVarData(): array
{
$this->initViewer();
return [
'View Data' => $this->viewer->getData(),
];
}
/**
* Display the icon.
*
* Icon from https://icons8.com - 1em package
*/
public function icon(): string
{
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADeSURBVEhL7ZSxDcIwEEWNYA0YgGmgyAaJLTcUaaBzQQEVjMEabBQxAdw53zTHiThEovGTfnE/9rsoRUxhKLOmaa6Uh7X2+UvguLCzVxN1XW9x4EYHzik033Hp3X0LO+DaQG8MDQcuq6qao4qkHuMgQggLvkPLjqh00ZgFDBacMJYFkuwFlH1mshdkZ5JPJERA9JpI6xNCBESvibQ+IURA9JpI6xNCBESvibQ+IURA9DTsuHTOrVFFxixgB/eUFlU8uKJ0eDBFOu/9EvoeKnlJS2/08Tc8NOwQ8sIfMeYFjqKDjdU2sp4AAAAASUVORK5CYII=';
}
}
@@ -0,0 +1,27 @@
<table>
<thead>
<tr>
<th class="debug-bar-width6r">Time</th>
<th>Query String</th>
</tr>
</thead>
<tbody>
<?php if ($queries) { foreach ($queries as $t) { ?>
<tr class="<?php echo $t['class'];?>" title="<?php echo $t['hover'];?>" data-toggle="<?php echo $t['qid'];?>-trace">
<td class="narrow"><?php echo $t['duration'];?></td>
<td><?php echo $t['sql'];?></td>
<td class="debug-bar-alignRight"><strong><?php echo $t['trace-file'];?></strong></td>
</tr>
<tr class="muted debug-bar-ndisplay" id="<?php echo $t['qid'];?>-trace">
<td></td>
<td colspan="2">
<?php foreach ($t['trace'] as $tt) { ?>
<?php echo $tt['index'];?>
<strong><?php echo $tt['file'];?></strong><br/>
<?php echo $tt['function'];?><br/><br/>
<?php } ?>
</td>
</tr>
<?php } } ?>
</tbody>
</table>
@@ -0,0 +1,17 @@
<table>
<tbody>
<?php foreach ($userFiles as $t) { ?>
<tr>
<td> <?php echo $t['name'];?></td>
<td> <?php echo $t['path'];?></td>
</tr>
<?php } ?>
<?php foreach ($coreFiles as $t) { ?>
<tr class="muted">
<td class="debug-bar-width20e"> <?php echo $t['name'];?> </td>
<td> <?php echo $t['path'];?></td>
</tr>
<?php } ?>
</tbody>
</table>
@@ -0,0 +1,32 @@
<h3>Matched Route</h3>
<table>
<tbody>
<?php if ($matchedRoute) { foreach ($matchedRoute as $key => $tt) { ?>
<tr>
<td width=200><?php echo $key;?></td>
<td><?php echo $tt;?></td>
</tr>
<?php } } ?>
</tbody>
</table>
<h3>GET</h3>
<table>
<thead>
<tr>
<th>key</th>
<th>value</th>
</tr>
</thead>
<tbody>
<?php if ($get) { foreach ($get as $key => $tt) { ?>
<tr>
<td width=200><?php echo $key;?></td>
<td><?php echo $tt;?></td>
</tr>
<?php } } ?>
</tbody>
</table>
@@ -0,0 +1,58 @@
<?php if ($times) { foreach ($times as $t) { ?>
<h3><?php echo $t['tpl'];?>ms</h3>
<?php } } ?>
<br>
<h2>模板文件</h2>
<table>
<thead>
<tr>
<th width=200>模板</th>
<th>路径</th>
</tr>
</thead>
<tbody>
<?php if ($files) { foreach ($files as $t) { ?>
<tr>
<td><?php echo $t['name'];?></td>
<td><?php echo $t['path'];?></td>
</tr>
<?php } } ?>
</tbody>
</table>
<br>
<h2>引用提示</h2>
<table>
<thead>
<tr>
<th width=200>模板</th>
<th>提示</th>
</tr>
</thead>
<tbody>
<?php if ($tips) { foreach ($tips as $t) { ?>
<tr>
<td><?php echo $t['name'];?></td>
<td><?php echo $t['tips'];?></td>
</tr>
<?php } } ?>
</tbody>
</table>
<br>
<h2>模板变量</h2>
<table>
<tbody>
<?php if ($vars) { foreach ($vars as $t) { ?>
<tr>
<td width=200><?php echo $t['name'];?></td>
<td><pre><?php echo $t['value'];?></pre></td>
</tr>
<?php } } ?>
</tbody>
</table>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,887 @@
/**
* This file is part of the CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#debug-icon {
bottom: 0;
position: fixed;
right: 0;
z-index: 10000;
height: 36px;
width: 36px;
margin: 0;
padding: 0;
clear: both;
text-align: center;
cursor: pointer;
}
#debug-icon a svg {
margin: 8px;
max-width: 20px;
max-height: 20px;
}
#debug-icon.fixed-top {
bottom: auto;
top: 0;
}
#debug-icon .debug-bar-ndisplay {
display: none;
}
.debug-bar-vars {
cursor: pointer;
}
#debug-bar {
bottom: 0;
left: 0;
position: fixed;
right: 0;
z-index: 10000;
height: 36px;
line-height: 36px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 16px;
font-weight: 400;
}
#debug-bar h1 {
display: flex;
font-weight: normal;
margin: 0 0 0 auto;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
}
#debug-bar h1 svg {
width: 16px;
margin-right: 5px;
}
#debug-bar h2 {
font-weight: bold;
font-size: 16px;
margin: 0;
padding: 5px 0 10px 0;
}
#debug-bar h2 span {
font-size: 13px;
}
#debug-bar h3 {
font-size: 12px;
font-weight: 200;
margin: 0 0 0 10px;
padding: 0;
text-transform: uppercase;
}
#debug-bar p {
font-size: 12px;
margin: 0 0 0 15px;
padding: 0;
}
#debug-bar a {
text-decoration: none;
}
#debug-bar a:hover {
text-decoration: underline;
}
#debug-bar button {
border: 1px solid;
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
cursor: pointer;
line-height: 15px;
}
#debug-bar button:hover {
text-decoration: underline;
}
#debug-bar table {
border-collapse: collapse;
font-size: 14px;
line-height: normal;
margin: 5px 10px 15px 10px;
width: calc(100% - 10px);
}
#debug-bar table strong {
font-weight: 500;
}
#debug-bar table th {
display: table-cell;
font-weight: 600;
padding-bottom: 0.7em;
text-align: left;
}
#debug-bar table tr {
border: none;
}
#debug-bar table td {
border: none;
display: table-cell;
margin: 0;
text-align: left;
}
#debug-bar table td:first-child {
max-width: 20%;
}
#debug-bar table td:first-child.narrow {
width: 7em;
}
#debug-bar td[data-debugbar-route] form {
display: none;
}
#debug-bar td[data-debugbar-route]:hover form {
display: block;
}
#debug-bar td[data-debugbar-route]:hover > div {
display: none;
}
#debug-bar td[data-debugbar-route] input[type=text] {
padding: 2px;
}
#debug-bar .toolbar {
display: flex;
overflow: hidden;
overflow-y: auto;
padding: 0 12px 0 12px;
white-space: nowrap;
z-index: 10000;
}
#debug-bar .toolbar .rotate {
animation: toolbar-rotate 9s linear infinite;
}
@keyframes toolbar-rotate {
to {
transform: rotate(360deg);
}
}
#debug-bar.fixed-top {
bottom: auto;
top: 0;
}
#debug-bar.fixed-top .tab {
bottom: auto;
top: 36px;
}
#debug-bar #toolbar-position,
#debug-bar #toolbar-theme {
padding: 0 6px;
display: inline-flex;
vertical-align: top;
cursor: pointer;
}
#debug-bar #toolbar-position:hover,
#debug-bar #toolbar-theme:hover {
text-decoration: none;
}
#debug-bar #debug-bar-link {
display: flex;
padding: 6px;
cursor: pointer;
}
#debug-bar .ci-label {
display: inline-flex;
font-size: 14px;
}
#debug-bar .ci-label:hover {
cursor: pointer;
}
#debug-bar .ci-label a {
color: inherit;
display: flex;
letter-spacing: normal;
padding: 0 10px;
text-decoration: none;
align-items: center;
}
#debug-bar .ci-label img {
margin: 6px 3px 6px 0;
width: 16px !important;
}
#debug-bar .ci-label .badge {
border-radius: 12px;
-moz-border-radius: 12px;
-webkit-border-radius: 12px;
display: inline-block;
font-size: 75%;
font-weight: bold;
line-height: 12px;
margin-left: 5px;
padding: 2px 5px;
text-align: center;
vertical-align: baseline;
white-space: nowrap;
}
#debug-bar .tab {
height: fit-content;
text-align: left;
bottom: 35px;
display: none;
left: 0;
max-height: 62%;
overflow: hidden;
overflow-y: auto;
padding: 1em 2em;
position: fixed;
right: 0;
z-index: 9999;
}
#debug-bar .timeline {
position: static;
display: table;
margin-left: 0;
width: 100%;
}
#debug-bar .timeline th {
border-left: 1px solid;
font-size: 12px;
font-weight: 200;
padding: 5px 5px 10px 5px;
position: relative;
text-align: left;
}
#debug-bar .timeline th:first-child {
border-left: 0;
}
#debug-bar .timeline td {
border-left: 1px solid;
padding: 5px;
position: relative;
}
#debug-bar .timeline td:first-child {
border-left: 0;
max-width: none;
}
#debug-bar .timeline td.child-container {
padding: 0px;
}
#debug-bar .timeline td.child-container .timeline {
margin: 0px;
}
#debug-bar .timeline td.child-container .timeline td:first-child:not(.child-container) {
padding-left: calc(5px + 10px * var(--level));
}
#debug-bar .timeline .timer {
border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;
display: inline-block;
padding: 5px;
position: absolute;
top: 30%;
}
#debug-bar .timeline .timeline-parent {
cursor: pointer;
}
#debug-bar .timeline .timeline-parent td:first-child nav {
background: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMCAxNTAiPjxwYXRoIGQ9Ik02IDdoMThsLTkgMTV6bTAgMzBoMThsLTkgMTV6bTAgNDVoMThsLTktMTV6bTAgMzBoMThsLTktMTV6bTAgMTJsMTggMThtLTE4IDBsMTgtMTgiIGZpbGw9IiM1NTUiLz48cGF0aCBkPSJNNiAxMjZsMTggMThtLTE4IDBsMTgtMTgiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlPSIjNTU1Ii8+PC9zdmc+") no-repeat scroll 0 0/15px 75px transparent;
background-position: 0 25%;
display: inline-block;
height: 15px;
width: 15px;
margin-right: 3px;
vertical-align: middle;
}
#debug-bar .timeline .timeline-parent-open {
background-color: #DFDFDF;
}
#debug-bar .timeline .timeline-parent-open td:first-child nav {
background-position: 0 75%;
}
#debug-bar .timeline .child-row:hover {
background: transparent;
}
#debug-bar .route-params,
#debug-bar .route-params-item {
vertical-align: top;
}
#debug-bar .route-params td:first-child,
#debug-bar .route-params-item td:first-child {
font-style: italic;
padding-left: 1em;
text-align: right;
}
#debug-bar > .debug-bar-dblock {
display: block;
}
.debug-view.show-view {
border: 1px solid;
margin: 4px;
}
.debug-view-path {
font-family: monospace;
font-size: 12px;
letter-spacing: normal;
min-height: 16px;
padding: 2px;
text-align: left;
}
.show-view .debug-view-path {
display: block !important;
}
@media screen and (max-width: 1024px) {
#debug-bar .ci-label img {
margin: unset;
}
.hide-sm {
display: none !important;
}
}
@media screen and (max-width: 768px) {
#debug-bar table {
display: block;
overflow-x: auto;
font-size: 12px;
margin: 5px 5px 10px 5px;
}
#debug-bar table td,
#debug-bar table th {
padding: 4px 6px;
}
#debug-bar .timeline {
display: block;
white-space: nowrap;
font-size: 12px;
}
#debug-bar .toolbar {
overflow-x: auto;
}
}
#debug-icon {
background-color: #FFFFFF;
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF;
}
#debug-icon a:active,
#debug-icon a:link,
#debug-icon a:visited {
color: #DD8615;
}
#debug-bar {
background-color: #FFFFFF;
color: #434343;
}
#debug-bar h1,
#debug-bar h2,
#debug-bar h3,
#debug-bar p,
#debug-bar a,
#debug-bar button,
#debug-bar table,
#debug-bar thead,
#debug-bar tr,
#debug-bar td,
#debug-bar button,
#debug-bar .toolbar {
background-color: transparent;
color: #434343;
}
#debug-bar button {
background-color: #FFFFFF;
}
#debug-bar table strong {
color: #DD8615;
}
#debug-bar table tbody tr:hover {
background-color: #DFDFDF;
}
#debug-bar table tbody tr.current {
background-color: #FDC894;
}
#debug-bar table tbody tr.current:hover td {
background-color: #DD4814;
color: #FFFFFF;
}
#debug-bar .toolbar {
background-color: #FFFFFF;
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF;
}
#debug-bar .toolbar img {
filter: brightness(0) invert(0.4);
}
#debug-bar.fixed-top .toolbar {
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF;
}
#debug-bar.fixed-top .tab {
box-shadow: 0 1px 4px #DFDFDF;
-moz-box-shadow: 0 1px 4px #DFDFDF;
-webkit-box-shadow: 0 1px 4px #DFDFDF;
}
#debug-bar .muted {
color: #434343;
}
#debug-bar .muted td {
color: #DFDFDF;
}
#debug-bar .muted:hover td {
color: #434343;
}
#debug-bar #toolbar-position,
#debug-bar #toolbar-theme {
filter: brightness(0) invert(0.6);
}
#debug-bar .ci-label.active {
background-color: #DFDFDF;
}
#debug-bar .ci-label:hover {
background-color: #DFDFDF;
}
#debug-bar .ci-label .badge {
background-color: #DD4814;
color: #FFFFFF;
}
#debug-bar .tab {
background-color: #FFFFFF;
box-shadow: 0 -1px 4px #DFDFDF;
-moz-box-shadow: 0 -1px 4px #DFDFDF;
-webkit-box-shadow: 0 -1px 4px #DFDFDF;
}
#debug-bar .timeline th,
#debug-bar .timeline td {
border-color: #DFDFDF;
}
#debug-bar .timeline .timer {
background-color: #DD8615;
}
.debug-view.show-view {
border-color: #DD8615;
}
.debug-view-path {
background-color: #FDC894;
color: #434343;
}
@media (prefers-color-scheme: dark) {
#debug-icon {
background-color: #252525;
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF;
}
#debug-icon a:active,
#debug-icon a:link,
#debug-icon a:visited {
color: #DD8615;
}
#debug-bar {
background-color: #252525;
color: #DFDFDF;
}
#debug-bar h1,
#debug-bar h2,
#debug-bar h3,
#debug-bar p,
#debug-bar a,
#debug-bar button,
#debug-bar table,
#debug-bar thead,
#debug-bar tr,
#debug-bar td,
#debug-bar button,
#debug-bar .toolbar {
background-color: transparent;
color: #DFDFDF;
}
#debug-bar button {
background-color: #252525;
}
#debug-bar table strong {
color: #DD8615;
}
#debug-bar table tbody tr:hover {
background-color: #434343;
}
#debug-bar table tbody tr.current {
background-color: #FDC894;
}
#debug-bar table tbody tr.current td {
color: #252525;
}
#debug-bar table tbody tr.current:hover td {
background-color: #DD4814;
color: #FFFFFF;
}
#debug-bar .toolbar {
background-color: #434343;
box-shadow: 0 0 4px #434343;
-moz-box-shadow: 0 0 4px #434343;
-webkit-box-shadow: 0 0 4px #434343;
}
#debug-bar .toolbar img {
filter: brightness(0) invert(1);
}
#debug-bar.fixed-top .toolbar {
box-shadow: 0 0 4px #434343;
-moz-box-shadow: 0 0 4px #434343;
-webkit-box-shadow: 0 0 4px #434343;
}
#debug-bar.fixed-top .tab {
box-shadow: 0 1px 4px #434343;
-moz-box-shadow: 0 1px 4px #434343;
-webkit-box-shadow: 0 1px 4px #434343;
}
#debug-bar .muted {
color: #DFDFDF;
}
#debug-bar .muted td {
color: #434343;
}
#debug-bar .muted:hover td {
color: #DFDFDF;
}
#debug-bar #toolbar-position,
#debug-bar #toolbar-theme {
filter: brightness(0) invert(0.6);
}
#debug-bar .ci-label.active {
background-color: #252525;
}
#debug-bar .ci-label:hover {
background-color: #252525;
}
#debug-bar .ci-label .badge {
background-color: #DD4814;
color: #FFFFFF;
}
#debug-bar .tab {
background-color: #252525;
box-shadow: 0 -1px 4px #434343;
-moz-box-shadow: 0 -1px 4px #434343;
-webkit-box-shadow: 0 -1px 4px #434343;
}
#debug-bar .timeline th,
#debug-bar .timeline td {
border-color: #434343;
}
#debug-bar .timeline .timer {
background-color: #DD8615;
}
.debug-view.show-view {
border-color: #DD8615;
}
.debug-view-path {
background-color: #FDC894;
color: #434343;
}
}
#toolbarContainer.dark #debug-icon {
background-color: #252525;
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF;
}
#toolbarContainer.dark #debug-icon a:active,
#toolbarContainer.dark #debug-icon a:link,
#toolbarContainer.dark #debug-icon a:visited {
color: #DD8615;
}
#toolbarContainer.dark #debug-bar {
background-color: #252525;
color: #DFDFDF;
}
#toolbarContainer.dark #debug-bar h1,
#toolbarContainer.dark #debug-bar h2,
#toolbarContainer.dark #debug-bar h3,
#toolbarContainer.dark #debug-bar p,
#toolbarContainer.dark #debug-bar a,
#toolbarContainer.dark #debug-bar button,
#toolbarContainer.dark #debug-bar table,
#toolbarContainer.dark #debug-bar thead,
#toolbarContainer.dark #debug-bar tr,
#toolbarContainer.dark #debug-bar td,
#toolbarContainer.dark #debug-bar button,
#toolbarContainer.dark #debug-bar .toolbar {
background-color: transparent;
color: #DFDFDF;
}
#toolbarContainer.dark #debug-bar button {
background-color: #252525;
}
#toolbarContainer.dark #debug-bar table strong {
color: #DD8615;
}
#toolbarContainer.dark #debug-bar table tbody tr:hover {
background-color: #434343;
}
#toolbarContainer.dark #debug-bar table tbody tr.current {
background-color: #FDC894;
}
#toolbarContainer.dark #debug-bar table tbody tr.current td {
color: #252525;
}
#toolbarContainer.dark #debug-bar table tbody tr.current:hover td {
background-color: #DD4814;
color: #FFFFFF;
}
#toolbarContainer.dark #debug-bar .toolbar {
background-color: #434343;
box-shadow: 0 0 4px #434343;
-moz-box-shadow: 0 0 4px #434343;
-webkit-box-shadow: 0 0 4px #434343;
}
#toolbarContainer.dark #debug-bar .toolbar img {
filter: brightness(0) invert(1);
}
#toolbarContainer.dark #debug-bar.fixed-top .toolbar {
box-shadow: 0 0 4px #434343;
-moz-box-shadow: 0 0 4px #434343;
-webkit-box-shadow: 0 0 4px #434343;
}
#toolbarContainer.dark #debug-bar.fixed-top .tab {
box-shadow: 0 1px 4px #434343;
-moz-box-shadow: 0 1px 4px #434343;
-webkit-box-shadow: 0 1px 4px #434343;
}
#toolbarContainer.dark #debug-bar .muted {
color: #DFDFDF;
}
#toolbarContainer.dark #debug-bar .muted td {
color: #434343;
}
#toolbarContainer.dark #debug-bar .muted:hover td {
color: #DFDFDF;
}
#toolbarContainer.dark #debug-bar #toolbar-position,
#toolbarContainer.dark #debug-bar #toolbar-theme {
filter: brightness(0) invert(0.6);
}
#toolbarContainer.dark #debug-bar .ci-label.active {
background-color: #252525;
}
#toolbarContainer.dark #debug-bar .ci-label:hover {
background-color: #252525;
}
#toolbarContainer.dark #debug-bar .ci-label .badge {
background-color: #DD4814;
color: #FFFFFF;
}
#toolbarContainer.dark #debug-bar .tab {
background-color: #252525;
box-shadow: 0 -1px 4px #434343;
-moz-box-shadow: 0 -1px 4px #434343;
-webkit-box-shadow: 0 -1px 4px #434343;
}
#toolbarContainer.dark #debug-bar .timeline th,
#toolbarContainer.dark #debug-bar .timeline td {
border-color: #434343;
}
#toolbarContainer.dark #debug-bar .timeline .timer {
background-color: #DD8615;
}
#toolbarContainer.dark .debug-view.show-view {
border-color: #DD8615;
}
#toolbarContainer.dark .debug-view-path {
background-color: #FDC894;
color: #434343;
}
#toolbarContainer.dark td[data-debugbar-route] input[type=text] {
background: #000;
color: #fff;
}
#toolbarContainer.light #debug-icon {
background-color: #FFFFFF;
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF;
}
#toolbarContainer.light #debug-icon a:active,
#toolbarContainer.light #debug-icon a:link,
#toolbarContainer.light #debug-icon a:visited {
color: #DD8615;
}
#toolbarContainer.light #debug-bar {
background-color: #FFFFFF;
color: #434343;
}
#toolbarContainer.light #debug-bar h1,
#toolbarContainer.light #debug-bar h2,
#toolbarContainer.light #debug-bar h3,
#toolbarContainer.light #debug-bar p,
#toolbarContainer.light #debug-bar a,
#toolbarContainer.light #debug-bar button,
#toolbarContainer.light #debug-bar table,
#toolbarContainer.light #debug-bar thead,
#toolbarContainer.light #debug-bar tr,
#toolbarContainer.light #debug-bar td,
#toolbarContainer.light #debug-bar button,
#toolbarContainer.light #debug-bar .toolbar {
background-color: transparent;
color: #434343;
}
#toolbarContainer.light #debug-bar button {
background-color: #FFFFFF;
}
#toolbarContainer.light #debug-bar table strong {
color: #DD8615;
}
#toolbarContainer.light #debug-bar table tbody tr:hover {
background-color: #DFDFDF;
}
#toolbarContainer.light #debug-bar table tbody tr.current {
background-color: #FDC894;
}
#toolbarContainer.light #debug-bar table tbody tr.current:hover td {
background-color: #DD4814;
color: #FFFFFF;
}
#toolbarContainer.light #debug-bar .toolbar {
background-color: #FFFFFF;
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF;
}
#toolbarContainer.light #debug-bar .toolbar img {
filter: brightness(0) invert(0.4);
}
#toolbarContainer.light #debug-bar.fixed-top .toolbar {
box-shadow: 0 0 4px #DFDFDF;
-moz-box-shadow: 0 0 4px #DFDFDF;
-webkit-box-shadow: 0 0 4px #DFDFDF;
}
#toolbarContainer.light #debug-bar.fixed-top .tab {
box-shadow: 0 1px 4px #DFDFDF;
-moz-box-shadow: 0 1px 4px #DFDFDF;
-webkit-box-shadow: 0 1px 4px #DFDFDF;
}
#toolbarContainer.light #debug-bar .muted {
color: #434343;
}
#toolbarContainer.light #debug-bar .muted td {
color: #DFDFDF;
}
#toolbarContainer.light #debug-bar .muted:hover td {
color: #434343;
}
#toolbarContainer.light #debug-bar #toolbar-position,
#toolbarContainer.light #debug-bar #toolbar-theme {
filter: brightness(0) invert(0.6);
}
#toolbarContainer.light #debug-bar .ci-label.active {
background-color: #DFDFDF;
}
#toolbarContainer.light #debug-bar .ci-label:hover {
background-color: #DFDFDF;
}
#toolbarContainer.light #debug-bar .ci-label .badge {
background-color: #DD4814;
color: #FFFFFF;
}
#toolbarContainer.light #debug-bar .tab {
background-color: #FFFFFF;
box-shadow: 0 -1px 4px #DFDFDF;
-moz-box-shadow: 0 -1px 4px #DFDFDF;
-webkit-box-shadow: 0 -1px 4px #DFDFDF;
}
#toolbarContainer.light #debug-bar .timeline th,
#toolbarContainer.light #debug-bar .timeline td {
border-color: #DFDFDF;
}
#toolbarContainer.light #debug-bar .timeline .timer {
background-color: #DD8615;
}
#toolbarContainer.light .debug-view.show-view {
border-color: #DD8615;
}
#toolbarContainer.light .debug-view-path {
background-color: #FDC894;
color: #434343;
}
.debug-bar-width30 {
width: 30%;
}
.debug-bar-width10 {
width: 10%;
}
.debug-bar-width70p {
width: 70px;
}
.debug-bar-width190p {
width: 190px;
}
.debug-bar-width20e {
width: 20em;
}
.debug-bar-width6r {
width: 6rem;
}
.debug-bar-ndisplay {
display: none;
}
.debug-bar-alignRight {
text-align: right;
}
.debug-bar-alignLeft {
text-align: left;
}
.debug-bar-noverflow {
overflow: hidden;
}
.debug-bar-dtableRow {
display: table-row;
}
.debug-bar-dinlineBlock {
display: inline-block;
}
.debug-bar-pointer {
cursor: pointer;
}
.debug-bar-mleft4 {
margin-left: 4px;
}
.debug-bar-level-0 {
--level: 0;
}
.debug-bar-level-1 {
--level: 1;
}
.debug-bar-level-2 {
--level: 2;
}
.debug-bar-level-3 {
--level: 3;
}
.debug-bar-level-4 {
--level: 4;
}
.debug-bar-level-5 {
--level: 5;
}
.debug-bar-level-6 {
--level: 6;
}
@@ -0,0 +1,817 @@
/*
* Functionality for the CodeIgniter Debug Toolbar.
*/
var ciDebugBar = {
toolbarContainer: null,
toolbar: null,
icon: null,
init: function () {
this.toolbarContainer = document.getElementById("toolbarContainer");
this.toolbar = document.getElementById("debug-bar");
this.icon = document.getElementById("debug-icon");
ciDebugBar.createListeners();
ciDebugBar.setToolbarState();
ciDebugBar.setToolbarPosition();
ciDebugBar.setToolbarTheme();
ciDebugBar.toggleViewsHints();
ciDebugBar.routerLink();
ciDebugBar.setHotReloadState();
document
.getElementById("debug-bar-link")
.addEventListener("click", ciDebugBar.toggleToolbar, true);
document
.getElementById("debug-icon-link")
.addEventListener("click", ciDebugBar.toggleToolbar, true);
historyLoad = this.toolbar.getElementsByClassName("ci-history-load");
if (historyLoad.length) {
// Allows highlighting the row of the current history request
var btn = this.toolbar.querySelector(
'button[data-time="' + localStorage.getItem("debugbar-time-new") + '"]'
);
ciDebugBar.addClass(btn.parentNode.parentNode, "current");
for (var i = 0; i < historyLoad.length; i++) {
historyLoad[i].addEventListener(
"click",
function () {
loadDoc(this.getAttribute("data-time"));
},
true
);
}
}
// Display the active Tab on page load
var tab = ciDebugBar.readCookie("debug-bar-tab");
if (document.getElementById(tab)) {
var el = document.getElementById(tab);
ciDebugBar.switchClass(el, "debug-bar-ndisplay", "debug-bar-dblock");
ciDebugBar.addClass(el, "active");
tab = document.querySelector("[data-tab=" + tab + "]");
if (tab) {
ciDebugBar.addClass(tab.parentNode, "active");
}
}
},
createListeners: function () {
var buttons = [].slice.call(
this.toolbar.querySelectorAll(".ci-label a")
);
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", ciDebugBar.showTab, true);
}
// Hook up generic toggle via data attributes `data-toggle="foo"`
var links = this.toolbar.querySelectorAll("[data-toggle]");
for (var i = 0; i < links.length; i++) {
let toggleData = links[i].getAttribute("data-toggle");
if (toggleData === "datatable") {
let datatable = links[i].getAttribute("data-table");
links[i].addEventListener("click", function() {
ciDebugBar.toggleDataTable(datatable)
}, true);
} else if (toggleData === "childrows") {
let child = links[i].getAttribute("data-child");
links[i].addEventListener("click", function() {
ciDebugBar.toggleChildRows(child)
}, true);
} else {
links[i].addEventListener("click", ciDebugBar.toggleRows, true);
}
}
},
showTab: function () {
// Get the target tab, if any
var tab = document.getElementById(this.getAttribute("data-tab"));
// If the label have not a tab stops here
if (! tab) {
return;
}
// Remove debug-bar-tab cookie
ciDebugBar.createCookie("debug-bar-tab", "", -1);
// Check our current state.
var state = tab.classList.contains("debug-bar-dblock");
// Hide all tabs
var tabs = document.querySelectorAll("#debug-bar .tab");
for (var i = 0; i < tabs.length; i++) {
ciDebugBar.switchClass(tabs[i], "debug-bar-dblock", "debug-bar-ndisplay");
}
// Mark all labels as inactive
var labels = document.querySelectorAll("#debug-bar .ci-label");
for (var i = 0; i < labels.length; i++) {
ciDebugBar.removeClass(labels[i], "active");
}
// Show/hide the selected tab
if (! state) {
ciDebugBar.switchClass(tab, "debug-bar-ndisplay", "debug-bar-dblock");
ciDebugBar.addClass(this.parentNode, "active");
// Create debug-bar-tab cookie to persistent state
ciDebugBar.createCookie(
"debug-bar-tab",
this.getAttribute("data-tab"),
365
);
}
},
addClass: function (el, className) {
if (el.classList) {
el.classList.add(className);
} else {
el.className += " " + className;
}
},
removeClass: function (el, className) {
if (el.classList) {
el.classList.remove(className);
} else {
el.className = el.className.replace(
new RegExp(
"(^|\\b)" + className.split(" ").join("|") + "(\\b|$)",
"gi"
),
" "
);
}
},
switchClass : function(el, classFrom, classTo) {
ciDebugBar.removeClass(el, classFrom);
ciDebugBar.addClass(el, classTo);
},
/**
* Toggle display of another object based on
* the data-toggle value of this object
*
* @param event
*/
toggleRows: function (event) {
if (event.target) {
let row = event.target.closest("tr");
let target = document.getElementById(
row.getAttribute("data-toggle")
);
if (target.classList.contains("debug-bar-ndisplay")) {
ciDebugBar.switchClass(target, "debug-bar-ndisplay", "debug-bar-dtableRow");
} else {
ciDebugBar.switchClass(target, "debug-bar-dtableRow", "debug-bar-ndisplay");
}
}
},
/**
* Toggle display of a data table
*
* @param obj
*/
toggleDataTable: function (obj) {
if (typeof obj == "string") {
obj = document.getElementById(obj + "_table");
}
if (obj) {
if (obj.classList.contains("debug-bar-ndisplay")) {
ciDebugBar.switchClass(obj, "debug-bar-ndisplay", "debug-bar-dblock");
} else {
ciDebugBar.switchClass(obj, "debug-bar-dblock", "debug-bar-ndisplay");
}
}
},
/**
* Toggle display of timeline child elements
*
* @param obj
*/
toggleChildRows: function (obj) {
if (typeof obj == "string") {
par = document.getElementById(obj + "_parent");
obj = document.getElementById(obj + "_children");
}
if (par && obj) {
if (obj.classList.contains("debug-bar-ndisplay")) {
ciDebugBar.removeClass(obj, "debug-bar-ndisplay");
} else {
ciDebugBar.addClass(obj, "debug-bar-ndisplay");
}
par.classList.toggle("timeline-parent-open");
}
},
//--------------------------------------------------------------------
/**
* Toggle tool bar from full to icon and icon to full
*/
toggleToolbar: function () {
var open = ! ciDebugBar.toolbar.classList.contains("debug-bar-ndisplay");
if (open) {
ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
} else {
ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
}
// Remember it for other page loads on this site
ciDebugBar.createCookie("debug-bar-state", "", -1);
ciDebugBar.createCookie(
"debug-bar-state",
open == true ? "minimized" : "open",
365
);
},
/**
* Sets the initial state of the toolbar (open or minimized) when
* the page is first loaded to allow it to remember the state between refreshes.
*/
setToolbarState: function () {
var open = ciDebugBar.readCookie("debug-bar-state");
if (open != "open") {
ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
} else {
ciDebugBar.switchClass(ciDebugBar.icon, "debug-bar-dinlineBlock", "debug-bar-ndisplay");
ciDebugBar.switchClass(ciDebugBar.toolbar, "debug-bar-ndisplay", "debug-bar-dinlineBlock");
}
},
toggleViewsHints: function () {
// Avoid toggle hints on history requests that are not the initial
if (
localStorage.getItem("debugbar-time") !=
localStorage.getItem("debugbar-time-new")
) {
var a = document.querySelector('a[data-tab="ci-views"]');
a.href = "#";
return;
}
var nodeList = []; // [ Element, NewElement( 1 )/OldElement( 0 ) ]
var sortedComments = [];
var comments = [];
var getComments = function () {
var nodes = [];
var result = [];
var xpathResults = document.evaluate(
"//comment()[starts-with(., ' DEBUG-VIEW')]",
document,
null,
XPathResult.ANY_TYPE,
null
);
var nextNode = xpathResults.iterateNext();
while (nextNode) {
nodes.push(nextNode);
nextNode = xpathResults.iterateNext();
}
// sort comment by opening and closing tags
for (var i = 0; i < nodes.length; ++i) {
// get file path + name to use as key
var path = nodes[i].nodeValue.substring(
18,
nodes[i].nodeValue.length - 1
);
if (nodes[i].nodeValue[12] === "S") {
// simple check for start comment
// create new entry
result[path] = [nodes[i], null];
} else if (result[path]) {
// add to existing entry
result[path][1] = nodes[i];
}
}
return result;
};
// find node that has TargetNode as parentNode
var getParentNode = function (node, targetNode) {
if (node.parentNode === null) {
return null;
}
if (node.parentNode !== targetNode) {
return getParentNode(node.parentNode, targetNode);
}
return node;
};
// define invalid & outer ( also invalid ) elements
const INVALID_ELEMENTS = ["NOSCRIPT", "SCRIPT", "STYLE"];
const OUTER_ELEMENTS = ["HTML", "BODY", "HEAD"];
var getValidElementInner = function (node, reverse) {
// handle invalid tags
if (OUTER_ELEMENTS.indexOf(node.nodeName) !== -1) {
for (var i = 0; i < document.body.children.length; ++i) {
var index = reverse
? document.body.children.length - (i + 1)
: i;
var element = document.body.children[index];
// skip invalid tags
if (INVALID_ELEMENTS.indexOf(element.nodeName) !== -1) {
continue;
}
return [element, reverse];
}
return null;
}
// get to next valid element
while (
node !== null &&
INVALID_ELEMENTS.indexOf(node.nodeName) !== -1
) {
node = reverse
? node.previousElementSibling
: node.nextElementSibling;
}
// return non array if we couldnt find something
if (node === null) {
return null;
}
return [node, reverse];
};
// get next valid element ( to be safe to add divs )
// @return [ element, skip element ] or null if we couldnt find a valid place
var getValidElement = function (nodeElement) {
if (nodeElement) {
if (nodeElement.nextElementSibling !== null) {
return (
getValidElementInner(
nodeElement.nextElementSibling,
false
) ||
getValidElementInner(
nodeElement.previousElementSibling,
true
)
);
}
if (nodeElement.previousElementSibling !== null) {
return getValidElementInner(
nodeElement.previousElementSibling,
true
);
}
}
// something went wrong! -> element is not in DOM
return null;
};
function showHints() {
// Had AJAX? Reset view blocks
sortedComments = getComments();
for (var key in sortedComments) {
var startElement = getValidElement(sortedComments[key][0]);
var endElement = getValidElement(sortedComments[key][1]);
// skip if we couldnt get a valid element
if (startElement === null || endElement === null) {
continue;
}
// find element which has same parent as startelement
var jointParent = getParentNode(
endElement[0],
startElement[0].parentNode
);
if (jointParent === null) {
// find element which has same parent as endelement
jointParent = getParentNode(
startElement[0],
endElement[0].parentNode
);
if (jointParent === null) {
// both tries failed
continue;
} else {
startElement[0] = jointParent;
}
} else {
endElement[0] = jointParent;
}
var debugDiv = document.createElement("div"); // holder
var debugPath = document.createElement("div"); // path
var childArray = startElement[0].parentNode.childNodes; // target child array
var parent = startElement[0].parentNode;
var start, end;
// setup container
debugDiv.classList.add("debug-view");
debugDiv.classList.add("show-view");
debugPath.classList.add("debug-view-path");
debugPath.innerText = key;
debugDiv.appendChild(debugPath);
// calc distance between them
// start
for (var i = 0; i < childArray.length; ++i) {
// check for comment ( start & end ) -> if its before valid start element
if (
childArray[i] === sortedComments[key][1] ||
childArray[i] === sortedComments[key][0] ||
childArray[i] === startElement[0]
) {
start = i;
if (childArray[i] === sortedComments[key][0]) {
start++; // increase to skip the start comment
}
break;
}
}
// adjust if we want to skip the start element
if (startElement[1]) {
start++;
}
// end
for (var i = start; i < childArray.length; ++i) {
if (childArray[i] === endElement[0]) {
end = i;
// dont break to check for end comment after end valid element
} else if (childArray[i] === sortedComments[key][1]) {
// if we found the end comment, we can break
end = i;
break;
}
}
// move elements
var number = end - start;
if (endElement[1]) {
number++;
}
for (var i = 0; i < number; ++i) {
if (INVALID_ELEMENTS.indexOf(childArray[start]) !== -1) {
// skip invalid childs that can cause problems if moved
start++;
continue;
}
debugDiv.appendChild(childArray[start]);
}
// add container to DOM
nodeList.push(parent.insertBefore(debugDiv, childArray[start]));
}
ciDebugBar.createCookie("debug-view", "show", 365);
ciDebugBar.addClass(btn, "active");
}
function hideHints() {
for (var i = 0; i < nodeList.length; ++i) {
var index;
// find index
for (
var j = 0;
j < nodeList[i].parentNode.childNodes.length;
++j
) {
if (nodeList[i].parentNode.childNodes[j] === nodeList[i]) {
index = j;
break;
}
}
// move child back
while (nodeList[i].childNodes.length !== 1) {
nodeList[i].parentNode.insertBefore(
nodeList[i].childNodes[1],
nodeList[i].parentNode.childNodes[index].nextSibling
);
index++;
}
nodeList[i].parentNode.removeChild(nodeList[i]);
}
nodeList.length = 0;
ciDebugBar.createCookie("debug-view", "", -1);
ciDebugBar.removeClass(btn, "active");
}
var btn = document.querySelector("[data-tab=ci-views]");
// If the Views Collector is inactive stops here
if (! btn) {
return;
}
btn.parentNode.onclick = function () {
if (ciDebugBar.readCookie("debug-view")) {
hideHints();
} else {
showHints();
}
};
// Determine Hints state on page load
if (ciDebugBar.readCookie("debug-view")) {
showHints();
}
},
setToolbarPosition: function () {
var btnPosition = this.toolbar.querySelector("#toolbar-position");
if (ciDebugBar.readCookie("debug-bar-position") === "top") {
ciDebugBar.addClass(ciDebugBar.icon, "fixed-top");
ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top");
}
btnPosition.addEventListener(
"click",
function () {
var position = ciDebugBar.readCookie("debug-bar-position");
ciDebugBar.createCookie("debug-bar-position", "", -1);
if (! position || position === "bottom") {
ciDebugBar.createCookie("debug-bar-position", "top", 365);
ciDebugBar.addClass(ciDebugBar.icon, "fixed-top");
ciDebugBar.addClass(ciDebugBar.toolbar, "fixed-top");
} else {
ciDebugBar.createCookie(
"debug-bar-position",
"bottom",
365
);
ciDebugBar.removeClass(ciDebugBar.icon, "fixed-top");
ciDebugBar.removeClass(ciDebugBar.toolbar, "fixed-top");
}
},
true
);
},
setToolbarTheme: function () {
var btnTheme = this.toolbar.querySelector("#toolbar-theme");
var isDarkMode = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
var isLightMode = window.matchMedia(
"(prefers-color-scheme: light)"
).matches;
// If a cookie is set with a value, we force the color scheme
if (ciDebugBar.readCookie("debug-bar-theme") === "dark") {
ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "light");
ciDebugBar.addClass(ciDebugBar.toolbarContainer, "dark");
} else if (ciDebugBar.readCookie("debug-bar-theme") === "light") {
ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark");
ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light");
}
btnTheme.addEventListener(
"click",
function () {
var theme = ciDebugBar.readCookie("debug-bar-theme");
if (
! theme &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
// If there is no cookie, and "prefers-color-scheme" is set to "dark"
// It means that the user wants to switch to light mode
ciDebugBar.createCookie("debug-bar-theme", "light", 365);
ciDebugBar.removeClass(ciDebugBar.toolbarContainer, "dark");
ciDebugBar.addClass(ciDebugBar.toolbarContainer, "light");
} else {
if (theme === "dark") {
ciDebugBar.createCookie(
"debug-bar-theme",
"light",
365
);
ciDebugBar.removeClass(
ciDebugBar.toolbarContainer,
"dark"
);
ciDebugBar.addClass(
ciDebugBar.toolbarContainer,
"light"
);
} else {
// In any other cases: if there is no cookie, or the cookie is set to
// "light", or the "prefers-color-scheme" is "light"...
ciDebugBar.createCookie("debug-bar-theme", "dark", 365);
ciDebugBar.removeClass(
ciDebugBar.toolbarContainer,
"light"
);
ciDebugBar.addClass(
ciDebugBar.toolbarContainer,
"dark"
);
}
}
},
true
);
},
setHotReloadState: function () {
var btn = document.getElementById("debug-hot-reload").parentNode;
var btnImg = btn.getElementsByTagName("img")[0];
var eventSource;
// If the Hot Reload Collector is inactive stops here
if (! btn) {
return;
}
btn.onclick = function () {
if (ciDebugBar.readCookie("debug-hot-reload")) {
ciDebugBar.createCookie("debug-hot-reload", "", -1);
ciDebugBar.removeClass(btn, "active");
ciDebugBar.removeClass(btnImg, "rotate");
// Close the EventSource connection if it exists
if (typeof eventSource !== "undefined") {
eventSource.close();
eventSource = void 0; // Undefine the variable
}
} else {
ciDebugBar.createCookie("debug-hot-reload", "show", 365);
ciDebugBar.addClass(btn, "active");
ciDebugBar.addClass(btnImg, "rotate");
eventSource = ciDebugBar.hotReloadConnect();
}
};
// Determine Hot Reload state on page load
if (ciDebugBar.readCookie("debug-hot-reload")) {
ciDebugBar.addClass(btn, "active");
ciDebugBar.addClass(btnImg, "rotate");
eventSource = ciDebugBar.hotReloadConnect();
}
},
hotReloadConnect: function () {
return;
},
/**
* Helper to create a cookie.
*
* @param name
* @param value
* @param days
*/
createCookie: function (name, value, days) {
if (days) {
var date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
var expires = "; expires=" + date.toGMTString();
} else {
var expires = "";
}
document.cookie =
name + "=" + value + expires + "; path=/; samesite=Lax";
},
readCookie: function (name) {
var nameEQ = name + "=";
var ca = document.cookie.split(";");
for (var i = 0; i < ca.length; i++) {
var c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1, c.length);
}
if (c.indexOf(nameEQ) == 0) {
return c.substring(nameEQ.length, c.length);
}
}
return null;
},
trimSlash: function (text) {
return text.replace(/^\/|\/$/g, "");
},
routerLink: function () {
var row, _location;
var rowGet = this.toolbar.querySelectorAll(
'td[data-debugbar-route="GET"]'
);
var patt = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/;
for (var i = 0; i < rowGet.length; i++) {
row = rowGet[i];
if (!/\/\(.+?\)/.test(rowGet[i].innerText)) {
ciDebugBar.addClass(row, "debug-bar-pointer");
row.setAttribute(
"title",
location.origin + "/" + ciDebugBar.trimSlash(row.innerText)
);
row.addEventListener("click", function (ev) {
_location =
location.origin +
"/" +
ciDebugBar.trimSlash(ev.target.innerText);
var redirectWindow = window.open(_location, "_blank");
redirectWindow.location;
});
} else {
row.innerHTML =
"<div>" +
row.innerText +
"</div>" +
'<form data-debugbar-route-tpl="' +
ciDebugBar.trimSlash(row.innerText.replace(patt, "?")) +
'">' +
row.innerText.replace(
patt,
'<input type="text" placeholder="$1">'
) +
'<input type="submit" value="Go" class="debug-bar-mleft4">' +
"</form>";
}
}
rowGet = this.toolbar.querySelectorAll(
'td[data-debugbar-route="GET"] form'
);
for (var i = 0; i < rowGet.length; i++) {
row = rowGet[i];
row.addEventListener("submit", function (event) {
event.preventDefault();
var inputArray = [],
t = 0;
var input = event.target.querySelectorAll("input[type=text]");
var tpl = event.target.getAttribute("data-debugbar-route-tpl");
for (var n = 0; n < input.length; n++) {
if (input[n].value.length > 0) {
inputArray.push(input[n].value);
}
}
if (inputArray.length > 0) {
_location =
location.origin +
"/" +
tpl.replace(/\?/g, function () {
return inputArray[t++];
});
var redirectWindow = window.open(_location, "_blank");
redirectWindow.location;
}
});
}
},
};
@@ -0,0 +1,124 @@
<?php declare(strict_types=1);
use CodeIgniter\Debug\Toolbar;
use CodeIgniter\View\Parser;
/**
* @var Toolbar $this
* @var int $totalTime
* @var int $totalMemory
* @var string $url
* @var string $method
* @var bool $isAJAX
* @var int $startTime
* @var int $totalTime
* @var int $totalMemory
* @var float $segmentDuration
* @var int $segmentCount
* @var string $CI_VERSION
* @var array $collectors
* @var array $vars
* @var array $styles
* @var Parser $parser
*/
?>
<style>
<?= preg_replace('#[\r\n\t ]+#', ' ', file_get_contents(__DIR__ . '/toolbar.css')) ?>
</style>
<script id="toolbar_js">
var ciSiteURL = "<?= rtrim(site_url(), '/') ?>"
<?= file_get_contents(__DIR__ . '/toolbar.js') ?>
</script>
<div id="debug-icon" class="debug-bar-ndisplay">
<a id="debug-icon-link">
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 155 200"><defs/><path fill="#2890ff" d="M73.7 3.7c2.2 7.9-.7 18.5-7.8 29-1.8 2.6-10.7 12.2-19.7 21.3-23.9 24-33.6 37.1-40.3 54.4-7.9 20.6-7.8 40.8.5 58.2C12.8 180 27.6 193 42.5 198l6 2-3-2.2c-21-15.2-22.9-38.7-4.8-58.8 2.5-2.7 4.8-5 5.1-5 .4 0 .7 2.7.7 6.1 0 5.7.2 6.2 3.7 9.5 3 2.7 4.6 3.4 7.8 3.4 5.6 0 9.9-2.4 11.6-6.5 2.9-6.9 1.6-12-5-20.5-10.5-13.4-11.7-23.3-4.3-34.7l3.1-4.8.7 4.7c1.3 8.2 5.8 12.9 25 25.8 20.9 14.1 30.6 26.1 32.8 40.5 1.1 7.2-.1 16.1-3.1 21.8-2.7 5.3-11.2 14.3-16.5 17.4-2.4 1.4-4.3 2.6-4.3 2.8 0 .2 2.4-.4 5.3-1.4 24.1-8.3 42.7-27.1 48.2-48.6 1.9-7.6 1.9-20.2-.1-28.5-3.5-15.2-14.6-30.5-29.9-41.2l-7-4.9-.6 3.3c-.8 4.8-2.6 7.6-5.9 9.3-4.5 2.3-10.3 1.9-13.8-1-6.7-5.7-7.8-14.6-3.7-30.5 3-11.6 3.2-20.6.5-29.1C88.3 18 80.6 6.3 74.8 2.2 73.1.9 73 1 73.7 3.7z"/></svg>
</a>
</div>
<div id="debug-bar">
<div class="toolbar">
<span id="toolbar-position">&#8597;</span>
<span id="toolbar-theme">&#128261;</span>
<span id="hot-reload-btn" class="ci-label">
<a id="debug-hot-reload" title="Toggle Hot Reload">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAACXBIWXMAAAsTAAALEwEAmpwYAAABNklEQVR4nN2US04CQRCGv/DaiBxEvYWuBRPDKSCIXsCdcg0ULqTI8xIGN7JwTCU/ScV5tTO64Us6maSq/7+nuqvgkLgHopTl+QAWwBToAg3+wMTzM7YBrihp4jkCToEB8OJyRkCFAB5yDDxVoAd8OpNMOkrcAeMAgz3nzsQ0EqkDayXZqXy5Qugrdy2tGNdKeNWv40xCqGpvJK0YEwXt8ooylMZzUnCh4EkJgzNpmFaMrYLNEgbH0thmGVhSUVrSeE8KLv+7RBMFb0oY3EnDeihGN+WZhmJ7ZlnPtKHB5RvtNwy0d5XWaGgqRmp7a/9QLjRevoDLvOSRM+nnlKumk++0xwZlLhVnEulOhnohTS37vnU1t5M/ho7rPR03/LKW1bxNQep6ETZb5mpGW2/Ak2KpF3oYfAPX9Xpc671kqwAAAABJRU5ErkJggg==" />
</a>
</span>
<span class="ci-label">
<a data-tab="ci-timeline">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAD7SURBVEhLY6ArSEtLK09NTbWHcvGC9PR0BaDaQiAdUl9fzwQVxg+AFvwHamqHcnGCpKQkeaDa9yD1UD09UCn8AKaBWJySkmIApFehi0ONwwRQBceBLurAh4FqFoHUAtkrgPgREN+ByYEw1DhMANVEMIhAYQ5U1wtU/wmILwLZRlAp/IBYC8gGw88CaFj3A/FnIL4ETDXGUCnyANSC/UC6HIpnQMXAqQXIvo0khxNDjcMEQEmU9AzDuNI7Lgw1DhOAJIEuhQcRKMcC+e+QNHdDpcgD6BaAANSSQqBcENFlDi6AzQKqgkFlwWhxjVI8o2OgmkFaXI8CTMDAAAAxd1O4FzLMaAAAAABJRU5ErkJggg==">
<span class="hide-sm"><?= $totalTime ?> ms &nbsp; <?= $totalMemory ?> MB</span>
</a>
</span>
<?php foreach ($collectors as $c) : ?>
<?php if (! $c['isEmpty'] && ($c['hasTabContent'] || $c['hasLabel'])) : ?>
<span class="ci-label">
<a data-tab="ci-<?= $c['titleSafe'] ?>">
<img src="<?= $c['icon'] ?>">
<span class="hide-sm">
<?= $c['title'] ?>
<?php if ($c['badgeValue'] !== null) : ?>
<span class="badge"><?= $c['badgeValue'] ?></span>
<?php endif ?>
</span>
</a>
</span>
<?php endif ?>
<?php endforeach ?>
<h1>
<span class="ci-label">
<a data-tab="ci-config">
<svg xmlns="http://www.w3.org/2000/svg" version="1.0" viewBox="0 0 155 200"><defs/><path fill="#2890ff" d="M73.7 3.7c2.2 7.9-.7 18.5-7.8 29-1.8 2.6-10.7 12.2-19.7 21.3-23.9 24-33.6 37.1-40.3 54.4-7.9 20.6-7.8 40.8.5 58.2C12.8 180 27.6 193 42.5 198l6 2-3-2.2c-21-15.2-22.9-38.7-4.8-58.8 2.5-2.7 4.8-5 5.1-5 .4 0 .7 2.7.7 6.1 0 5.7.2 6.2 3.7 9.5 3 2.7 4.6 3.4 7.8 3.4 5.6 0 9.9-2.4 11.6-6.5 2.9-6.9 1.6-12-5-20.5-10.5-13.4-11.7-23.3-4.3-34.7l3.1-4.8.7 4.7c1.3 8.2 5.8 12.9 25 25.8 20.9 14.1 30.6 26.1 32.8 40.5 1.1 7.2-.1 16.1-3.1 21.8-2.7 5.3-11.2 14.3-16.5 17.4-2.4 1.4-4.3 2.6-4.3 2.8 0 .2 2.4-.4 5.3-1.4 24.1-8.3 42.7-27.1 48.2-48.6 1.9-7.6 1.9-20.2-.1-28.5-3.5-15.2-14.6-30.5-29.9-41.2l-7-4.9-.6 3.3c-.8 4.8-2.6 7.6-5.9 9.3-4.5 2.3-10.3 1.9-13.8-1-6.7-5.7-7.8-14.6-3.7-30.5 3-11.6 3.2-20.6.5-29.1C88.3 18 80.6 6.3 74.8 2.2 73.1.9 73 1 73.7 3.7z"/></svg>
</a>
</span>
</h1>
<!-- Open/Close Toggle -->
<a id="debug-bar-link" role="button" title="Open/Close">
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAEPSURBVEhL7ZVLDoJAEEThRuoGDwSEG+jCuFU34s3AK3APP1VDDSGMqI1xx0s6M/2rnlHEaMZElmWrPM+vsDvsYbQ7+us0TReSC2EBrEHxCevRYuppYLXkQpC8sVCuGfTvqSE3hFdFwUGuGfRvqSE35NUAfKZrbQNQm2jrMA+gOK+M+FmhDsRL5voHMA8gFGecq0JOXLWlQg7E7AMIxZnjOiZOEJ82gFCcedUE4gS56QP8yf8ywItz7e+RituKlkkDBoIOH4Nd4HZD4NsGYJ/Abn1xEVOcuZ8f0zc/tHiYmzTAwscBvDIK/veyQ9K/rnewjdF26q0kF1IUxZIFPAVW98x/a+qp8L2M/+HMhETRE6S8TxpZ7KGXAAAAAElFTkSuQmCC">
</a>
</div>
<!-- Timeline -->
<div id="ci-timeline" class="tab">
<table class="timeline">
<thead>
<tr>
<th class="debug-bar-width30">NAME</th>
<th class="debug-bar-width10">COMPONENT</th>
<th class="debug-bar-width10">DURATION</th>
<?php for ($i = 0; $i < $segmentCount; $i++) : ?>
<th><?= $i * $segmentDuration ?> ms</th>
<?php endfor ?>
</tr>
</thead>
<tbody>
<?= $this->renderTimeline($collectors, $startTime, $segmentCount, $segmentDuration, $styles) ?>
</tbody>
</table>
</div>
<!-- Collector-provided Tabs -->
<?php foreach ($collectors as $c) : ?>
<?php if (! $c['isEmpty']) : ?>
<?php if ($c['hasTabContent']) : ?>
<div id="ci-<?= $c['titleSafe'] ?>" class="tab">
<h2><?= $c['title'] ?> <span><?= $c['titleDetails'] ?></span></h2>
<?= is_string($c['display']) ? $c['display'] : $this->render("_{$c['titleSafe']}.tpl", $c['display']) ?>
</div>
<?php endif ?>
<?php endif ?>
<?php endforeach ?>
</div>
<style>
<?php foreach ($styles as $name => $style): ?>
<?= sprintf(".%s { %s }\n", $name, $style) ?>
<?php endforeach ?>
</style>
@@ -0,0 +1,90 @@
document.addEventListener('DOMContentLoaded', loadDoc, false);
function loadDoc(time) {
if (isNaN(time)) {
time = document.getElementById("debugbar_loader").getAttribute("data-time");
localStorage.setItem('debugbar-time', time);
}
localStorage.setItem('debugbar-time-new', time);
let url = '{url}';
let xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState === 4 && this.status === 200) {
let toolbar = document.getElementById("toolbarContainer");
if (! toolbar) {
toolbar = document.createElement('div');
toolbar.setAttribute('id', 'toolbarContainer');
document.body.appendChild(toolbar);
}
let responseText = this.responseText;
let dynamicStyle = document.getElementById('debugbar_dynamic_style');
let dynamicScript = document.getElementById('debugbar_dynamic_script');
// get the first style block, copy contents to dynamic_style, then remove here
let start = responseText.indexOf('>', responseText.indexOf('<style')) + 1;
let end = responseText.indexOf('</style>', start);
dynamicStyle.innerHTML = responseText.substr(start, end - start);
responseText = responseText.substr(end + 8);
// get the first script after the first style, copy contents to dynamic_script, then remove here
start = responseText.indexOf('>', responseText.indexOf('<script')) + 1;
end = responseText.indexOf('\<\/script>', start);
dynamicScript.innerHTML = responseText.substr(start, end - start);
responseText = responseText.substr(end + 9);
// check for last style block, append contents to dynamic_style, then remove here
start = responseText.indexOf('>', responseText.indexOf('<style')) + 1;
end = responseText.indexOf('</style>', start);
dynamicStyle.innerHTML += responseText.substr(start, end - start);
responseText = responseText.substr(0, start - 8);
toolbar.innerHTML = responseText;
if (typeof ciDebugBar === 'object') {
ciDebugBar.init();
}
} else if (this.readyState === 4 && this.status === 404) {
console.log('CodeIgniter DebugBar: File "WRITEPATH/debugbar/debugbar_' + time + '" not found.');
}
};
xhttp.open("GET", url + "?debugbar_time=" + time, true);
xhttp.send();
}
window.oldXHR = window.ActiveXObject
? new ActiveXObject('Microsoft.XMLHTTP')
: window.XMLHttpRequest;
function newXHR() {
const realXHR = new window.oldXHR();
realXHR.addEventListener("readystatechange", function() {
// Only success responses and URLs that do not contains "debugbar_time" are tracked
if (realXHR.readyState === 4 && realXHR.status.toString()[0] === '2' && realXHR.responseURL.indexOf('debugbar_time') === -1) {
if (realXHR.getAllResponseHeaders().indexOf("Debugbar-Time") >= 0) {
let debugbarTime = realXHR.getResponseHeader('Debugbar-Time');
if (debugbarTime) {
let h2 = document.querySelector('#ci-history > h2');
if (h2) {
h2.innerHTML = 'History <small>You have new debug data.</small> <button id="ci-history-update">Update</button>';
document.querySelector('a[data-tab="ci-history"] > span > .badge').className += ' active';
document.getElementById('ci-history-update').addEventListener('click', function () {
loadDoc(debugbarTime);
}, false)
}
}
}
}
}, false);
return realXHR;
}
window.XMLHttpRequest = newXHR;
+270
View File
@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Events;
/**
* Events
*
* @see \CodeIgniter\Events\EventsTest
*/
class Events
{
public const PRIORITY_LOW = 200;
public const PRIORITY_NORMAL = 100;
public const PRIORITY_HIGH = 10;
/**
* The list of listeners.
*
* @var array<string, array{0: bool, 1: list<int>, 2: list<callable(mixed): mixed>}>
*/
protected static $listeners = [];
/**
* Flag to let us know if we've read from the Config file(s)
* and have all of the defined events.
*
* @var bool
*/
protected static $initialized = false;
/**
* If true, events will not actually be fired.
* Useful during testing.
*
* @var bool
*/
protected static $simulate = false;
/**
* Stores information about the events
* for display in the debug toolbar.
*
* @var list<array{start: float, end: float, event: string}>
*/
protected static $performanceLog = [];
/**
* A list of found files.
*
* @var list<string>
*/
protected static $files = [];
/**
* Ensures that we have a events file ready.
*
* @return void
*/
public static function initialize()
{
// Don't overwrite anything....
if (static::$initialized) {
return;
}
$files = [];
static::$initialized = true;
}
/**
* Registers an action to happen on an event. The action can be any sort
* of callable:
*
* Events::on('create', 'myFunction'); // procedural function
* Events::on('create', ['myClass', 'myMethod']); // Class::method
* Events::on('create', [$myInstance, 'myMethod']); // Method on an existing instance
* Events::on('create', function() {}); // Closure
*
* @param string $eventName
* @param callable(mixed): mixed $callback
* @param int $priority
*
* @return void
*/
public static function on($eventName, $callback, $priority = self::PRIORITY_NORMAL)
{
if (! isset(static::$listeners[$eventName])) {
static::$listeners[$eventName] = [
true, // If there's only 1 item, it's sorted.
[$priority],
[$callback],
];
} else {
static::$listeners[$eventName][0] = false; // Not sorted
static::$listeners[$eventName][1][] = $priority;
static::$listeners[$eventName][2][] = $callback;
}
}
/**
* Runs through all subscribed methods running them one at a time,
* until either:
* a) All subscribers have finished or
* b) a method returns false, at which point execution of subscribers stops.
*
* @param string $eventName
* @param mixed ...$arguments
*/
public static function trigger($eventName, ...$arguments): bool
{
// Read in our Config/Events file so that we have them all!
if (! static::$initialized) {
static::initialize();
}
$listeners = static::listeners($eventName);
foreach ($listeners as $listener) {
$start = microtime(true);
$result = static::$simulate === false ? $listener(...$arguments) : true;
if (CI_DEBUG) {
static::$performanceLog[] = [
'start' => $start,
'end' => microtime(true),
'event' => $eventName,
];
}
if ($result === false) {
return false;
}
}
return true;
}
/**
* Returns an array of listeners for a single event. They are
* sorted by priority.
*
* @param string $eventName
*
* @return list<callable(mixed): mixed>
*/
public static function listeners($eventName): array
{
if (! isset(static::$listeners[$eventName])) {
return [];
}
// The list is not sorted
if (! static::$listeners[$eventName][0]) {
// Sort it!
array_multisort(static::$listeners[$eventName][1], SORT_NUMERIC, static::$listeners[$eventName][2]);
// Mark it as sorted already!
static::$listeners[$eventName][0] = true;
}
return static::$listeners[$eventName][2];
}
/**
* Removes a single listener from an event.
*
* If the listener couldn't be found, returns FALSE, else TRUE if
* it was removed.
*
* @param string $eventName
* @param callable(mixed): mixed $listener
*/
public static function removeListener($eventName, callable $listener): bool
{
if (! isset(static::$listeners[$eventName])) {
return false;
}
foreach (static::$listeners[$eventName][2] as $index => $check) {
if ($check === $listener) {
return true;
}
}
return false;
}
/**
* Removes all listeners.
*
* If the event_name is specified, only listeners for that event will be
* removed, otherwise all listeners for all events are removed.
*
* @param string|null $eventName
*
* @return void
*/
public static function removeAllListeners($eventName = null)
{
if ($eventName !== null) {
unset(static::$listeners[$eventName]);
} else {
static::$listeners = [];
}
}
/**
* Sets the path to the file that routes are read from.
*
* @param list<string> $files
*
* @return void
*/
public static function setFiles(array $files)
{
static::$files = $files;
}
/**
* Returns the files that were found/loaded during this request.
*
* @return list<string>
*/
public static function getFiles()
{
return static::$files;
}
/**
* Turns simulation on or off. When on, events will not be triggered,
* simply logged. Useful during testing when you don't actually want
* the tests to run.
*
* @return void
*/
public static function simulate(bool $choice = true)
{
static::$simulate = $choice;
}
/**
* Getter for the performance log records.
*
* @return list<array{start: float, end: float, event: string}>
*/
public static function getPerformanceLogs()
{
return static::$performanceLog;
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
/**
* Error: Critical conditions, like component unavailable, etc.
*/
class CriticalError extends RuntimeException
{
}
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
use Throwable;
/**
* This trait provides framework exceptions the ability to pinpoint
* accurately where the exception was raised rather than instantiated.
*
* This is used primarily for factory-instantiated exceptions.
*/
trait DebugTraceableTrait
{
/**
* Tweaks the exception's constructor to assign the file/line to where
* it is actually raised rather than were it is instantiated.
*/
final public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$trace = $this->getTrace()[0];
if (isset($trace['class']) && $trace['class'] === static::class) {
[
'line' => $this->line,
'file' => $this->file,
] = $trace;
}
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
/**
* Provides a domain-level interface for broad capture
* of all framework-related exceptions.
*
* catch (\CodeIgniter\Exceptions\ExceptionInterface) { ... }
*/
interface ExceptionInterface
{
}
@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
/**
* Class FrameworkException
*
* A collection of exceptions thrown by the framework
* that can only be determined at run time.
*/
class FrameworkException extends RuntimeException
{
use DebugTraceableTrait;
/**
* @return static
*/
public static function forEnabledZlibOutputCompression()
{
return new static(lang('Core.enabledZlibOutputCompression'));
}
/**
* @return static
*/
public static function forInvalidFile(string $path)
{
return new static(lang('Core.invalidFile', [$path]));
}
/**
* @return static
*/
public static function forInvalidDirectory(string $path)
{
return new static(lang('Core.invalidDirectory', [$path]));
}
/**
* @return static
*/
public static function forCopyError(string $path)
{
return new static(lang('Core.copyError', [$path]));
}
/**
* @return static
*
* @deprecated 4.5.0 No longer used.
*/
public static function forMissingExtension(string $extension)
{
if (str_contains($extension, 'intl')) {
// @codeCoverageIgnoreStart
$message = sprintf(
'The framework needs the following extension(s) installed and loaded: %s.',
$extension,
);
// @codeCoverageIgnoreEnd
} else {
$message = lang('Core.missingExtension', [$extension]);
}
return new static($message);
}
/**
* @return static
*/
public static function forNoHandlers(string $class)
{
return new static(lang('Core.noHandlers', [$class]));
}
/**
* @return static
*/
public static function forFabricatorCreateFailed(string $table, string $reason)
{
return new static(lang('Fabricator.createFailed', [$table, $reason]));
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
/**
* Interface for Exceptions that has exception code as HTTP status code.
*/
interface HTTPExceptionInterface extends ExceptionInterface
{
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
/**
* Interface for Exceptions that has exception code as exit code.
*/
interface HasExitCodeInterface extends ExceptionInterface
{
/**
* Returns exit status code.
*/
public function getExitCode(): int;
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
class PageNotFoundException extends RuntimeException implements HTTPExceptionInterface
{
use DebugTraceableTrait;
/**
* HTTP status code
*
* @var int
*/
protected $code = 404;
/**
* @return static
*/
public static function forPageNotFound(?string $message = null)
{
return new static($message ?? lang('页面未找到'));
}
/**
* @return static
*/
public static function forControllerNotFound(string $controller)
{
return new static(lang('控制器(%s)不存在', $controller));
}
/**
* @return static
*/
public static function forMethodNotFound(string $controller, string $method)
{
return new static(lang('控制器(%s)的方法(%s)不存在', $controller, $method));
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Exceptions;
/**
* Exception thrown if an error which can only be found on runtime occurs.
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}
@@ -0,0 +1,7 @@
<?php
use CodeIgniter\CLI\CLI;
CLI::error('ERROR: ' . $code);
CLI::write($message);
CLI::newLine();
@@ -0,0 +1,65 @@
<?php
use CodeIgniter\CLI\CLI;
// The main Exception
CLI::write('[' . $exception::class . ']', 'light_gray', 'red');
CLI::write($message);
CLI::write('at ' . CLI::color(clean_path($exception->getFile()) . ':' . $exception->getLine(), 'green'));
CLI::newLine();
$last = $exception;
while ($prevException = $last->getPrevious()) {
$last = $prevException;
CLI::write(' Caused by:');
CLI::write(' [' . $prevException::class . ']', 'red');
CLI::write(' ' . $prevException->getMessage());
CLI::write(' at ' . CLI::color(clean_path($prevException->getFile()) . ':' . $prevException->getLine(), 'green'));
CLI::newLine();
}
// The backtrace
if (defined('SHOW_DEBUG_BACKTRACE') && SHOW_DEBUG_BACKTRACE) {
$backtraces = $last->getTrace();
if ($backtraces) {
CLI::write('Backtrace:', 'green');
}
foreach ($backtraces as $i => $error) {
$padFile = ' '; // 4 spaces
$padClass = ' '; // 7 spaces
$c = str_pad($i + 1, 3, ' ', STR_PAD_LEFT);
if (isset($error['file'])) {
$filepath = clean_path($error['file']) . ':' . $error['line'];
CLI::write($c . $padFile . CLI::color($filepath, 'yellow'));
} else {
CLI::write($c . $padFile . CLI::color('[internal function]', 'yellow'));
}
$function = '';
if (isset($error['class'])) {
$type = ($error['type'] === '->') ? '()' . $error['type'] : $error['type'];
$function .= $padClass . $error['class'] . $type . $error['function'];
} elseif (! isset($error['class']) && isset($error['function'])) {
$function .= $padClass . $error['function'];
}
$args = implode(', ', array_map(static fn ($value): string => match (true) {
is_object($value) => 'Object(' . $value::class . ')',
is_array($value) => $value !== [] ? '[...]' : '[]',
$value === null => 'null', // return the lowercased version
default => var_export($value, true),
}, array_values($error['args'] ?? [])));
$function .= '(' . $args . ')';
CLI::write($function);
CLI::newLine();
}
}
@@ -0,0 +1,5 @@
<?php
// On the CLI, we still want errors in productions
// so just use the exception template.
include __DIR__ . '/error_exception.php';
@@ -0,0 +1,193 @@
:root {
--main-bg-color: #fff;
--main-text-color: #555;
--dark-text-color: #222;
--light-text-color: #c7c7c7;
--brand-primary-color: #2890ff;
--light-bg-color: #ededee;
--dark-bg-color: #404040;
}
body {
height: 100%;
background: var(--main-bg-color);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
color: var(--main-text-color);
font-weight: 300;
margin: 0;
padding: 0;
}
h1 {
font-weight: lighter;
font-size: 3rem;
color: var(--dark-text-color);
margin: 0;
}
h1.headline {
margin-top: 20%;
font-size: 5rem;
}
.text-center {
text-align: center;
}
p.lead {
font-size: 1.6rem;
}
.container {
max-width: 75rem;
margin: 0 auto;
padding: 1rem;
}
.header {
background: var(--light-bg-color);
color: var(--dark-text-color);
margin-top: 2.17rem;
}
.header .container {
padding: 1rem;
}
.header h1 {
font-size: 2.5rem;
font-weight: 500;
}
.header p {
font-size: 1.2rem;
margin: 0;
line-height: 2.5;
}
.header a {
color: var(--brand-primary-color);
text-decoration: none;
}
.header:hover a {
display: inline;
}
.environment {
background: var(--brand-primary-color);
color: var(--main-bg-color);
text-align: center;
padding: calc(4px + 0.2083vw);
width: 100%;
top: 0;
position: fixed;
margin-top: 5px;
}
.source {
background: #343434;
color: var(--light-text-color);
padding: 0.5em 1em;
border-radius: 5px;
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
font-size: 0.85rem;
margin: 0;
overflow-x: scroll;
}
.source span.line {
line-height: 1.4;
}
.source span.line .number {
color: #666;
}
.source .line .highlight {
display: block;
background: var(--dark-text-color);
color: var(--light-text-color);
}
.source span.highlight .number {
color: #fff;
}
.tabs {
list-style: none;
list-style-position: inside;
margin: 0;
padding: 0;
margin-bottom: -1px;
}
.tabs li {
display: inline;
}
.tabs a:link,
.tabs a:visited {
padding: 0 1rem;
line-height: 2.7;
text-decoration: none;
color: var(--dark-text-color);
background: var(--light-bg-color);
border: 1px solid rgba(0,0,0,0.15);
border-bottom: 0;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
display: inline-block;
}
.tabs a:hover {
background: var(--light-bg-color);
border-color: rgba(0,0,0,0.15);
}
.tabs a.active {
background: var(--main-bg-color);
color: var(--main-text-color);
}
.tab-content {
background: var(--main-bg-color);
border: 1px solid rgba(0,0,0,0.15);
}
.content {
padding: 1rem;
}
.hide {
display: none;
}
.alert {
margin-top: 2rem;
display: block;
text-align: center;
line-height: 3.0;
background: #d9edf7;
border: 1px solid #bcdff1;
border-radius: 5px;
color: #31708f;
}
table {
width: 100%;
overflow: hidden;
}
th {
text-align: left;
border-bottom: 1px solid #e7e7e7;
padding-bottom: 0.5rem;
}
td {
padding: 0.2rem 0.5rem 0.2rem 0;
}
tr:hover td {
background: #f1f1f1;
}
td pre {
white-space: pre-wrap;
}
.trace a {
color: inherit;
}
.trace table {
width: auto;
}
.trace tr td:first-child {
min-width: 5em;
font-weight: bold;
}
.trace td {
background: var(--light-bg-color);
padding: 0 1rem;
}
.trace td pre {
margin: 0;
}
.args {
display: none;
}
@@ -0,0 +1,116 @@
var tabLinks = new Array();
var contentDivs = new Array();
function init()
{
// Grab the tab links and content divs from the page
var tabListItems = document.getElementById('tabs').childNodes;
console.log(tabListItems);
for (var i = 0; i < tabListItems.length; i ++)
{
if (tabListItems[i].nodeName == "LI")
{
var tabLink = getFirstChildWithTagName(tabListItems[i], 'A');
var id = getHash(tabLink.getAttribute('href'));
tabLinks[id] = tabLink;
contentDivs[id] = document.getElementById(id);
}
}
// Assign onclick events to the tab links, and
// highlight the first tab
var i = 0;
for (var id in tabLinks)
{
tabLinks[id].onclick = showTab;
tabLinks[id].onfocus = function () {
this.blur()
};
if (i == 0)
{
tabLinks[id].className = 'active';
}
i ++;
}
// Hide all content divs except the first
var i = 0;
for (var id in contentDivs)
{
if (i != 0)
{
console.log(contentDivs[id]);
contentDivs[id].className = 'content hide';
}
i ++;
}
}
function showTab()
{
var selectedId = getHash(this.getAttribute('href'));
// Highlight the selected tab, and dim all others.
// Also show the selected content div, and hide all others.
for (var id in contentDivs)
{
if (id == selectedId)
{
tabLinks[id].className = 'active';
contentDivs[id].className = 'content';
}
else
{
tabLinks[id].className = '';
contentDivs[id].className = 'content hide';
}
}
// Stop the browser following the link
return false;
}
function getFirstChildWithTagName(element, tagName)
{
for (var i = 0; i < element.childNodes.length; i ++)
{
if (element.childNodes[i].nodeName == tagName)
{
return element.childNodes[i];
}
}
}
function getHash(url)
{
var hashPos = url.lastIndexOf('#');
return url.substring(hashPos + 1);
}
function toggle(elem)
{
elem = document.getElementById(elem);
if (elem.style && elem.style['display'])
{
// Only works with the "style" attr
var disp = elem.style['display'];
}
else if (elem.currentStyle)
{
// For MSIE, naturally
var disp = elem.currentStyle['display'];
}
else if (window.getComputedStyle)
{
// For most other browsers
var disp = document.defaultView.getComputedStyle(elem, null).getPropertyValue('display');
}
// Toggle the state of the "display" style
elem.style.display = disp == 'block' ? 'none' : 'block';
return false;
}
@@ -0,0 +1,84 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('系统故障') ?></title>
<style>
div.logo {
height: 200px;
width: 155px;
display: inline-block;
opacity: 0.08;
position: absolute;
top: 2rem;
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
font-size: 3rem;
margin-top: 0;
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
text-align: center;
border: 1px solid #efefef;
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
padding: 0.5rem 1rem;
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
padding: 1em 2em 0 2em;
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
color: #dd4814;
}
</style>
</head>
<body>
<div class="wrap">
<h1>400</h1>
<p>
<?php if (CI_DEBUG) : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('系统故障') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><?= lang('页面未找到') ?></title>
<style>
div.logo {
height: 200px;
width: 155px;
display: inline-block;
opacity: 0.08;
position: absolute;
top: 2rem;
left: 50%;
margin-left: -73px;
}
body {
height: 100%;
background: #fafafa;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #777;
font-weight: 300;
}
h1 {
font-weight: lighter;
letter-spacing: normal;
font-size: 3rem;
margin-top: 0;
margin-bottom: 0;
color: #222;
}
.wrap {
max-width: 1024px;
margin: 5rem auto;
padding: 2rem;
background: #fff;
text-align: center;
border: 1px solid #efefef;
border-radius: 0.5rem;
position: relative;
}
pre {
white-space: normal;
margin-top: 1.5rem;
}
code {
background: #fafafa;
border: 1px solid #efefef;
padding: 0.5rem 1rem;
border-radius: 5px;
display: block;
}
p {
margin-top: 1.5rem;
}
.footer {
margin-top: 2rem;
border-top: 1px solid #efefef;
padding: 1em 2em 0 2em;
font-size: 85%;
color: #999;
}
a:active,
a:link,
a:visited {
color: #dd4814;
}
</style>
</head>
<body>
<div class="wrap">
<h1>404</h1>
<p>
<?php if (CI_DEBUG) : ?>
<?= nl2br(esc($message)) ?>
<?php else : ?>
<?= lang('没有找到此种页面') ?>
<?php endif; ?>
</p>
</div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More