Initial EV files

This commit is contained in:
Olu Amey
2021-08-05 18:27:41 -04:00
commit 326a5bfdad
591 changed files with 114945 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
<IfModule authz_core_module>
Require all denied
</IfModule>
<IfModule !authz_core_module>
Deny from all
</IfModule>
+439
View File
@@ -0,0 +1,439 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\API;
use CodeIgniter\Format\FormatterInterface;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\Response;
use Config\Services;
/**
* Response trait.
*
* Provides common, more readable, methods to provide
* consistent HTTP responses under a variety of common
* situations when working as an API.
*
* @property IncomingRequest $request
* @property Response $response
*/
trait ResponseTrait
{
/**
* Allows child classes to override the
* status code that is used in their API.
*
* @var array
*/
protected $codes = [
'created' => 201,
'deleted' => 200,
'updated' => 200,
'no_content' => 204,
'invalid_request' => 400,
'unsupported_response_type' => 400,
'invalid_scope' => 400,
'temporarily_unavailable' => 400,
'invalid_grant' => 400,
'invalid_credentials' => 400,
'invalid_refresh' => 400,
'no_data' => 400,
'invalid_data' => 400,
'access_denied' => 401,
'unauthorized' => 401,
'invalid_client' => 401,
'forbidden' => 403,
'resource_not_found' => 404,
'not_acceptable' => 406,
'resource_exists' => 409,
'conflict' => 409,
'resource_gone' => 410,
'payload_too_large' => 413,
'unsupported_media_type' => 415,
'too_many_requests' => 429,
'server_error' => 500,
'unsupported_grant_type' => 501,
'not_implemented' => 501,
];
/**
* How to format the response data.
* Either 'json' or 'xml'. If blank will be
* determine through content negotiation.
*
* @var string
*/
protected $format = 'json';
/**
* Current Formatter instance. This is usually set by ResponseTrait::format
*
* @var FormatterInterface
*/
protected $formatter;
//--------------------------------------------------------------------
/**
* Provides a single, simple method to return an API response, formatted
* to match the requested format, with proper content-type and status code.
*
* @param array|string|null $data
* @param integer $status
* @param string $message
*
* @return mixed
*/
public function respond($data = null, int $status = null, string $message = '')
{
// If data is null and status code not provided, exit and bail
if ($data === null && $status === null)
{
$status = 404;
// Create the output var here in case of $this->response([]);
$output = null;
}
// If data is null but status provided, keep the output empty.
elseif ($data === null && is_numeric($status))
{
$output = null;
}
else
{
$status = empty($status) ? 200 : $status;
$output = $this->format($data);
}
if (! is_null($output))
{
if ($this->format === 'json')
{
return $this->response->setJSON($output)->setStatusCode($status, $message);
}
if ($this->format === 'xml')
{
return $this->response->setXML($output)->setStatusCode($status, $message);
}
}
return $this->response->setBody($output)->setStatusCode($status, $message);
}
//--------------------------------------------------------------------
/**
* Used for generic failures that no custom methods exist for.
*
* @param string|array $messages
* @param integer $status HTTP status code
* @param string|null $code Custom, API-specific, error code
* @param string $customMessage
*
* @return mixed
*/
public function fail($messages, int $status = 400, string $code = null, string $customMessage = '')
{
if (! is_array($messages))
{
$messages = ['error' => $messages];
}
$response = [
'status' => $status,
'error' => $code ?? $status,
'messages' => $messages,
];
return $this->respond($response, $status, $customMessage);
}
//--------------------------------------------------------------------
//--------------------------------------------------------------------
// Response Helpers
//--------------------------------------------------------------------
/**
* Used after successfully creating a new resource.
*
* @param mixed $data Data.
* @param string $message Message.
*
* @return mixed
*/
public function respondCreated($data = null, string $message = '')
{
return $this->respond($data, $this->codes['created'], $message);
}
//--------------------------------------------------------------------
/**
* Used after a resource has been successfully deleted.
*
* @param mixed $data Data.
* @param string $message Message.
*
* @return mixed
*/
public function respondDeleted($data = null, string $message = '')
{
return $this->respond($data, $this->codes['deleted'], $message);
}
/**
* Used after a resource has been successfully updated.
*
* @param mixed $data Data.
* @param string $message Message.
*
* @return mixed
*/
public function respondUpdated($data = null, string $message = '')
{
return $this->respond($data, $this->codes['updated'], $message);
}
//--------------------------------------------------------------------
/**
* Used after a command has been successfully executed but there is no
* meaningful reply to send back to the client.
*
* @param string $message Message.
*
* @return mixed
*/
public function respondNoContent(string $message = 'No Content')
{
return $this->respond(null, $this->codes['no_content'], $message);
}
//--------------------------------------------------------------------
/**
* Used when the client is either didn't send authorization information,
* or had bad authorization credentials. User is encouraged to try again
* with the proper information.
*
* @param string $description
* @param string $code
* @param string $message
*
* @return mixed
*/
public function failUnauthorized(string $description = 'Unauthorized', string $code = null, string $message = '')
{
return $this->fail($description, $this->codes['unauthorized'], $code, $message);
}
//--------------------------------------------------------------------
/**
* Used when access is always denied to this resource and no amount
* of trying again will help.
*
* @param string $description
* @param string $code
* @param string $message
*
* @return mixed
*/
public function failForbidden(string $description = 'Forbidden', string $code = null, string $message = '')
{
return $this->fail($description, $this->codes['forbidden'], $code, $message);
}
//--------------------------------------------------------------------
/**
* Used when a specified resource cannot be found.
*
* @param string $description
* @param string $code
* @param string $message
*
* @return mixed
*/
public function failNotFound(string $description = 'Not Found', string $code = null, string $message = '')
{
return $this->fail($description, $this->codes['resource_not_found'], $code, $message);
}
//--------------------------------------------------------------------
/**
* Used when the data provided by the client cannot be validated.
*
* @param string $description
* @param string $code
* @param string $message
*
* @return mixed
*
* @deprecated Use failValidationErrors instead
*/
public function failValidationError(string $description = 'Bad Request', string $code = null, string $message = '')
{
return $this->fail($description, $this->codes['invalid_data'], $code, $message);
}
/**
* Used when the data provided by the client cannot be validated on one or more fields.
*
* @param string|string[] $errors
* @param string|null $code
* @param string $message
*
* @return mixed
*/
public function failValidationErrors($errors, string $code = null, string $message = '')
{
return $this->fail($errors, $this->codes['invalid_data'], $code, $message);
}
//--------------------------------------------------------------------
/**
* Use when trying to create a new resource and it already exists.
*
* @param string $description
* @param string $code
* @param string $message
*
* @return mixed
*/
public function failResourceExists(string $description = 'Conflict', string $code = null, string $message = '')
{
return $this->fail($description, $this->codes['resource_exists'], $code, $message);
}
//--------------------------------------------------------------------
/**
* Use when a resource was previously deleted. This is different than
* Not Found, because here we know the data previously existed, but is now gone,
* where Not Found means we simply cannot find any information about it.
*
* @param string $description
* @param string $code
* @param string $message
*
* @return mixed
*/
public function failResourceGone(string $description = 'Gone', string $code = null, string $message = '')
{
return $this->fail($description, $this->codes['resource_gone'], $code, $message);
}
//--------------------------------------------------------------------
/**
* Used when the user has made too many requests for the resource recently.
*
* @param string $description
* @param string $code
* @param string $message
*
* @return mixed
*/
public function failTooManyRequests(string $description = 'Too Many Requests', string $code = null, string $message = '')
{
return $this->fail($description, $this->codes['too_many_requests'], $code, $message);
}
//--------------------------------------------------------------------
/**
* Used when there is a server error.
*
* @param string $description The error message to show the user.
* @param string|null $code A custom, API-specific, error code.
* @param string $message A custom "reason" message to return.
*
* @return Response The value of the Response's send() method.
*/
public function failServerError(string $description = 'Internal Server Error', string $code = null, string $message = ''): Response
{
return $this->fail($description, $this->codes['server_error'], $code, $message);
}
//--------------------------------------------------------------------
// Utility Methods
//--------------------------------------------------------------------
/**
* Handles formatting a response. Currently makes some heavy assumptions
* and needs updating! :)
*
* @param string|array|null $data
*
* @return string|null
*/
protected function format($data = null)
{
// If the data is a string, there's not much we can do to it...
if (is_string($data))
{
// The content type should be text/... and not application/...
$contentType = $this->response->getHeaderLine('Content-Type');
$contentType = str_replace('application/json', 'text/html', $contentType);
$contentType = str_replace('application/', 'text/', $contentType);
$this->response->setContentType($contentType);
$this->format = 'html';
return $data;
}
$format = Services::format();
$mime = "application/{$this->format}";
// Determine correct response type through content negotiation if not explicitly declared
if (empty($this->format) || ! in_array($this->format, ['json', 'xml'], true))
{
$mime = $this->request->negotiate('media', $format->getConfig()->supportedResponseFormats, false);
}
$this->response->setContentType($mime);
// if we don't have a formatter, make one
if (! isset($this->formatter))
{
// if no formatter, use the default
$this->formatter = $format->getFormatter($mime);
}
if ($mime !== 'application/json')
{
// Recursively convert objects into associative arrays
// Conversion not required for JSONFormatter
$data = json_decode(json_encode($data), true);
}
return $this->formatter->format($data);
}
/**
* Sets the format the response should be in.
*
* @param string $format
*
* @return $this
*/
public function setResponseFormat(string $format = null)
{
$this->format = strtolower($format);
return $this;
}
}
+372
View File
@@ -0,0 +1,372 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Autoloader;
use Composer\Autoload\ClassLoader;
use Config\Autoload;
use Config\Modules;
use InvalidArgumentException;
/**
* CodeIgniter Autoloader
*
* An autoloader that uses both PSR4 autoloading, and traditional classmaps.
*
* Given a foo-bar package of classes in the file system at the following paths:
* ```
* /path/to/packages/foo-bar/
* /src
* Baz.php # Foo\Bar\Baz
* Qux/
* Quux.php # Foo\Bar\Qux\Quux
* ```
* you can add the path to the configuration array that is passed in the constructor.
* The Config array consists of 2 primary keys, both of which are associative arrays:
* 'psr4', and 'classmap'.
* ```
* $Config = [
* 'psr4' => [
* 'Foo\Bar' => '/path/to/packages/foo-bar'
* ],
* 'classmap' => [
* 'MyClass' => '/path/to/class/file.php'
* ]
* ];
* ```
* Example:
* ```
* <?php
* // our configuration array
* $Config = [ ... ];
* $loader = new \CodeIgniter\Autoloader\Autoloader($Config);
*
* // register the autoloader
* $loader->register();
* ```
*/
class Autoloader
{
/**
* Stores namespaces as key, and path as values.
*
* @var array<string, array<string>>
*/
protected $prefixes = [];
/**
* Stores class name as key, and path as values.
*
* @var array<string, string>
*/
protected $classmap = [];
/**
* Stores files as a list.
*
* @var array<int, string>
*/
protected $files = [];
/**
* Reads in the configuration array (described above) and stores
* the valid parts that we'll need.
*
* @param Autoload $config
* @param Modules $modules
*
* @return $this
*/
public function initialize(Autoload $config, Modules $modules)
{
// We have to have one or the other, though we don't enforce the need
// to have both present in order to work.
if (empty($config->psr4) && empty($config->classmap))
{
throw new InvalidArgumentException('Config array must contain either the \'psr4\' key or the \'classmap\' key.');
}
if (isset($config->psr4))
{
$this->addNamespace($config->psr4);
}
if (isset($config->classmap))
{
$this->classmap = $config->classmap;
}
if (isset($config->files))
{
$this->files = $config->files;
}
// Should we load through Composer's namespaces, also?
if ($modules->discoverInComposer)
{
$this->discoverComposerNamespaces();
}
return $this;
}
/**
* Register the loader with the SPL autoloader stack.
*/
public function register()
{
// Prepend the PSR4 autoloader for maximum performance.
spl_autoload_register([$this, 'loadClass'], true, true); // @phpstan-ignore-line
// Now prepend another loader for the files in our class map.
spl_autoload_register([$this, 'loadClassmap'], true, true); // @phpstan-ignore-line
// Load our non-class files
foreach ($this->files as $file)
{
if (is_string($file))
{
$this->includeFile($file);
}
}
}
/**
* Registers namespaces with the autoloader.
*
* @param array|string $namespace
* @param string $path
*
* @return $this
*/
public function addNamespace($namespace, string $path = null)
{
if (is_array($namespace))
{
foreach ($namespace as $prefix => $path)
{
$prefix = trim($prefix, '\\');
if (is_array($path))
{
foreach ($path as $dir)
{
$this->prefixes[$prefix][] = rtrim($dir, '\\/') . DIRECTORY_SEPARATOR;
}
continue;
}
$this->prefixes[$prefix][] = rtrim($path, '\\/') . DIRECTORY_SEPARATOR;
}
}
else
{
$this->prefixes[trim($namespace, '\\')][] = rtrim($path, '\\/') . DIRECTORY_SEPARATOR;
}
return $this;
}
/**
* Get namespaces with prefixes as keys and paths as values.
*
* If a prefix param is set, returns only paths to the given prefix.
*
* @param string|null $prefix
*
* @return array
*/
public function getNamespace(string $prefix = null)
{
if ($prefix === null)
{
return $this->prefixes;
}
return $this->prefixes[trim($prefix, '\\')] ?? [];
}
/**
* Removes a single namespace from the psr4 settings.
*
* @param string $namespace
*
* @return $this
*/
public function removeNamespace(string $namespace)
{
if (isset($this->prefixes[trim($namespace, '\\')]))
{
unset($this->prefixes[trim($namespace, '\\')]);
}
return $this;
}
/**
* Load a class using available class mapping.
*
* @param string $class
*
* @return string|false
*/
public function loadClassmap(string $class)
{
$file = $this->classmap[$class] ?? '';
if (is_string($file) && $file !== '')
{
return $this->includeFile($file);
}
return false;
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully qualified class name.
*
* @return string|false The mapped file on success, or boolean false
* on failure.
*/
public function loadClass(string $class)
{
$class = trim($class, '\\');
$class = str_ireplace('.php', '', $class);
return $this->loadInNamespace($class);
}
/**
* Loads the class file for a given class name.
*
* @param string $class The fully-qualified class name
*
* @return string|false The mapped file name on success, or boolean false on fail
*/
protected function loadInNamespace(string $class)
{
if (strpos($class, '\\') === false)
{
return false;
}
foreach ($this->prefixes as $namespace => $directories)
{
foreach ($directories as $directory)
{
$directory = rtrim($directory, '\\/');
if (strpos($class, $namespace) === 0)
{
$filePath = $directory . str_replace('\\', DIRECTORY_SEPARATOR, substr($class, strlen($namespace))) . '.php';
$filename = $this->includeFile($filePath);
if ($filename)
{
return $filename;
}
}
}
}
// never found a mapped file
return false;
}
/**
* A central way to include a file. Split out primarily for testing purposes.
*
* @param string $file
*
* @return string|false The filename on success, false if the file is not loaded
*/
protected function includeFile(string $file)
{
$file = $this->sanitizeFilename($file);
if (is_file($file))
{
include_once $file;
return $file;
}
return false;
}
/**
* Sanitizes a filename, replacing spaces with dashes.
*
* Removes special characters that are illegal in filenames on certain
* operating systems and special characters requiring special escaping
* to manipulate at the command line. Replaces spaces and consecutive
* dashes with a single dash. Trim period, dash and underscore from beginning
* and end of filename.
*
* @param string $filename
*
* @return string The sanitized filename
*/
public function sanitizeFilename(string $filename): string
{
// Only allow characters deemed safe for POSIX portable filenames.
// Plus the forward slash for directory separators since this might be a path.
// http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_278
// Modified to allow backslash and colons for on Windows machines.
$filename = preg_replace('/[^0-9\p{L}\s\/\-\_\.\:\\\\]/u', '', $filename);
// Clean up our filename edges.
$filename = trim($filename, '.-_');
return $filename;
}
/**
* Locates autoload information from Composer, if available.
*
* @return void
*/
protected function discoverComposerNamespaces()
{
if (! is_file(COMPOSER_PATH))
{
return;
}
/**
* @var ClassLoader $composer
*/
$composer = include COMPOSER_PATH;
$paths = $composer->getPrefixesPsr4();
$classes = $composer->getClassMap();
unset($composer);
// Get rid of CodeIgniter so we don't have duplicates
if (isset($paths['CodeIgniter\\']))
{
unset($paths['CodeIgniter\\']);
}
$newPaths = [];
foreach ($paths as $key => $value)
{
// Composer stores namespaces with trailing slash. We don't.
$newPaths[rtrim($key, '\\ ')] = $value;
}
$this->prefixes = array_merge($this->prefixes, $newPaths);
$this->classmap = array_merge($this->classmap, $classes);
}
}
+452
View File
@@ -0,0 +1,452 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Autoloader;
/**
* Class FileLocator
*
* Allows loading non-class files in a namespaced manner.
* Works with Helpers, Views, etc.
*/
class FileLocator
{
/**
* The Autoloader to use.
*
* @var Autoloader
*/
protected $autoloader;
/**
* Constructor
*
* @param Autoloader $autoloader
*/
public function __construct(Autoloader $autoloader)
{
$this->autoloader = $autoloader;
}
/**
* Attempts to locate a file by examining the name for a namespace
* and looking through the PSR-4 namespaced files that we know about.
*
* @param string $file The namespaced file to locate
* @param string|null $folder The folder within the namespace that we should look for the file.
* @param string $ext The file extension the file should have.
*
* @return string|false The path to the file, or false if not found.
*/
public function locateFile(string $file, string $folder = null, string $ext = 'php')
{
$file = $this->ensureExt($file, $ext);
// Clears the folder name if it is at the beginning of the filename
if (! empty($folder) && strpos($file, $folder) === 0)
{
$file = substr($file, strlen($folder . '/'));
}
// Is not namespaced? Try the application folder.
if (strpos($file, '\\') === false)
{
return $this->legacyLocate($file, $folder);
}
// Standardize slashes to handle nested directories.
$file = strtr($file, '/', '\\');
$segments = explode('\\', $file);
// The first segment will be empty if a slash started the filename.
if (empty($segments[0]))
{
unset($segments[0]);
}
$paths = [];
$prefix = '';
$filename = '';
// Namespaces always comes with arrays of paths
$namespaces = $this->autoloader->getNamespace();
while (! empty($segments))
{
$prefix .= empty($prefix) ? array_shift($segments) : '\\' . array_shift($segments);
if (empty($namespaces[$prefix]))
{
continue;
}
$paths = $namespaces[$prefix];
$filename = implode('/', $segments);
break;
}
// if no namespaces matched then quit
if (empty($paths))
{
return false;
}
// Check each path in the namespace
foreach ($paths as $path)
{
// Ensure trailing slash
$path = rtrim($path, '/') . '/';
// If we have a folder name, then the calling function
// expects this file to be within that folder, like 'Views',
// or 'libraries'.
if (! empty($folder) && strpos($path . $filename, '/' . $folder . '/') === false)
{
$path .= trim($folder, '/') . '/';
}
$path .= $filename;
if (is_file($path))
{
return $path;
}
}
return false;
}
/**
* Examines a file and returns the fully qualified domain name.
*
* @param string $file
*
* @return string
*/
public function getClassname(string $file) : string
{
$php = file_get_contents($file);
$tokens = token_get_all($php);
$dlm = false;
$namespace = '';
$className = '';
foreach ($tokens as $i => $token)
{
if ($i < 2)
{
continue;
}
if ((isset($tokens[$i - 2][1]) && ($tokens[$i - 2][1] === 'phpnamespace' || $tokens[$i - 2][1] === 'namespace')) || ($dlm && $tokens[$i - 1][0] === T_NS_SEPARATOR && $token[0] === T_STRING))
{
if (! $dlm)
{
$namespace = 0;
}
if (isset($token[1]))
{
$namespace = $namespace ? $namespace . '\\' . $token[1] : $token[1];
$dlm = true;
}
}
elseif ($dlm && ($token[0] !== T_NS_SEPARATOR) && ($token[0] !== T_STRING))
{
$dlm = false;
}
if (($tokens[$i - 2][0] === T_CLASS || (isset($tokens[$i - 2][1]) && $tokens[$i - 2][1] === 'phpclass'))
&& $tokens[$i - 1][0] === T_WHITESPACE
&& $token[0] === T_STRING)
{
$className = $token[1];
break;
}
}
if (empty($className))
{
return '';
}
return $namespace . '\\' . $className;
}
/**
* Searches through all of the defined namespaces looking for a file.
* Returns an array of all found locations for the defined file.
*
* Example:
*
* $locator->search('Config/Routes.php');
* // Assuming PSR4 namespaces include foo and bar, might return:
* [
* 'app/Modules/foo/Config/Routes.php',
* 'app/Modules/bar/Config/Routes.php',
* ]
*
* @param string $path
* @param string $ext
* @param boolean $prioritizeApp
*
* @return array
*/
public function search(string $path, string $ext = 'php', bool $prioritizeApp = true): array
{
$path = $this->ensureExt($path, $ext);
$foundPaths = [];
$appPaths = [];
foreach ($this->getNamespaces() as $namespace)
{
if (isset($namespace['path']) && is_file($namespace['path'] . $path))
{
$fullPath = $namespace['path'] . $path;
$fullPath = realpath($fullPath) ?: $fullPath;
if ($prioritizeApp)
{
$foundPaths[] = $fullPath;
}
elseif (strpos($fullPath, APPPATH) === 0)
{
$appPaths[] = $fullPath;
}
else
{
$foundPaths[] = $fullPath;
}
}
}
if (! $prioritizeApp && ! empty($appPaths))
{
$foundPaths = array_merge($foundPaths, $appPaths);
}
// Remove any duplicates
$foundPaths = array_unique($foundPaths);
return $foundPaths;
}
/**
* Ensures a extension is at the end of a filename
*
* @param string $path
* @param string $ext
*
* @return string
*/
protected function ensureExt(string $path, string $ext): string
{
if ($ext)
{
$ext = '.' . $ext;
if (substr($path, -strlen($ext)) !== $ext)
{
$path .= $ext;
}
}
return $path;
}
/**
* Return the namespace mappings we know about.
*
* @return array|string
*/
protected function getNamespaces()
{
$namespaces = [];
// Save system for last
$system = [];
foreach ($this->autoloader->getNamespace() as $prefix => $paths)
{
foreach ($paths as $path)
{
if ($prefix === 'CodeIgniter')
{
$system = [
'prefix' => $prefix,
'path' => rtrim($path, '\\/') . DIRECTORY_SEPARATOR,
];
continue;
}
$namespaces[] = [
'prefix' => $prefix,
'path' => rtrim($path, '\\/') . DIRECTORY_SEPARATOR,
];
}
}
$namespaces[] = $system;
return $namespaces;
}
/**
* Find the qualified name of a file according to
* the namespace of the first matched namespace path.
*
* @param string $path
*
* @return string|false The qualified name or false if the path is not found
*/
public function findQualifiedNameFromPath(string $path)
{
$path = realpath($path) ?: $path;
if (! is_file($path))
{
return false;
}
foreach ($this->getNamespaces() as $namespace)
{
$namespace['path'] = realpath($namespace['path']) ?: $namespace['path'];
if (empty($namespace['path']))
{
continue;
}
if (mb_strpos($path, $namespace['path']) === 0)
{
$className = '\\' . $namespace['prefix'] . '\\' .
ltrim(str_replace('/', '\\', mb_substr(
$path, mb_strlen($namespace['path']))
), '\\');
// Remove the file extension (.php)
$className = mb_substr($className, 0, -4);
// Check if this exists
if (class_exists($className))
{
return $className;
}
}
}
return false;
}
/**
* Scans the defined namespaces, returning a list of all files
* that are contained within the subpath specified by $path.
*
* @param string $path
*
* @return array
*/
public function listFiles(string $path): array
{
if (empty($path))
{
return [];
}
$files = [];
helper('filesystem');
foreach ($this->getNamespaces() as $namespace)
{
$fullPath = $namespace['path'] . $path;
$fullPath = realpath($fullPath) ?: $fullPath;
if (! is_dir($fullPath))
{
continue;
}
$tempFiles = get_filenames($fullPath, true);
if (! empty($tempFiles))
{
$files = array_merge($files, $tempFiles);
}
}
return $files;
}
/**
* Scans the provided namespace, returning a list of all files
* that are contained within the subpath specified by $path.
*
* @param string $prefix
* @param string $path
*
* @return array
*/
public function listNamespaceFiles(string $prefix, string $path): array
{
if (empty($path) || empty($prefix))
{
return [];
}
$files = [];
helper('filesystem');
// autoloader->getNamespace($prefix) returns an array of paths for that namespace
foreach ($this->autoloader->getNamespace($prefix) as $namespacePath)
{
$fullPath = rtrim($namespacePath, '/') . '/' . $path;
$fullPath = realpath($fullPath) ?: $fullPath;
if (! is_dir($fullPath))
{
continue;
}
$tempFiles = get_filenames($fullPath, true);
if (! empty($tempFiles))
{
$files = array_merge($files, $tempFiles);
}
}
return $files;
}
/**
* Checks the app folder to see if the file can be found.
* Only for use with filenames that DO NOT include namespacing.
*
* @param string $file
* @param string|null $folder
*
* @return string|false The path to the file, or false if not found.
*/
protected function legacyLocate(string $file, string $folder = null)
{
$path = APPPATH . (empty($folder) ? $file : $folder . '/' . $file);
$path = realpath($path) ?: $path;
if (is_file($path))
{
return $path;
}
return false;
}
}
+1812
View File
File diff suppressed because it is too large Load Diff
+261
View File
@@ -0,0 +1,261 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\CLI;
use Psr\Log\LoggerInterface;
use ReflectionException;
use Throwable;
/**
* BaseCommand is the base class used in creating CLI commands.
*
* @property string $group
* @property string $name
* @property string $usage
* @property string $description
* @property array $options
* @property array $arguments
* @property LoggerInterface $logger
* @property Commands $commands
*/
abstract class BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group;
/**
* The Command's name
*
* @var string
*/
protected $name;
/**
* the Command's usage description
*
* @var string
*/
protected $usage;
/**
* the Command's short description
*
* @var string
*/
protected $description;
/**
* the Command's options description
*
* @var array
*/
protected $options = [];
/**
* the Command's Arguments description
*
* @var array
*/
protected $arguments = [];
/**
* The Logger to use for a command
*
* @var LoggerInterface
*/
protected $logger;
/**
* Instance of Commands so
* commands can call other commands.
*
* @var Commands
*/
protected $commands;
/**
* BaseCommand constructor.
*
* @param LoggerInterface $logger
* @param Commands $commands
*/
public function __construct(LoggerInterface $logger, Commands $commands)
{
$this->logger = $logger;
$this->commands = $commands;
}
/**
* Actually execute a command.
* This has to be over-ridden in any concrete implementation.
*
* @param array $params
*/
abstract public function run(array $params);
/**
* Can be used by a command to run other commands.
*
* @param string $command
* @param array $params
*
* @return mixed
* @throws ReflectionException
*/
protected function call(string $command, array $params = [])
{
return $this->commands->run($command, $params);
}
/**
* A simple method to display an error with line/file, in child commands.
*
* @param Throwable $e
*/
protected function showError(Throwable $e)
{
$exception = $e;
$message = $e->getMessage();
require APPPATH . 'Views/errors/cli/error_exception.php';
}
/**
* Show Help includes (Usage, Arguments, Description, Options).
*/
public function showHelp()
{
CLI::write(lang('CLI.helpUsage'), 'yellow');
if (! empty($this->usage))
{
$usage = $this->usage;
}
else
{
$usage = $this->name;
if (! empty($this->arguments))
{
$usage .= ' [arguments]';
}
}
CLI::write($this->setPad($usage, 0, 0, 2));
if (! empty($this->description))
{
CLI::newLine();
CLI::write(lang('CLI.helpDescription'), 'yellow');
CLI::write($this->setPad($this->description, 0, 0, 2));
}
if (! empty($this->arguments))
{
CLI::newLine();
CLI::write(lang('CLI.helpArguments'), 'yellow');
$length = max(array_map('strlen', array_keys($this->arguments)));
foreach ($this->arguments as $argument => $description)
{
CLI::write(CLI::color($this->setPad($argument, $length, 2, 2), 'green') . $description);
}
}
if (! empty($this->options))
{
CLI::newLine();
CLI::write(lang('CLI.helpOptions'), 'yellow');
$length = max(array_map('strlen', array_keys($this->options)));
foreach ($this->options as $option => $description)
{
CLI::write(CLI::color($this->setPad($option, $length, 2, 2), 'green') . $description);
}
}
}
/**
* Pads our string out so that all titles are the same length to nicely line up descriptions.
*
* @param string $item
* @param integer $max
* @param integer $extra How many extra spaces to add at the end
* @param integer $indent
*
* @return string
*/
public function setPad(string $item, int $max, int $extra = 2, int $indent = 0): string
{
$max += $extra + $indent;
return str_pad(str_repeat(' ', $indent) . $item, $max);
}
/**
* Get pad for $key => $value array output
*
* @param array $array
* @param integer $pad
*
* @return integer
*
* @deprecated Use setPad() instead.
*
* @codeCoverageIgnore
*/
public function getPad(array $array, int $pad): int
{
$max = 0;
foreach (array_keys($array) as $key)
{
$max = max($max, strlen($key));
}
return $max + $pad;
}
/**
* Makes it simple to access our protected properties.
*
* @param string $key
*
* @return mixed
*/
public function __get(string $key)
{
if (isset($this->$key))
{
return $this->$key;
}
return null;
}
/**
* Makes it simple to check our protected properties.
*
* @param string $key
*
* @return boolean
*/
public function __isset(string $key): bool
{
return isset($this->$key);
}
}
+1185
View File
File diff suppressed because it is too large Load Diff
+83
View File
@@ -0,0 +1,83 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\CLI;
use CodeIgniter\Controller;
use Config\Services;
use ReflectionException;
/**
* Command runner
*/
class CommandRunner extends Controller
{
/**
* Instance of class managing the collection of commands
*
* @var Commands
*/
protected $commands;
/**
* Constructor
*/
public function __construct()
{
$this->commands = Services::commands();
}
/**
* We map all un-routed CLI methods through this function
* so we have the chance to look for a Command first.
*
* @param string $method
* @param array ...$params
*
* @return mixed
* @throws ReflectionException
*/
public function _remap($method, ...$params)
{
// The first param is usually empty, so scrap it.
if (empty($params[0]))
{
array_shift($params);
}
return $this->index($params);
}
/**
* Default command.
*
* @param array $params
*
* @return mixed
* @throws ReflectionException
*/
public function index(array $params)
{
$command = array_shift($params) ?? 'list';
return $this->commands->run($command, $params);
}
/**
* Allows access to the current commands that have been found.
*
* @return array
*/
public function getCommands(): array
{
return $this->commands->getCommands();
}
}
+213
View File
@@ -0,0 +1,213 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\CLI;
use CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Log\Logger;
use ReflectionClass;
use ReflectionException;
/**
* Core functionality for running, listing, etc commands.
*/
class Commands
{
/**
* The found commands.
*
* @var array
*/
protected $commands = [];
/**
* Logger instance.
*
* @var Logger
*/
protected $logger;
/**
* Constructor
*
* @param Logger|null $logger
*/
public function __construct($logger = null)
{
$this->logger = $logger ?? service('logger');
$this->discoverCommands();
}
/**
* Runs a command given
*
* @param string $command
* @param array $params
*/
public function run(string $command, array $params)
{
if (! $this->verifyCommand($command, $this->commands))
{
return;
}
// The file would have already been loaded during the
// createCommandList function...
$className = $this->commands[$command]['class'];
$class = new $className($this->logger, $this);
return $class->run($params);
}
/**
* Provide access to the list of commands.
*
* @return array
*/
public function getCommands()
{
return $this->commands;
}
/**
* Discovers all commands in the framework and within user code,
* and collects instances of them to work with.
*
* @return void
*/
public function discoverCommands()
{
if ($this->commands !== [])
{
return;
}
/** @var FileLocator $locator */
$locator = service('locator');
$files = $locator->listFiles('Commands/');
// If no matching command files were found, bail
// This should never happen in unit testing.
if ($files === [])
{
return; // @codeCoverageIgnore
}
// Loop over each file checking to see if a command with that
// alias exists in the class.
foreach ($files as $file)
{
$className = $locator->findQualifiedNameFromPath($file);
if (empty($className) || ! class_exists($className))
{
continue;
}
try
{
$class = new ReflectionClass($className);
if (! $class->isInstantiable() || ! $class->isSubclassOf(BaseCommand::class))
{
continue;
}
/** @var BaseCommand $class */
$class = new $className($this->logger, $this);
if (isset($class->group))
{
$this->commands[$class->name] = [
'class' => $className,
'file' => $file,
'group' => $class->group,
'description' => $class->description,
];
}
unset($class);
}
catch (ReflectionException $e)
{
$this->logger->error($e->getMessage());
}
}
asort($this->commands);
}
/**
* Verifies if the command being sought is found
* in the commands list.
*
* @param string $command
* @param array $commands
*
* @return boolean
*/
public function verifyCommand(string $command, array $commands): bool
{
if (isset($commands[$command]))
{
return true;
}
$message = lang('CLI.commandNotFound', [$command]);
if ($alternatives = $this->getCommandAlternatives($command, $commands))
{
if (count($alternatives) === 1)
{
$message .= "\n\n" . lang('CLI.altCommandSingular') . "\n ";
}
else
{
$message .= "\n\n" . lang('CLI.altCommandPlural') . "\n ";
}
$message .= implode("\n ", $alternatives);
}
CLI::error($message);
CLI::newLine();
return false;
}
/**
* Finds alternative of `$name` among collection
* of commands.
*
* @param string $name
* @param array $collection
*
* @return array
*/
protected function getCommandAlternatives(string $name, array $collection): array
{
$alternatives = [];
foreach (array_keys($collection) as $commandName)
{
$lev = levenshtein($name, $commandName);
if ($lev <= strlen($commandName) / 3 || strpos($commandName, $name) !== false)
{
$alternatives[$commandName] = $lev;
}
}
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
return array_keys($alternatives);
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\CLI;
use CodeIgniter\CodeIgniter;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;
use Exception;
/**
* Console
*/
class Console
{
/**
* Main CodeIgniter instance.
*
* @var CodeIgniter
*/
protected $app;
//--------------------------------------------------------------------
/**
* Console constructor.
*
* @param CodeIgniter $app
*/
public function __construct(CodeIgniter $app)
{
$this->app = $app;
}
//--------------------------------------------------------------------
/**
* Runs the current command discovered on the CLI.
*
* @param boolean $useSafeOutput
*
* @return RequestInterface|Response|ResponseInterface|mixed
* @throws Exception
*/
public function run(bool $useSafeOutput = false)
{
$path = CLI::getURI() ?: 'list';
// Set the path for the application to route to.
$this->app->setPath("ci{$path}");
return $this->app->useSafeOutput($useSafeOutput)->run();
}
//--------------------------------------------------------------------
/**
* Displays basic information about the Console.
*
* @param boolean $suppress
*/
public function showHeader(bool $suppress = false)
{
if ($suppress)
{
return;
}
CLI::write(sprintf('CodeIgniter v%s Command Line Tool - Server Time: %s UTC%s', CodeIgniter::CI_VERSION, date('Y-m-d H:i:s'), date('P')), 'green');
CLI::newLine();
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\CLI\Exceptions;
use CodeIgniter\Exceptions\DebugTraceableTrait;
use RuntimeException;
/**
* CLIException
*/
class CLIException extends RuntimeException
{
use DebugTraceableTrait;
/**
* Thrown when `$color` specified for `$type` is not within the
* allowed list of colors.
*
* @param string $type
* @param string $color
*
* @return CLIException
*/
public static function forInvalidColor(string $type, string $color)
{
return new static(lang('CLI.invalidColor', [$type, $color]));
}
}
+410
View File
@@ -0,0 +1,410 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\CLI;
use Config\Services;
use Throwable;
/**
* GeneratorTrait contains a collection of methods
* to build the commands that generates a file.
*/
trait GeneratorTrait
{
/**
* Component Name
*
* @var string
*/
protected $component;
/**
* File directory
*
* @var string
*/
protected $directory;
/**
* View template name
*
* @var string
*/
protected $template;
/**
* Language string key for required class names.
*
* @var string
*/
protected $classNameLang = '';
/**
* Whether to require class name.
*
* @internal
*
* @var boolean
*/
private $hasClassName = true;
/**
* Whether to sort class imports.
*
* @internal
*
* @var boolean
*/
private $sortImports = true;
/**
* Whether the `--suffix` option has any effect.
*
* @internal
*
* @var boolean
*/
private $enabledSuffixing = true;
/**
* The params array for easy access by other methods.
*
* @internal
*
* @var array
*/
private $params = [];
/**
* Execute the command.
*
* @param array $params
*
* @return void
*/
protected function execute(array $params): void
{
$this->params = $params;
if ($this->getOption('namespace') === 'CodeIgniter')
{
// @codeCoverageIgnoreStart
CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow');
CLI::newLine();
if (CLI::prompt('Are you sure you want to continue?', ['y', 'n'], 'required') === 'n')
{
CLI::newLine();
CLI::write(lang('CLI.generator.cancelOperation'), 'yellow');
CLI::newLine();
return;
}
CLI::newLine();
// @codeCoverageIgnoreEnd
}
// Get the fully qualified class name from the input.
$class = $this->qualifyClassName();
// Get the file path from class name.
$path = $this->buildPath($class);
// Check if path is empty.
if (empty($path))
{
return;
}
$isFile = is_file($path);
// Overwriting files unknowingly is a serious annoyance, So we'll check if
// we are duplicating things, If 'force' option is not supplied, we bail.
if (! $this->getOption('force') && $isFile)
{
CLI::error(lang('CLI.generator.fileExist', [clean_path($path)]), 'light_gray', 'red');
CLI::newLine();
return;
}
// Check if the directory to save the file is existing.
$dir = dirname($path);
if (! is_dir($dir))
{
mkdir($dir, 0755, true);
}
helper('filesystem');
// Build the class based on the details we have, We'll be getting our file
// contents from the template, and then we'll do the necessary replacements.
if (! write_file($path, $this->buildContent($class)))
{
// @codeCoverageIgnoreStart
CLI::error(lang('CLI.generator.fileError', [clean_path($path)]), 'light_gray', 'red');
CLI::newLine();
return;
// @codeCoverageIgnoreEnd
}
if ($this->getOption('force') && $isFile)
{
CLI::write(lang('CLI.generator.fileOverwrite', [clean_path($path)]), 'yellow');
CLI::newLine();
return;
}
CLI::write(lang('CLI.generator.fileCreate', [clean_path($path)]), 'green');
CLI::newLine();
}
/**
* Prepare options and do the necessary replacements.
*
* @param string $class
*
* @return string
*/
protected function prepare(string $class): string
{
return $this->parseTemplate($class);
}
/**
* Change file basename before saving.
*
* Useful for components where the file name has a date.
*
* @param string $filename
*
* @return string
*/
protected function basename(string $filename): string
{
return basename($filename);
}
/**
* Parses the class name and checks if it is already qualified.
*
* @return string
*/
protected function qualifyClassName(): string
{
// Gets the class name from input.
$class = $this->params[0] ?? CLI::getSegment(2);
if (is_null($class) && $this->hasClassName)
{
// @codeCoverageIgnoreStart
$nameLang = $this->classNameLang ?: 'CLI.generator.className.default';
$class = CLI::prompt(lang($nameLang), null, 'required');
CLI::newLine();
// @codeCoverageIgnoreEnd
}
helper('inflector');
$component = singular($this->component);
/**
* @see https://regex101.com/r/a5KNCR/1
*/
$pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)/i', $component);
if (preg_match($pattern, $class, $matches) === 1)
{
$class = $matches[1] . ucfirst($matches[2]);
}
if ($this->enabledSuffixing && $this->getOption('suffix') && ! strripos($class, $component))
{
$class .= ucfirst($component);
}
// Trims input, normalize separators, and ensure that all paths are in Pascalcase.
$class = ltrim(implode('\\', array_map('pascalize', explode('\\', str_replace('/', '\\', trim($class))))), '\\/');
// Gets the namespace from input.
$namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\');
if (strncmp($class, $namespace, strlen($namespace)) === 0)
{
return $class; // @codeCoverageIgnore
}
return $namespace . '\\' . $this->directory . '\\' . str_replace('/', '\\', $class);
}
/**
* Gets the generator view as defined in the `Config\Generators::$views`,
* with fallback to `$template` when the defined view does not exist.
*
* @param array $data Data to be passed to the view.
*
* @return string
*/
protected function renderTemplate(array $data = []): string
{
try
{
return view(config('Generators')->views[$this->name], $data, ['debug' => false]);
}
catch (Throwable $e)
{
log_message('error', $e->getMessage());
return view("CodeIgniter\Commands\Generators\Views\\{$this->template}", $data, ['debug' => false]);
}
}
/**
* Performs pseudo-variables contained within view file.
*
* @param string $class
* @param array $search
* @param array $replace
* @param array $data
*
* @return string
*/
protected function parseTemplate(string $class, array $search = [], array $replace = [], array $data = []): string
{
// Retrieves the namespace part from the fully qualified class name.
$namespace = trim(implode('\\', array_slice(explode('\\', $class), 0, -1)), '\\');
$search[] = '<@php';
$search[] = '{namespace}';
$search[] = '{class}';
$replace[] = '<?php';
$replace[] = $namespace;
$replace[] = str_replace($namespace . '\\', '', $class);
return str_replace($search, $replace, $this->renderTemplate($data));
}
/**
* Builds the contents for class being generated, doing all
* the replacements necessary, and alphabetically sorts the
* imports for a given template.
*
* @param string $class
*
* @return string
*/
protected function buildContent(string $class): string
{
$template = $this->prepare($class);
if ($this->sortImports && preg_match('/(?P<imports>(?:^use [^;]+;$\n?)+)/m', $template, $match))
{
$imports = explode("\n", trim($match['imports']));
sort($imports);
return str_replace(trim($match['imports']), implode("\n", $imports), $template);
}
return $template;
}
/**
* Builds the file path from the class name.
*
* @param string $class
*
* @return string
*/
protected function buildPath(string $class): string
{
$namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\');
// Check if the namespace is actually defined and we are not just typing gibberish.
$base = Services::autoloader()->getNamespace($namespace);
if (! $base = reset($base))
{
CLI::error(lang('CLI.namespaceNotDefined', [$namespace]), 'light_gray', 'red');
CLI::newLine();
return '';
}
$base = realpath($base) ?: $base;
$file = $base . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, trim(str_replace($namespace . '\\', '', $class), '\\')) . '.php';
return implode(DIRECTORY_SEPARATOR, array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, -1)) . DIRECTORY_SEPARATOR . $this->basename($file);
}
/**
* Allows child generators to modify the internal `$hasClassName` flag.
*
* @param boolean $hasClassName
*
* @return $this
*/
protected function setHasClassName(bool $hasClassName)
{
$this->hasClassName = $hasClassName;
return $this;
}
/**
* Allows child generators to modify the internal `$sortImports` flag.
*
* @param boolean $sortImports
*
* @return $this
*/
protected function setSortImports(bool $sortImports)
{
$this->sortImports = $sortImports;
return $this;
}
/**
* Allows child generators to modify the internal `$enabledSuffixing` flag.
*
* @param boolean $enabledSuffixing
*
* @return $this
*/
protected function setEnabledSuffixing(bool $enabledSuffixing)
{
$this->enabledSuffixing = $enabledSuffixing;
return $this;
}
/**
* Gets a single command-line option. Returns TRUE if the option exists,
* but doesn't have a value, and is simply acting as a flag.
*
* @param string $name
*
* @return mixed
*/
protected function getOption(string $name)
{
if (! array_key_exists($name, $this->params))
{
return CLI::getOption($name);
}
return is_null($this->params[$name]) ? true : $this->params[$name];
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache;
use CodeIgniter\Cache\Exceptions\CacheException;
use CodeIgniter\Exceptions\CriticalError;
use Config\Cache;
/**
* Class Cache
*
* A factory for loading the desired
*/
class CacheFactory
{
/**
* Attempts to create the desired cache handler, based upon the
*
* @param Cache $config
* @param string|null $handler
* @param string|null $backup
*
* @return CacheInterface
*/
public static function getHandler(Cache $config, string $handler = null, string $backup = null)
{
if (! isset($config->validHandlers) || ! is_array($config->validHandlers))
{
throw CacheException::forInvalidHandlers();
}
if (! isset($config->handler) || ! isset($config->backupHandler))
{
throw CacheException::forNoBackup();
}
$handler = ! empty($handler) ? $handler : $config->handler;
$backup = ! empty($backup) ? $backup : $config->backupHandler;
if (! array_key_exists($handler, $config->validHandlers) || ! array_key_exists($backup, $config->validHandlers))
{
throw CacheException::forHandlerNotFound();
}
// Get an instance of our handler.
$adapter = new $config->validHandlers[$handler]($config);
if (! $adapter->isSupported())
{
$adapter = new $config->validHandlers[$backup]($config);
if (! $adapter->isSupported())
{
// Log stuff here, don't throw exception. No need to raise a fuss.
// Fall back to the dummy adapter.
$adapter = new $config->validHandlers['dummy']();
}
}
// If $adapter->initialization throws a CriticalError exception, we will attempt to
// use the $backup handler, if that also fails, we resort to the dummy handler.
try
{
$adapter->initialize();
}
catch (CriticalError $e)
{
// log the fact that an exception occurred as well what handler we are resorting to
log_message('critical', $e->getMessage() . ' Resorting to using ' . $backup . ' handler.');
// get the next best cache handler (or dummy if the $backup also fails)
$adapter = self::getHandler($config, $backup, 'dummy');
}
return $adapter;
}
//--------------------------------------------------------------------
}
+128
View File
@@ -0,0 +1,128 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache;
/**
* Cache interface
*/
interface CacheInterface
{
/**
* Takes care of any handler-specific setup that must be done.
*/
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 integer $ttl Time To Live, in seconds (default 60)
*
* @return boolean 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 boolean Success or failure
*/
public function delete(string $key);
//--------------------------------------------------------------------
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return mixed
*/
public function increment(string $key, int $offset = 1);
//--------------------------------------------------------------------
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return mixed
*/
public function decrement(string $key, int $offset = 1);
//--------------------------------------------------------------------
/**
* Will delete all items in the entire cache.
*
* @return boolean 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 mixed
*/
public function getCacheInfo();
//--------------------------------------------------------------------
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return array|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.
*
* @return boolean
*/
public function isSupported(): bool;
//--------------------------------------------------------------------
}
@@ -0,0 +1,66 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Exceptions;
use CodeIgniter\Exceptions\DebugTraceableTrait;
use CodeIgniter\Exceptions\ExceptionInterface;
use RuntimeException;
/**
* CacheException
*/
class CacheException extends RuntimeException implements ExceptionInterface
{
use DebugTraceableTrait;
/**
* Thrown when handler has no permission to write cache.
*
* @param string $path
*
* @return CacheException
*/
public static function forUnableToWrite(string $path)
{
return new static(lang('Cache.unableToWrite', [$path]));
}
/**
* Thrown when an unrecognized handler is used.
*
* @return CacheException
*/
public static function forInvalidHandlers()
{
return new static(lang('Cache.invalidHandlers'));
}
/**
* Thrown when no backup handler is setup in config.
*
* @return CacheException
*/
public static function forNoBackup()
{
return new static(lang('Cache.noBackup'));
}
/**
* Thrown when specified handler was not found.
*
* @return CacheException
*/
public static function forHandlerNotFound()
{
return new static(lang('Cache.handlerNotFound'));
}
}
@@ -0,0 +1,24 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Exceptions;
/**
* Provides a domain-level interface for broad capture
* of all framework-related exceptions.
*
* catch (\CodeIgniter\Cache\Exceptions\ExceptionInterface) { ... }
*
* @deprecated 4.1.2
*/
interface ExceptionInterface
{
}
+110
View File
@@ -0,0 +1,110 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Handlers;
use Closure;
use CodeIgniter\Cache\CacheInterface;
use Exception;
use InvalidArgumentException;
/**
* Base class for cache handling
*/
abstract class BaseHandler implements CacheInterface
{
/**
* Reserved characters that cannot be used in a key or tag.
* From https://github.com/symfony/cache-contracts/blob/c0446463729b89dd4fa62e9aeecc80287323615d/ItemInterface.php#L43
*/
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 string $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.');
}
if (strpbrk($key, self::RESERVED_CHARACTERS) !== false)
{
throw new InvalidArgumentException('Cache key contains reserved characters ' . self::RESERVED_CHARACTERS);
}
// 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 integer $ttl Time to live
* @param Closure $callback Callback return value
*
* @return mixed
*/
public function remember(string $key, int $ttl, Closure $callback)
{
$value = $this->get($key);
if (! is_null($value))
{
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
*
* @throws Exception
*/
public function deleteMatching(string $pattern)
{
throw new Exception('The deleteMatching method is not implemented.');
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Handlers;
use Closure;
/**
* Dummy cache handler
*/
class DummyHandler extends BaseHandler
{
/**
* Takes care of any handler-specific setup that must be done.
*/
public function initialize()
{
}
//--------------------------------------------------------------------
/**
* Attempts to fetch an item from the cache store.
*
* @param string $key Cache item name
*
* @return null
*/
public function get(string $key)
{
return null;
}
//--------------------------------------------------------------------
/**
* Get an item from the cache, or execute the given Closure and store the result.
*
* @param string $key Cache item name
* @param integer $ttl Time to live
* @param Closure $callback Callback return value
*
* @return null
*/
public function remember(string $key, int $ttl, Closure $callback)
{
return null;
}
//--------------------------------------------------------------------
/**
* Saves an item to the cache store.
*
* @param string $key Cache item name
* @param mixed $value The data to save
* @param integer $ttl Time To Live, in seconds (default 60)
*
* @return boolean Success or failure
*/
public function save(string $key, $value, int $ttl = 60)
{
return true;
}
//--------------------------------------------------------------------
/**
* Deletes a specific item from the cache store.
*
* @param string $key Cache item name
*
* @return boolean Success or failure
*/
public function delete(string $key)
{
return true;
}
//--------------------------------------------------------------------
/**
* Deletes items from the cache store matching a given pattern.
*
* @param string $pattern Cache items glob-style pattern
*
* @return integer The number of deleted items
*/
public function deleteMatching(string $pattern)
{
return 0;
}
//--------------------------------------------------------------------
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return boolean
*/
public function increment(string $key, int $offset = 1)
{
return true;
}
//--------------------------------------------------------------------
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return boolean
*/
public function decrement(string $key, int $offset = 1)
{
return true;
}
//--------------------------------------------------------------------
/**
* Will delete all items in the entire cache.
*
* @return boolean Success or failure
*/
public function clean()
{
return true;
}
//--------------------------------------------------------------------
/**
* Returns information on the entire cache.
*
* The information returned and the structure of the data
* varies depending on the handler.
*
* @return null
*/
public function getCacheInfo()
{
return null;
}
//--------------------------------------------------------------------
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return null
*/
public function getMetaData(string $key)
{
return null;
}
//--------------------------------------------------------------------
/**
* Determines if the driver is supported on this system.
*
* @return boolean
*/
public function isSupported(): bool
{
return true;
}
}
+548
View File
@@ -0,0 +1,548 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Cache\Exceptions\CacheException;
use Config\Cache;
use Throwable;
/**
* File system cache handler
*/
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 integer
*
* @see https://www.php.net/manual/en/function.chmod.php
*/
protected $mode;
//--------------------------------------------------------------------
/**
* Constructor.
*
* @param Cache $config
* @throws CacheException
*/
public function __construct(Cache $config)
{
if (! property_exists($config, 'file'))
{
$config->file = [
'storePath' => $config->storePath ?? WRITEPATH . 'cache',
'mode' => 0640,
];
}
$this->path = ! empty($config->file['storePath']) ? $config->file['storePath'] : WRITEPATH . 'cache';
$this->path = rtrim($this->path, '/') . '/';
if (! is_really_writable($this->path))
{
throw CacheException::forUnableToWrite($this->path);
}
$this->mode = $config->file['mode'] ?? 0640;
$this->prefix = $config->prefix;
}
//--------------------------------------------------------------------
/**
* Takes care of any handler-specific setup that must be done.
*/
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)
{
$key = static::validateKey($key, $this->prefix);
$data = $this->getItem($key);
return is_array($data) ? $data['data'] : null;
}
//--------------------------------------------------------------------
/**
* Saves an item to the cache store.
*
* @param string $key Cache item name
* @param mixed $value The data to save
* @param integer $ttl Time To Live, in seconds (default 60)
*
* @return boolean Success or failure
*/
public function save(string $key, $value, int $ttl = 60)
{
$key = static::validateKey($key, $this->prefix);
$contents = [
'time' => time(),
'ttl' => $ttl,
'data' => $value,
];
if ($this->writeFile($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->getMessage());
}
// @codeCoverageIgnoreEnd
return true;
}
return false;
}
//--------------------------------------------------------------------
/**
* Deletes a specific item from the cache store.
*
* @param string $key Cache item name
*
* @return boolean Success or failure
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return is_file($this->path . $key) && unlink($this->path . $key);
}
//--------------------------------------------------------------------
/**
* Deletes items from the cache store matching a given pattern.
*
* @param string $pattern Cache items glob-style pattern
*
* @return integer The number of deleted items
*/
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;
}
//--------------------------------------------------------------------
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return boolean
*/
public function increment(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
$data = $this->getItem($key);
if ($data === false)
{
$data = [
'data' => 0,
'ttl' => 60,
];
}
elseif (! is_int($data['data']))
{
return false;
}
$newValue = $data['data'] + $offset;
return $this->save($key, $newValue, $data['ttl']) ? $newValue : false;
}
//--------------------------------------------------------------------
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return boolean
*/
public function decrement(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
$data = $this->getItem($key);
if ($data === false)
{
$data = [
'data' => 0,
'ttl' => 60,
];
}
elseif (! is_int($data['data']))
{
return false;
}
$newValue = $data['data'] - $offset;
return $this->save($key, $newValue, $data['ttl']) ? $newValue : false;
}
//--------------------------------------------------------------------
/**
* Will delete all items in the entire cache.
*
* @return boolean Success or failure
*/
public function clean()
{
return $this->deleteFiles($this->path, false, true);
}
//--------------------------------------------------------------------
/**
* Returns information on the entire cache.
*
* The information returned and the structure of the data
* varies depending on the handler.
*
* @return array|false
*/
public function getCacheInfo()
{
return $this->getDirFileInfo($this->path);
}
//--------------------------------------------------------------------
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return array|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)
{
$key = static::validateKey($key, $this->prefix);
if (false === $data = $this->getItem($key))
{
return false; // This will return null in a future release
}
return [
'expire' => $data['time'] + $data['ttl'],
'mtime' => filemtime($this->path . $key),
'data' => $data['data'],
];
}
//--------------------------------------------------------------------
/**
* Determines if the driver is supported on this system.
*
* @return boolean
*/
public function isSupported(): bool
{
return is_writable($this->path);
}
//--------------------------------------------------------------------
/**
* Does the heavy lifting of actually retrieving the file and
* verifying it's age.
*
* @param string $filename
*
* @return boolean|mixed
*/
protected function getItem(string $filename)
{
if (! is_file($this->path . $filename))
{
return false;
}
$data = @unserialize(file_get_contents($this->path . $filename));
if (! is_array($data) || ! isset($data['ttl']))
{
return false;
}
// @phpstan-ignore-next-line
if ($data['ttl'] > 0 && time() > $data['time'] + $data['ttl'])
{
// If the file is still there then try to remove it
if (is_file($this->path . $filename))
{
@unlink($this->path . $filename);
}
return false;
}
return $data;
}
//--------------------------------------------------------------------
//--------------------------------------------------------------------
// SUPPORT METHODS FOR FILES
//--------------------------------------------------------------------
/**
* Writes a file to disk, or returns false if not successful.
*
* @param string $path
* @param string $data
* @param string $mode
*
* @return boolean
*/
protected function writeFile($path, $data, $mode = 'wb')
{
if (($fp = @fopen($path, $mode)) === false)
{
return false;
}
flock($fp, LOCK_EX);
for ($result = $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);
}
//--------------------------------------------------------------------
/**
* Delete Files
*
* 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.
*
* @param string $path File path
* @param boolean $delDir Whether to delete any directories found in the path
* @param boolean $htdocs Whether to skip deleting .htaccess and index page files
* @param integer $_level Current directory depth level (default: 0; internal use only)
*
* @return boolean
*/
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 !== true || ! preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename))
{
@unlink($path . DIRECTORY_SEPARATOR . $filename);
}
}
}
closedir($currentDir);
return ($delDir === true && $_level > 0) ? @rmdir($path) : true;
}
//--------------------------------------------------------------------
/**
* Get Directory File Information
*
* 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.
*
* @param string $sourceDir Path to source
* @param boolean $topLevelOnly Look only at the top level directory specified?
* @param boolean $_recursion Internal variable to determine recursion status - do not use in calls
*
* @return array|false
*/
protected function getDirFileInfo(string $sourceDir, bool $topLevelOnly = true, bool $_recursion = false)
{
static $_filedata = [];
$relativePath = $sourceDir;
if ($fp = @opendir($sourceDir))
{
// reset the array and make sure $source_dir has a trailing slash on the initial call
if ($_recursion === false)
{
$_filedata = [];
$sourceDir = rtrim(realpath($sourceDir) ?: $sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}
// Used to be foreach (scandir($source_dir, 1) as $file), but scandir() is simply not as fast
while (false !== ($file = readdir($fp)))
{
if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false)
{
$this->getDirFileInfo($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true);
}
elseif ($file[0] !== '.')
{
$_filedata[$file] = $this->getFileInfo($sourceDir . $file);
$_filedata[$file]['relative_path'] = $relativePath;
}
}
closedir($fp);
return $_filedata;
}
return false;
}
//--------------------------------------------------------------------
/**
* Get File Info
*
* 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.
*
* @param string $file Path to file
* @param mixed $returnedValues Array or comma separated string of information returned
*
* @return array|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;
}
}
+377
View File
@@ -0,0 +1,377 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Exceptions\CriticalError;
use Config\Cache;
use Exception;
use Memcache;
use Memcached;
/**
* Mamcached cache handler
*/
class MemcachedHandler extends BaseHandler
{
/**
* The memcached object
*
* @var Memcached|Memcache
*/
protected $memcached;
/**
* Memcached Configuration
*
* @var array
*/
protected $config = [
'host' => '127.0.0.1',
'port' => 11211,
'weight' => 1,
'raw' => false,
];
//--------------------------------------------------------------------
/**
* Constructor.
*
* @param Cache $config
*/
public function __construct(Cache $config)
{
$this->prefix = $config->prefix;
if (! empty($config))
{
$this->config = array_merge($this->config, $config->memcached);
}
}
/**
* Class destructor
*
* 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();
}
}
//--------------------------------------------------------------------
/**
* Takes care of any handler-specific setup that must be done.
*/
public function initialize()
{
// Try to connect to Memcache or Memcached, if an issue occurs throw a CriticalError exception,
// so that the CacheFactory can attempt to initiate the next cache handler.
try
{
if (class_exists(Memcached::class))
{
// Create new instance of Memcached
$this->memcached = new Memcached();
if ($this->config['raw'])
{
$this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
}
// Add server
$this->memcached->addServer(
$this->config['host'], $this->config['port'], $this->config['weight']
);
// attempt to get status of servers
$stats = $this->memcached->getStats();
// $stats should be an associate array with a key in the format of host:port.
// If it doesn't have the key, we know the server is not working as expected.
if (! isset($stats[$this->config['host'] . ':' . $this->config['port']]))
{
throw new CriticalError('Cache: Memcached connection failed.');
}
}
elseif (class_exists(Memcache::class))
{
// Create new instance of Memcache
$this->memcached = new Memcache();
// Check if we can connect to the server
$canConnect = $this->memcached->connect(
$this->config['host'], $this->config['port']
);
// If we can't connect, throw a CriticalError exception
if ($canConnect === false)
{
throw new CriticalError('Cache: Memcache connection failed.');
}
// Add server, third parameter is persistence and defaults to TRUE.
$this->memcached->addServer(
$this->config['host'], $this->config['port'], true, $this->config['weight']
);
}
else
{
throw new CriticalError('Cache: Not support Memcache(d) extension.');
}
}
catch (CriticalError $e)
{
// If a CriticalError exception occurs, throw it up.
throw $e;
}
catch (Exception $e)
{
// If an \Exception occurs, convert it into a CriticalError exception and throw it.
throw new CriticalError('Cache: Memcache(d) connection refused (' . $e->getMessage() . ').');
}
}
//--------------------------------------------------------------------
/**
* Attempts to fetch an item from the cache store.
*
* @param string $key Cache item name
*
* @return mixed
*/
public function get(string $key)
{
$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); // @phpstan-ignore-line
// check for unmatched key (i.e. $flags is untouched)
if ($flags === false)
{
return null;
}
}
return is_array($data) ? $data[0] : $data; // @phpstan-ignore-line
}
//--------------------------------------------------------------------
/**
* Saves an item to the cache store.
*
* @param string $key Cache item name
* @param mixed $value The data to save
* @param integer $ttl Time To Live, in seconds (default 60)
*
* @return boolean Success or failure
*/
public function save(string $key, $value, int $ttl = 60)
{
$key = static::validateKey($key, $this->prefix);
if (! $this->config['raw'])
{
$value = [
$value,
time(),
$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);
}
// @phpstan-ignore-next-line
return false;
}
//--------------------------------------------------------------------
/**
* Deletes a specific item from the cache store.
*
* @param string $key Cache item name
*
* @return boolean Success or failure
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return $this->memcached->delete($key);
}
//--------------------------------------------------------------------
/**
* Deletes items from the cache store matching a given pattern.
*
* @param string $pattern Cache items glob-style pattern
*
* @throws Exception
*/
public function deleteMatching(string $pattern)
{
throw new Exception('The deleteMatching method is not implemented for Memcached. You must select File, Redis or Predis handlers to use it.');
}
//--------------------------------------------------------------------
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return integer|false
*/
public function increment(string $key, int $offset = 1)
{
if (! $this->config['raw'])
{
return false;
}
$key = static::validateKey($key, $this->prefix);
// @phpstan-ignore-next-line
return $this->memcached->increment($key, $offset, $offset, 60);
}
//--------------------------------------------------------------------
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return integer|false
*/
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.
// @phpstan-ignore-next-line
return $this->memcached->decrement($key, $offset, $offset, 60);
}
//--------------------------------------------------------------------
/**
* Will delete all items in the entire cache.
*
* @return boolean Success or failure
*/
public function clean()
{
return $this->memcached->flush();
}
//--------------------------------------------------------------------
/**
* Returns information on the entire cache.
*
* The information returned and the structure of the data
* varies depending on the handler.
*
* @return array|false
*/
public function getCacheInfo()
{
return $this->memcached->getStats();
}
//--------------------------------------------------------------------
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return array|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)
{
$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; // This will return null in a future release
}
[$data, $time, $limit] = $stored;
// Calculate the remaining time to live from the original limit
$ttl = time() - $time - $limit;
return [
'expire' => $limit > 0 ? $time + $limit : null,
'mtime' => $time,
'data' => $data,
];
}
//--------------------------------------------------------------------
/**
* Determines if the driver is supported on this system.
*
* @return boolean
*/
public function isSupported(): bool
{
return extension_loaded('memcached') || extension_loaded('memcache');
}
}
+311
View File
@@ -0,0 +1,311 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Exceptions\CriticalError;
use Config\Cache;
use Exception;
use Predis\Client;
use Predis\Collection\Iterator\Keyspace;
/**
* Predis cache handler
*/
class PredisHandler extends BaseHandler
{
/**
* Default config
*
* @var array
*/
protected $config = [
'scheme' => 'tcp',
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
];
/**
* Predis connection
*
* @var Client
*/
protected $redis;
//--------------------------------------------------------------------
/**
* Constructor.
*
* @param Cache $config
*/
public function __construct(Cache $config)
{
$this->prefix = $config->prefix;
if (isset($config->redis))
{
$this->config = array_merge($this->config, $config->redis);
}
}
//--------------------------------------------------------------------
/**
* Takes care of any handler-specific setup that must be done.
*/
public function initialize()
{
// Try to connect to Redis, if an issue occurs throw a CriticalError exception,
// so that the CacheFactory can attempt to initiate the next cache handler.
try
{
// Create a new instance of Predis\Client
$this->redis = new Client($this->config, ['prefix' => $this->prefix]);
// Check if the connection is valid by trying to get the time.
$this->redis->time();
}
catch (Exception $e)
{
// thrown if can't connect to redis server.
throw new CriticalError('Cache: Predis connection refused (' . $e->getMessage() . ').');
}
}
//--------------------------------------------------------------------
/**
* Attempts to fetch an item from the cache store.
*
* @param string $key Cache item name
*
* @return mixed
*/
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;
}
switch ($data['__ci_type'])
{
case 'array':
case 'object':
return unserialize($data['__ci_value']);
case 'boolean':
case 'integer':
case 'double': // Yes, 'double' is returned and NOT 'float'
case 'string':
case 'NULL':
return settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null;
case 'resource':
default:
return null;
}
}
//--------------------------------------------------------------------
/**
* Saves an item to the cache store.
*
* @param string $key Cache item name
* @param mixed $value The data to save
* @param integer $ttl Time To Live, in seconds (default 60)
*
* @return boolean Success or failure
*/
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]))
{
return false;
}
$this->redis->expireat($key, time() + $ttl);
return true;
}
//--------------------------------------------------------------------
/**
* Deletes a specific item from the cache store.
*
* @param string $key Cache item name
*
* @return boolean Success or failure
*/
public function delete(string $key)
{
$key = static::validateKey($key);
return $this->redis->del($key) === 1;
}
//--------------------------------------------------------------------
/**
* Deletes items from the cache store matching a given pattern.
*
* @param string $pattern Cache items glob-style pattern
*
* @return integer The number of deleted items
*/
public function deleteMatching(string $pattern)
{
$matchedKeys = [];
foreach (new Keyspace($this->redis, $pattern) as $key)
{
$matchedKeys[] = $key;
}
return $this->redis->del($matchedKeys);
}
//--------------------------------------------------------------------
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return integer
*/
public function increment(string $key, int $offset = 1)
{
$key = static::validateKey($key);
return $this->redis->hincrby($key, 'data', $offset);
}
//--------------------------------------------------------------------
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return integer
*/
public function decrement(string $key, int $offset = 1)
{
$key = static::validateKey($key);
return $this->redis->hincrby($key, 'data', -$offset);
}
//--------------------------------------------------------------------
/**
* Will delete all items in the entire cache.
*
* @return boolean Success or failure
*/
public function clean()
{
return $this->redis->flushdb()->getPayload() === 'OK';
}
//--------------------------------------------------------------------
/**
* Returns information on the entire cache.
*
* The information returned and the structure of the data
* varies depending on the handler.
*
* @return array
*/
public function getCacheInfo()
{
return $this->redis->info();
}
//--------------------------------------------------------------------
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return array|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).
*/
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();
$ttl = $this->redis->ttl($key);
return [
'expire' => $ttl > 0 ? time() + $ttl : null,
'mtime' => $time,
'data' => $data['__ci_value'],
];
}
return null;
}
//--------------------------------------------------------------------
/**
* Determines if the driver is supported on this system.
*
* @return boolean
*/
public function isSupported(): bool
{
return class_exists('\Predis\Client');
}
}
+352
View File
@@ -0,0 +1,352 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Exceptions\CriticalError;
use Config\Cache;
use Redis;
use RedisException;
/**
* Redis cache handler
*/
class RedisHandler extends BaseHandler
{
/**
* Default config
*
* @var array
*/
protected $config = [
'host' => '127.0.0.1',
'password' => null,
'port' => 6379,
'timeout' => 0,
'database' => 0,
];
/**
* Redis connection
*
* @var Redis
*/
protected $redis;
//--------------------------------------------------------------------
/**
* Constructor.
*
* @param Cache $config
*/
public function __construct(Cache $config)
{
$this->prefix = $config->prefix;
if (! empty($config))
{
$this->config = array_merge($this->config, $config->redis);
}
}
/**
* Class destructor
*
* Closes the connection to Redis if present.
*/
public function __destruct()
{
if (isset($this->redis))
{
$this->redis->close();
}
}
//--------------------------------------------------------------------
/**
* Takes care of any handler-specific setup that must be done.
*/
public function initialize()
{
$config = $this->config;
$this->redis = new Redis();
// Try to connect to Redis, if an issue occurs throw a CriticalError exception,
// so that the CacheFactory can attempt to initiate the next cache handler.
try
{
// 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)
{
// $this->redis->connect() can sometimes throw a RedisException.
// We need to convert the exception into a CriticalError exception and throw it.
throw new CriticalError('Cache: RedisException occurred with message (' . $e->getMessage() . ').');
}
}
//--------------------------------------------------------------------
/**
* Attempts to fetch an item from the cache store.
*
* @param string $key Cache item name
*
* @return mixed
*/
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;
}
switch ($data['__ci_type'])
{
case 'array':
case 'object':
return unserialize($data['__ci_value']);
case 'boolean':
case 'integer':
case 'double': // Yes, 'double' is returned and NOT 'float'
case 'string':
case 'NULL':
return settype($data['__ci_value'], $data['__ci_type']) ? $data['__ci_value'] : null;
case 'resource':
default:
return null;
}
}
//--------------------------------------------------------------------
/**
* Saves an item to the cache store.
*
* @param string $key Cache item name
* @param mixed $value The data to save
* @param integer $ttl Time To Live, in seconds (default 60)
*
* @return boolean Success or failure
*/
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)
{
$this->redis->expireAt($key, time() + $ttl);
}
return true;
}
//--------------------------------------------------------------------
/**
* Deletes a specific item from the cache store.
*
* @param string $key Cache item name
*
* @return boolean Success or failure
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return $this->redis->del($key) === 1;
}
//--------------------------------------------------------------------
/**
* Deletes items from the cache store matching a given pattern.
*
* @param string $pattern Cache items glob-style pattern
*
* @return integer The number of deleted items
*/
public function deleteMatching(string $pattern)
{
$matchedKeys = [];
$iterator = null;
do
{
// Scan for some keys
$keys = $this->redis->scan($iterator, $pattern);
// Redis may return empty results, so protect against that
if ($keys !== false)
{
foreach ($keys as $key)
{
$matchedKeys[] = $key;
}
}
}
while ($iterator > 0);
return $this->redis->del($matchedKeys);
}
//--------------------------------------------------------------------
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return integer
*/
public function increment(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
return $this->redis->hIncrBy($key, 'data', $offset);
}
//--------------------------------------------------------------------
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return integer
*/
public function decrement(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
return $this->redis->hIncrBy($key, 'data', -$offset);
}
//--------------------------------------------------------------------
/**
* Will delete all items in the entire cache.
*
* @return boolean Success or failure
*/
public function clean()
{
return $this->redis->flushDB();
}
//--------------------------------------------------------------------
/**
* Returns information on the entire cache.
*
* The information returned and the structure of the data
* varies depending on the handler.
*
* @return array
*/
public function getCacheInfo()
{
return $this->redis->info();
}
//--------------------------------------------------------------------
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return array|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).
*/
public function getMetaData(string $key)
{
$key = static::validateKey($key, $this->prefix);
$value = $this->get($key);
if ($value !== null)
{
$time = time();
$ttl = $this->redis->ttl($key);
return [
'expire' => $ttl > 0 ? time() + $ttl : null,
'mtime' => $time,
'data' => $value,
];
}
return null;
}
//--------------------------------------------------------------------
/**
* Determines if the driver is supported on this system.
*
* @return boolean
*/
public function isSupported(): bool
{
return extension_loaded('redis');
}
}
+216
View File
@@ -0,0 +1,216 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cache\Handlers;
use Config\Cache;
use Exception;
/**
* Cache handler for WinCache from Microsoft & IIS.
*
* @codeCoverageIgnore
*/
class WincacheHandler extends BaseHandler
{
/**
* Constructor.
*
* @param Cache $config
*/
public function __construct(Cache $config)
{
$this->prefix = $config->prefix;
}
//--------------------------------------------------------------------
/**
* Takes care of any handler-specific setup that must be done.
*/
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)
{
$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;
}
//--------------------------------------------------------------------
/**
* Saves an item to the cache store.
*
* @param string $key Cache item name
* @param mixed $value The data to save
* @param integer $ttl Time To Live, in seconds (default 60)
*
* @return boolean Success or failure
*/
public function save(string $key, $value, int $ttl = 60)
{
$key = static::validateKey($key, $this->prefix);
return wincache_ucache_set($key, $value, $ttl);
}
//--------------------------------------------------------------------
/**
* Deletes a specific item from the cache store.
*
* @param string $key Cache item name
*
* @return boolean Success or failure
*/
public function delete(string $key)
{
$key = static::validateKey($key, $this->prefix);
return wincache_ucache_delete($key);
}
//--------------------------------------------------------------------
/**
* Deletes items from the cache store matching a given pattern.
*
* @param string $pattern Cache items glob-style pattern
*
* @throws Exception
*/
public function deleteMatching(string $pattern)
{
throw new Exception('The deleteMatching method is not implemented for Wincache. You must select File, Redis or Predis handlers to use it.');
}
//--------------------------------------------------------------------
/**
* Performs atomic incrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return integer|false
*/
public function increment(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
return wincache_ucache_inc($key, $offset);
}
//--------------------------------------------------------------------
/**
* Performs atomic decrementation of a raw stored value.
*
* @param string $key Cache ID
* @param integer $offset Step/value to increase by
*
* @return integer|false
*/
public function decrement(string $key, int $offset = 1)
{
$key = static::validateKey($key, $this->prefix);
return wincache_ucache_dec($key, $offset);
}
//--------------------------------------------------------------------
/**
* Will delete all items in the entire cache.
*
* @return boolean Success or failure
*/
public function clean()
{
return wincache_ucache_clear();
}
//--------------------------------------------------------------------
/**
* Returns information on the entire cache.
*
* The information returned and the structure of the data
* varies depending on the handler.
*
* @return array|false
*/
public function getCacheInfo()
{
return wincache_ucache_info(true);
}
//--------------------------------------------------------------------
/**
* Returns detailed information about the specific item in the cache.
*
* @param string $key Cache item name.
*
* @return array|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)
{
$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() + $ttl : null,
'hitcount' => $hitcount,
'age' => $age,
'ttl' => $ttl,
];
}
return false; // This will return null in a future release
}
//--------------------------------------------------------------------
/**
* Determines if the driver is supported on this system.
*
* @return boolean
*/
public function isSupported(): bool
{
return extension_loaded('wincache') && ini_get('wincache.ucenabled');
}
}
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Cache;
use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Clears current cache.
*/
class ClearCache extends BaseCommand
{
/**
* Command grouping.
*
* @var string
*/
protected $group = 'Cache';
/**
* The Command's name
*
* @var string
*/
protected $name = 'cache:clear';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Clears the current system caches.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'cache:clear [driver]';
/**
* the Command's Arguments
*
* @var array
*/
protected $arguments = [
'driver' => 'The cache driver to use',
];
/**
* Clears the cache
*
* @param array $params
*/
public function run(array $params)
{
$config = config('Cache');
$handler = $params[0] ?? $config->handler;
if (! array_key_exists($handler, $config->validHandlers))
{
CLI::error($handler . ' is not a valid cache handler.');
return;
}
$config->handler = $handler;
$cache = CacheFactory::getHandler($config);
if (! $cache->clean())
{
// @codeCoverageIgnoreStart
CLI::error('Error while clearing the cache.');
return;
// @codeCoverageIgnoreEnd
}
CLI::write(CLI::color('Cache cleared.', 'green'));
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Cache;
use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\I18n\Time;
/**
* Shows information on the cache.
*/
class InfoCache extends BaseCommand
{
/**
* Command grouping.
*
* @var string
*/
protected $group = 'Cache';
/**
* The Command's name
*
* @var string
*/
protected $name = 'cache:info';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Shows file cache information in the current system.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'cache:info';
/**
* Clears the cache
*
* @param array $params
*/
public function run(array $params)
{
$config = config('Cache');
helper('number');
if ($config->handler !== 'file')
{
CLI::error('This command only supports the file cache handler.');
return;
}
$cache = CacheFactory::getHandler($config);
$caches = $cache->getCacheInfo();
$tbody = [];
foreach ($caches as $key => $field)
{
$tbody[] = [
$key,
clean_path($field['server_path']),
number_to_size($field['size']),
Time::createFromTimestamp($field['date']),
];
}
$thead = [
CLI::color('Name', 'green'),
CLI::color('Server Path', 'green'),
CLI::color('Size', 'green'),
CLI::color('Date', 'green'),
];
CLI::table($tbody, $thead);
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Database;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Config\Factories;
use CodeIgniter\Database\SQLite3\Connection;
use Config\Database;
use Throwable;
/**
* Creates a new database.
*/
class CreateDatabase extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Database';
/**
* The Command's name
*
* @var string
*/
protected $name = 'db:create';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Create a new database schema.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'db:create <db_name> [options]';
/**
* The Command's arguments
*
* @var array<string, string>
*/
protected $arguments = [
'db_name' => 'The database name to use',
];
/**
* The Command's options
*
* @var array<string, string>
*/
protected $options = [
'--ext' => 'File extension of the database file for SQLite3. Can be `db` or `sqlite`. Defaults to `db`.',
];
/**
* Creates a new database.
*
* @param array $params
*
* @return void
*/
public function run(array $params)
{
$name = array_shift($params);
if (empty($name))
{
$name = CLI::prompt('Database name', null, 'required'); // @codeCoverageIgnore
}
try
{
/**
* @var Database $config
*/
$config = config('Database');
// Set to an empty database to prevent connection errors.
$group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup;
$config->{$group}['database'] = '';
$db = Database::connect();
// Special SQLite3 handling
if ($db instanceof Connection)
{
$ext = $params['ext'] ?? CLI::getOption('ext') ?? 'db';
if (! in_array($ext, ['db', 'sqlite'], true))
{
$ext = CLI::prompt('Please choose a valid file extension', ['db', 'sqlite']); // @codeCoverageIgnore
}
if ($name !== ':memory:')
{
$name = str_replace(['.db', '.sqlite'], '', $name) . ".{$ext}";
}
$config->{$group}['DBDriver'] = 'SQLite3';
$config->{$group}['database'] = $name;
if ($name !== ':memory:')
{
$dbName = strpos($name, DIRECTORY_SEPARATOR) === false ? WRITEPATH . $name : $name;
if (is_file($dbName))
{
CLI::error("Database \"{$dbName}\" already exists.", 'light_gray', 'red');
CLI::newLine();
return;
}
unset($dbName);
}
// Connect to new SQLite3 to create new database
$db = Database::connect(null, false);
$db->connect();
if (! is_file($db->getDatabase()) && $name !== ':memory:')
{
// @codeCoverageIgnoreStart
CLI::error('Database creation failed.', 'light_gray', 'red');
CLI::newLine();
return;
// @codeCoverageIgnoreEnd
}
}
elseif (! Database::forge()->createDatabase($name))
{
// @codeCoverageIgnoreStart
CLI::error('Database creation failed.', 'light_gray', 'red');
CLI::newLine();
return;
// @codeCoverageIgnoreEnd
}
CLI::write("Database \"{$name}\" successfully created.", 'green');
CLI::newLine();
}
catch (Throwable $e)
{
$this->showError($e);
}
finally
{
// Reset the altered config no matter what happens.
Factories::reset('config');
}
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Database;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Services;
use Throwable;
/**
* Runs all new migrations.
*/
class Migrate extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Database';
/**
* The Command's name
*
* @var string
*/
protected $name = 'migrate';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Locates and runs all new migrations against the database.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'migrate [options]';
/**
* the Command's Options
*
* @var array
*/
protected $options = [
'-n' => 'Set migration namespace',
'-g' => 'Set database group',
'--all' => 'Set for all namespaces, will ignore (-n) option',
];
/**
* Ensures that all migrations have been run.
*
* @param array $params
*
* @return void
*/
public function run(array $params)
{
$runner = Services::migrations();
$runner->clearCliMessages();
CLI::write(lang('Migrations.latest'), 'yellow');
$namespace = $params['n'] ?? CLI::getOption('n');
$group = $params['g'] ?? CLI::getOption('g');
try
{
// Check for 'all' namespaces
if (array_key_exists('all', $params) || CLI::getOption('all'))
{
$runner->setNamespace(null);
}
// Check for a specified namespace
elseif ($namespace)
{
$runner->setNamespace($namespace);
}
if (! $runner->latest($group))
{
CLI::error(lang('Migrations.generalFault'), 'light_gray', 'red'); // @codeCoverageIgnore
}
$messages = $runner->getCliMessages();
foreach ($messages as $message)
{
CLI::write($message);
}
CLI::write('Done migrations.', 'green');
}
// @codeCoverageIgnoreStart
catch (Throwable $e)
{
$this->showError($e);
}
// @codeCoverageIgnoreEnd
}
}
@@ -0,0 +1,93 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Database;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Does a rollback followed by a latest to refresh the current state
* of the database.
*/
class MigrateRefresh extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Database';
/**
* The Command's name
*
* @var string
*/
protected $name = 'migrate:refresh';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Does a rollback followed by a latest to refresh the current state of the database.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'migrate:refresh [options]';
/**
* the Command's Options
*
* @var array
*/
protected $options = [
'-n' => 'Set migration namespace',
'-g' => 'Set database group',
'--all' => 'Set latest for all namespace, will ignore (-n) option',
'-f' => 'Force command - this option allows you to bypass the confirmation question when running this command in a production environment',
];
/**
* Does a rollback followed by a latest to refresh the current state
* of the database.
*
* @param array $params
*
* @return void
*/
public function run(array $params)
{
$params['b'] = 0;
if (ENVIRONMENT === 'production')
{
// @codeCoverageIgnoreStart
$force = array_key_exists('f', $params) || CLI::getOption('f');
if (! $force && CLI::prompt(lang('Migrations.refreshConfirm'), ['y', 'n']) === 'n')
{
return;
}
$params['f'] = null;
// @codeCoverageIgnoreEnd
}
$this->call('migrate:rollback', $params);
$this->call('migrate', $params);
}
}
@@ -0,0 +1,121 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Database;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Services;
use Throwable;
/**
* Runs all of the migrations in reverse order, until they have
* all been unapplied.
*/
class MigrateRollback extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Database';
/**
* The Command's name
*
* @var string
*/
protected $name = 'migrate:rollback';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Runs the "down" method for all migrations in the last batch.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'migrate:rollback [options]';
/**
* the Command's Options
*
* @var array
*/
protected $options = [
'-b' => 'Specify a batch to roll back to; e.g. "3" to return to batch #3 or "-2" to roll back twice',
'-g' => 'Set database group',
'-f' => 'Force command - this option allows you to bypass the confirmation question when running this command in a production environment',
];
/**
* Runs all of the migrations in reverse order, until they have
* all been unapplied.
*
* @param array $params
*
* @return void
*/
public function run(array $params)
{
if (ENVIRONMENT === 'production')
{
// @codeCoverageIgnoreStart
$force = array_key_exists('f', $params) || CLI::getOption('f');
if (! $force && CLI::prompt(lang('Migrations.rollBackConfirm'), ['y', 'n']) === 'n')
{
return;
}
// @codeCoverageIgnoreEnd
}
$runner = Services::migrations();
$group = $params['g'] ?? CLI::getOption('g');
if (is_string($group))
{
$runner->setGroup($group);
}
try
{
$batch = $params['b'] ?? CLI::getOption('b') ?? $runner->getLastBatch() - 1;
CLI::write(lang('Migrations.rollingBack') . ' ' . $batch, 'yellow');
if (! $runner->regress($batch))
{
CLI::error(lang('Migrations.generalFault'), 'light_gray', 'red'); // @codeCoverageIgnore
}
$messages = $runner->getCliMessages();
foreach ($messages as $message)
{
CLI::write($message);
}
CLI::write('Done rolling back migrations.', 'green');
}
// @codeCoverageIgnoreStart
catch (Throwable $e)
{
$this->showError($e);
}
// @codeCoverageIgnoreEnd
}
}
+175
View File
@@ -0,0 +1,175 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Database;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Services;
/**
* Displays a list of all migrations and whether they've been run or not.
*/
class MigrateStatus extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Database';
/**
* The Command's name
*
* @var string
*/
protected $name = 'migrate:status';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Displays a list of all migrations and whether they\'ve been run or not.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'migrate:status [options]';
/**
* the Command's Options
*
* @var array<string, string>
*/
protected $options = [
'-g' => 'Set database group',
];
/**
* Namespaces to ignore when looking for migrations.
*
* @var string[]
*/
protected $ignoredNamespaces = [
'CodeIgniter',
'Config',
'Kint',
'Laminas\ZendFrameworkBridge',
'Laminas\Escaper',
'Psr\Log',
];
/**
* Displays a list of all migrations and whether they've been run or not.
*
* @param array<string, mixed> $params
*
* @return void
*/
public function run(array $params)
{
$runner = Services::migrations();
$group = $params['g'] ?? CLI::getOption('g');
// Get all namespaces
$namespaces = Services::autoloader()->getNamespace();
// Collection of migration status
$status = [];
foreach (array_keys($namespaces) as $namespace)
{
if (ENVIRONMENT !== 'testing')
{
// Make Tests\\Support discoverable for testing
$this->ignoredNamespaces[] = 'Tests\Support'; // @codeCoverageIgnore
}
if (in_array($namespace, $this->ignoredNamespaces, true))
{
continue;
}
if (APP_NAMESPACE !== 'App' && $namespace === 'App')
{
continue; // @codeCoverageIgnore
}
$migrations = $runner->findNamespaceMigrations($namespace);
if (empty($migrations))
{
continue;
}
$history = $runner->getHistory((string) $group);
ksort($migrations);
foreach ($migrations as $uid => $migration)
{
$migrations[$uid]->name = mb_substr($migration->name, mb_strpos($migration->name, $uid . '_'));
$date = '---';
$group = '---';
$batch = '---';
foreach ($history as $row)
{
// @codeCoverageIgnoreStart
if ($runner->getObjectUid($row) !== $migration->uid)
{
continue;
}
$date = date('Y-m-d H:i:s', $row->time);
$group = $row->group;
$batch = $row->batch;
// @codeCoverageIgnoreEnd
}
$status[] = [
$namespace,
$migration->version,
$migration->name,
$group,
$date,
$batch,
];
}
}
if (! $status)
{
// @codeCoverageIgnoreStart
CLI::error(lang('Migrations.noneFound'), 'light_gray', 'red');
CLI::newLine();
return;
// @codeCoverageIgnoreEnd
}
$headers = [
CLI::color(lang('Migrations.namespace'), 'yellow'),
CLI::color(lang('Migrations.version'), 'yellow'),
CLI::color(lang('Migrations.filename'), 'yellow'),
CLI::color(lang('Migrations.group'), 'yellow'),
CLI::color(str_replace(': ', '', lang('Migrations.on')), 'yellow'),
CLI::color(lang('Migrations.batch'), 'yellow'),
];
CLI::table($status, $headers);
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Database;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Database\Seeder;
use Config\Database;
use Throwable;
/**
* Runs the specified Seeder file to populate the database
* with some data.
*/
class Seed extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Database';
/**
* The Command's name
*
* @var string
*/
protected $name = 'db:seed';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Runs the specified seeder to populate known data into the database.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'db:seed <seeder_name>';
/**
* the Command's Arguments
*
* @var array<string, string>
*/
protected $arguments = [
'seeder_name' => 'The seeder name to run',
];
/**
* Passes to Seeder to populate the database.
*
* @param array $params
*
* @return void
*/
public function run(array $params)
{
$seeder = new Seeder(new Database());
$seedName = array_shift($params);
if (empty($seedName))
{
$seedName = CLI::prompt(lang('Migrations.migSeeder'), null, 'required'); // @codeCoverageIgnore
}
try
{
$seeder->call($seedName);
}
catch (Throwable $e)
{
$this->showError($e);
}
}
}
+225
View File
@@ -0,0 +1,225 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Encryption;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Config\DotEnv;
use CodeIgniter\Encryption\Encryption;
/**
* Generates a new encryption key.
*/
class GenerateKey extends BaseCommand
{
/**
* The Command's group.
*
* @var string
*/
protected $group = 'Encryption';
/**
* The Command's name.
*
* @var string
*/
protected $name = 'key:generate';
/**
* The Command's usage.
*
* @var string
*/
protected $usage = 'key:generate [options]';
/**
* The Command's short description.
*
* @var string
*/
protected $description = 'Generates a new encryption key and writes it in an `.env` file.';
/**
* The command's options
*
* @var array
*/
protected $options = [
'--force' => 'Force overwrite existing key in `.env` file.',
'--length' => 'The length of the random string that should be returned in bytes. Defaults to 32.',
'--prefix' => 'Prefix to prepend to encoded key (either hex2bin or base64). Defaults to hex2bin.',
'--show' => 'Shows the generated key in the terminal instead of storing in the `.env` file.',
];
/**
* Actually execute the command.
*
* @param array $params
*
* @return void
*/
public function run(array $params)
{
$prefix = $params['prefix'] ?? CLI::getOption('prefix');
if (in_array($prefix, [null, true], true))
{
$prefix = 'hex2bin';
}
elseif (! in_array($prefix, ['hex2bin', 'base64'], true))
{
// @codeCoverageIgnoreStart
$prefix = CLI::prompt('Please provide a valid prefix to use.', ['hex2bin', 'base64'], 'required');
// @codeCoverageIgnoreEnd
}
$length = $params['length'] ?? CLI::getOption('length');
if (in_array($length, [null, true], true))
{
$length = 32;
}
$encodedKey = $this->generateRandomKey($prefix, $length);
if (array_key_exists('show', $params) || (bool) CLI::getOption('show'))
{
CLI::write($encodedKey, 'yellow');
CLI::newLine();
return;
}
if (! $this->setNewEncryptionKey($encodedKey, $params))
{
CLI::write('Error in setting new encryption key to .env file.', 'light_gray', 'red');
CLI::newLine();
return;
}
// force DotEnv to reload the new env vars
putenv('encryption.key');
unset($_ENV['encryption.key'], $_SERVER['encryption.key']);
$dotenv = new DotEnv(ROOTPATH);
$dotenv->load();
CLI::write('Application\'s new encryption key was successfully set.', 'green');
CLI::newLine();
}
/**
* Generates a key and encodes it.
*
* @param string $prefix
* @param integer $length
*
* @return string
*/
protected function generateRandomKey(string $prefix, int $length): string
{
$key = Encryption::createKey($length);
if ($prefix === 'hex2bin')
{
return 'hex2bin:' . bin2hex($key);
}
return 'base64:' . base64_encode($key);
}
/**
* Sets the new encryption key in your .env file.
*
* @param string $key
* @param array $params
*
* @return boolean
*/
protected function setNewEncryptionKey(string $key, array $params): bool
{
$currentKey = env('encryption.key', '');
if (strlen($currentKey) !== 0 && ! $this->confirmOverwrite($params))
{
// Not yet testable since it requires keyboard input
// @codeCoverageIgnoreStart
return false;
// @codeCoverageIgnoreEnd
}
return $this->writeNewEncryptionKeyToFile($currentKey, $key);
}
/**
* Checks whether to overwrite existing encryption key.
*
* @param array $params
*
* @return boolean
*/
protected function confirmOverwrite(array $params): bool
{
return (array_key_exists('force', $params) || CLI::getOption('force')) || CLI::prompt('Overwrite existing key?', ['n', 'y']) === 'y';
}
/**
* Writes the new encryption key to .env file.
*
* @param string $oldKey
* @param string $newKey
*
* @return boolean
*/
protected function writeNewEncryptionKeyToFile(string $oldKey, string $newKey): bool
{
$baseEnv = ROOTPATH . 'env';
$envFile = ROOTPATH . '.env';
if (! file_exists($envFile))
{
if (! file_exists($baseEnv))
{
CLI::write('Both default shipped `env` file and custom `.env` are missing.', 'yellow');
CLI::write('Here\'s your new key instead: ' . CLI::color($newKey, 'yellow'));
CLI::newLine();
return false;
}
copy($baseEnv, $envFile);
}
$ret = file_put_contents($envFile, preg_replace(
$this->keyPattern($oldKey),
"\nencryption.key = $newKey",
file_get_contents($envFile)
));
return $ret !== false;
}
/**
* Get the regex of the current encryption key.
*
* @param string $oldKey
*
* @return string
*/
protected function keyPattern(string $oldKey): string
{
$escaped = preg_quote($oldKey, '/');
if ($escaped !== '')
{
$escaped = "[$escaped]*";
}
return "/^[#\s]*encryption.key[=\s]*{$escaped}$/m";
}
}
@@ -0,0 +1,127 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton command file.
*/
class CommandGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:command';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new spark command.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:command <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The command class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--command' => 'The command name. Default: "command:name"',
'--type' => 'The command type. Options [basic, generator]. Default: "basic".',
'--group' => 'The command group. Default: [basic -> "CodeIgniter", generator -> "Generators"].',
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserCommand).',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Command';
$this->directory = 'Commands';
$this->template = 'command.tpl.php';
$this->classNameLang = 'CLI.generator.className.command';
$this->execute($params);
}
/**
* Prepare options and do the necessary replacements.
*
* @param string $class
*
* @return string
*/
protected function prepare(string $class): string
{
$command = $this->getOption('command');
$group = $this->getOption('group');
$type = $this->getOption('type');
$command = is_string($command) ? $command : 'command:name';
$type = is_string($type) ? $type : 'basic';
if (! in_array($type, ['basic', 'generator'], true))
{
// @codeCoverageIgnoreStart
$type = CLI::prompt(lang('CLI.generator.commandType'), ['basic', 'generator'], 'required');
CLI::newLine();
// @codeCoverageIgnoreEnd
}
if (! is_string($group))
{
$group = $type === 'generator' ? 'Generators' : 'CodeIgniter';
}
return $this->parseTemplate(
$class,
['{group}', '{command}'],
[$group, $command],
['type' => $type]
);
}
}
@@ -0,0 +1,105 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton config file.
*/
class ConfigGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:config';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new config file.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:config <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The config class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserConfig).',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Config';
$this->directory = 'Config';
$this->template = 'config.tpl.php';
$this->classNameLang = 'CLI.generator.className.config';
$this->execute($params);
}
/**
* Prepare options and do the necessary replacements.
*
* @param string $class
*
* @return string
*/
protected function prepare(string $class): string
{
$namespace = $this->getOption('namespace') ?? APP_NAMESPACE;
if ($namespace === APP_NAMESPACE)
{
$class = substr($class, strlen($namespace . '\\'));
}
return $this->parseTemplate($class);
}
}
@@ -0,0 +1,145 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton controller file.
*/
class ControllerGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:controller';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new controller file.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:controller <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The controller class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--bare' => 'Extends from CodeIgniter\Controller instead of BaseController.',
'--restful' => 'Extends from a RESTful resource, Options: [controller, presenter]. Default: "controller".',
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserController).',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Controller';
$this->directory = 'Controllers';
$this->template = 'controller.tpl.php';
$this->classNameLang = 'CLI.generator.className.controller';
$this->execute($params);
}
/**
* Prepare options and do the necessary replacements.
*
* @param string $class
*
* @return string
*/
protected function prepare(string $class): string
{
$bare = $this->getOption('bare');
$rest = $this->getOption('restful');
$useStatement = trim(APP_NAMESPACE, '\\') . '\Controllers\BaseController';
$extends = 'BaseController';
// Gets the appropriate parent class to extend.
if ($bare || $rest)
{
if ($bare)
{
$useStatement = 'CodeIgniter\Controller';
$extends = 'Controller';
}
elseif ($rest)
{
$rest = is_string($rest) ? $rest : 'controller';
if (! in_array($rest, ['controller', 'presenter'], true))
{
// @codeCoverageIgnoreStart
$rest = CLI::prompt(lang('CLI.generator.parentClass'), ['controller', 'presenter'], 'required');
CLI::newLine();
// @codeCoverageIgnoreEnd
}
if ($rest === 'controller')
{
$useStatement = 'CodeIgniter\RESTful\ResourceController';
$extends = 'ResourceController';
}
elseif ($rest === 'presenter')
{
$useStatement = 'CodeIgniter\RESTful\ResourcePresenter';
$extends = 'ResourcePresenter';
}
}
}
return $this->parseTemplate(
$class,
['{useStatement}', '{extends}'],
[$useStatement, $extends],
['type' => $rest]
);
}
}
@@ -0,0 +1,86 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton Entity file.
*/
class EntityGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:entity';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new entity file.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:entity <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The entity class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserEntity).',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Entity';
$this->directory = 'Entities';
$this->template = 'entity.tpl.php';
$this->classNameLang = 'CLI.generator.className.entity';
$this->execute($params);
}
}
@@ -0,0 +1,86 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton Filter file.
*/
class FilterGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:filter';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new filter file.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:filter <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The filter class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserFilter).',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Filter';
$this->directory = 'Filters';
$this->template = 'filter.tpl.php';
$this->classNameLang = 'CLI.generator.className.filter';
$this->execute($params);
}
}
@@ -0,0 +1,93 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Deprecated class for the migration creation command.
*
* @deprecated Use make:migration instead.
*
* @codeCoverageIgnore
*/
class MigrateCreate extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's name
*
* @var string
*/
protected $name = 'migrate:create';
/**
* The Command's short description
*
* @var string
*/
protected $description = '[DEPRECATED] Creates a new migration file. Please use "make:migration" instead.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'migrate:create <name> [options]';
/**
* The Command's arguments.
*
* @var array
*/
protected $arguments = [
'name' => 'The migration file name.',
];
/**
* The Command's options.
*
* @var array
*/
protected $options = [
'--namespace' => 'Set root namespace. Defaults to APP_NAMESPACE',
'--force' => 'Force overwrite existing files.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
// Resolve arguments before passing to make:migration
$params[0] = $params[0] ?? CLI::getSegment(2);
$params['namespace'] = $params['namespace'] ?? CLI::getOption('namespace') ?? APP_NAMESPACE;
if (array_key_exists('force', $params) || CLI::getOption('force'))
{
$params['force'] = null;
}
$this->call('make:migration', $params);
}
}
@@ -0,0 +1,132 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton migration file.
*/
class MigrationGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:migration';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new migration file.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:migration <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The migration class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--session' => 'Generates the migration file for database sessions.',
'--table' => 'Table name to use for database sessions. Default: "ci_sessions".',
'--dbgroup' => 'Database group to use for database sessions. Default: "default".',
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserMigration).',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Migration';
$this->directory = 'Database\Migrations';
$this->template = 'migration.tpl.php';
if (array_key_exists('session', $params) || CLI::getOption('session'))
{
$table = $params['table'] ?? CLI::getOption('table') ?? 'ci_sessions';
$params[0] = "_create_{$table}_table";
}
$this->classNameLang = 'CLI.generator.className.migration';
$this->execute($params);
}
/**
* Prepare options and do the necessary replacements.
*
* @param string $class
*
* @return string
*/
protected function prepare(string $class): string
{
$data['session'] = false;
if ($this->getOption('session'))
{
$table = $this->getOption('table');
$DBGroup = $this->getOption('dbgroup');
$data['session'] = true;
$data['table'] = is_string($table) ? $table : 'ci_sessions';
$data['DBGroup'] = is_string($DBGroup) ? $DBGroup : 'default';
$data['matchIP'] = config('App')->sessionMatchIP;
}
return $this->parseTemplate($class, [], [], $data);
}
/**
* Change file basename before saving.
*
* @param string $filename
*
* @return string
*/
protected function basename(string $filename): string
{
return gmdate(config('Migrations')->timestampFormat) . basename($filename);
}
}
@@ -0,0 +1,138 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton Model file.
*/
class ModelGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:model';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new model file.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:model <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The model class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--table' => 'Supply a table name. Default: "the lowercased plural of the class name".',
'--dbgroup' => 'Database group to use. Default: "default".',
'--return' => 'Return type, Options: [array, object, entity]. Default: "array".',
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserModel).',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Model';
$this->directory = 'Models';
$this->template = 'model.tpl.php';
$this->classNameLang = 'CLI.generator.className.model';
$this->execute($params);
}
/**
* Prepare options and do the necessary replacements.
*
* @param string $class
*
* @return string
*/
protected function prepare(string $class): string
{
$table = $this->getOption('table');
$DBGroup = $this->getOption('dbgroup');
$return = $this->getOption('return');
$baseClass = strtolower(str_replace(trim(implode('\\', array_slice(explode('\\', $class), 0, -1)), '\\') . '\\', '', $class));
$baseClass = strpos($baseClass, 'model') ? str_replace('model', '', $baseClass) : $baseClass;
$table = is_string($table) ? $table : plural($baseClass);
$DBGroup = is_string($DBGroup) ? $DBGroup : 'default';
$return = is_string($return) ? $return : 'array';
if (! in_array($return, ['array', 'object', 'entity'], true))
{
// @codeCoverageIgnoreStart
$return = CLI::prompt(lang('CLI.generator.returnType'), ['array', 'object', 'entity'], 'required');
CLI::newLine();
// @codeCoverageIgnoreEnd
}
if ($return === 'entity')
{
$return = str_replace('Models', 'Entities', $class);
if ($pos = strpos($return, 'Model'))
{
$return = substr($return, 0, $pos);
if ($this->getOption('suffix'))
{
$return .= 'Entity';
}
}
$this->call('make:entity', array_merge([$baseClass], $this->params));
}
return $this->parseTemplate($class, ['{table}', '{DBGroup}', '{return}'], [$table, $DBGroup, $return]);
}
}
@@ -0,0 +1,129 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a complete set of scaffold files.
*/
class ScaffoldGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:scaffold';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a complete set of scaffold files.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:scaffold <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The class name',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--bare' => 'Add the "--bare" option to controller component.',
'--restful' => 'Add the "--restful" option to controller component.',
'--table' => 'Add the "--table" option to the model component.',
'--dbgroup' => 'Add the "--dbgroup" option to model component.',
'--return' => 'Add the "--return" option to the model component.',
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name.',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->params = $params;
$options = [];
if ($this->getOption('namespace'))
{
$options['namespace'] = $this->getOption('namespace');
}
if ($this->getOption('suffix'))
{
$options['suffix'] = null;
}
if ($this->getOption('force'))
{
$options['force'] = null;
}
$controllerOpts = [];
if ($this->getOption('bare'))
{
$controllerOpts['bare'] = null;
}
elseif ($this->getOption('restful'))
{
$controllerOpts['restful'] = $this->getOption('restful');
}
$modelOpts = [
'table' => $this->getOption('table'),
'dbgroup' => $this->getOption('dbgroup'),
'return' => $this->getOption('return'),
];
$class = $params[0] ?? CLI::getSegment(2);
// Call those commands!
$this->call('make:controller', array_merge([$class], $controllerOpts, $options));
$this->call('make:model', array_merge([$class], $modelOpts, $options));
$this->call('make:migration', array_merge([$class], $options));
$this->call('make:seeder', array_merge([$class], $options));
}
}
@@ -0,0 +1,86 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton seeder file.
*/
class SeederGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:seeder';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new seeder file.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:seeder <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The seeder class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserSeeder).',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Seeder';
$this->directory = 'Database\Seeds';
$this->template = 'seeder.tpl.php';
$this->classNameLang = 'CLI.generator.className.seeder';
$this->execute($params);
}
}
@@ -0,0 +1,121 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a migration file for database sessions.
*
* @deprecated Use `make:migration --session` instead.
*
* @codeCoverageIgnore
*/
class SessionMigrationGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'session:migration';
/**
* The Command's Description
*
* @var string
*/
protected $description = '[DEPRECATED] Generates the migration file for database sessions, Please use "make:migration --session" instead.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'session:migration [options]';
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'-t' => 'Supply a table name.',
'-g' => 'Database group to use. Default: "default".',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Migration';
$this->directory = 'Database\Migrations';
$this->template = 'migration.tpl.php';
$table = 'ci_sessions';
if (array_key_exists('t', $params) || CLI::getOption('t'))
{
$table = $params['t'] ?? CLI::getOption('t');
}
$params[0] = "_create_{$table}_table";
$this->execute($params);
}
/**
* Performs the necessary replacements.
*
* @param string $class
*
* @return string
*/
protected function prepare(string $class): string
{
$data['session'] = true;
$data['table'] = $this->getOption('t');
$data['DBGroup'] = $this->getOption('g');
$data['matchIP'] = config('App')->sessionMatchIP ?? false;
$data['table'] = is_string($data['table']) ? $data['table'] : 'ci_sessions';
$data['DBGroup'] = is_string($data['DBGroup']) ? $data['DBGroup'] : 'default';
return $this->parseTemplate($class, [], [], $data);
}
/**
* Change file basename before saving.
*
* @param string $filename
*
* @return string
*/
protected function basename(string $filename): string
{
return gmdate(config('Migrations')->timestampFormat) . basename($filename);
}
}
@@ -0,0 +1,86 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Generators;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\GeneratorTrait;
/**
* Generates a skeleton Validation file.
*/
class ValidationGenerator extends BaseCommand
{
use GeneratorTrait;
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Generators';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'make:validation';
/**
* The Command's Description
*
* @var string
*/
protected $description = 'Generates a new validation file.';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = 'make:validation <name> [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [
'name' => 'The validation class name.',
];
/**
* The Command's Options
*
* @var array
*/
protected $options = [
'--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
'--suffix' => 'Append the component title to the class name (e.g. User => UserValidation).',
'--force' => 'Force overwrite existing file.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$this->component = 'Validation';
$this->directory = 'Validation';
$this->template = 'validation.tpl.php';
$this->classNameLang = 'CLI.generator.className.validation';
$this->execute($params);
}
}
@@ -0,0 +1,76 @@
<@php
namespace {namespace};
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
<?php if ($type === 'generator'): ?>
use CodeIgniter\CLI\GeneratorTrait;
<?php endif ?>
class {class} extends BaseCommand
{
<?php if ($type === 'generator'): ?>
use GeneratorTrait;
<?php endif ?>
/**
* The Command's Group
*
* @var string
*/
protected $group = '{group}';
/**
* The Command's Name
*
* @var string
*/
protected $name = '{command}';
/**
* The Command's Description
*
* @var string
*/
protected $description = '';
/**
* The Command's Usage
*
* @var string
*/
protected $usage = '{command} [arguments] [options]';
/**
* The Command's Arguments
*
* @var array
*/
protected $arguments = [];
/**
* The Command's Options
*
* @var array
*/
protected $options = [];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
<?php if ($type === 'generator'): ?>
$this->component = 'Command';
$this->directory = 'Commands';
$this->template = 'command.tpl.php';
$this->execute($params);
<?php else: ?>
//
<?php endif ?>
}
}
@@ -0,0 +1,10 @@
<@php
namespace {namespace};
use CodeIgniter\Config\BaseConfig;
class {class} extends BaseConfig
{
//
}
@@ -0,0 +1,177 @@
<@php
namespace {namespace};
use {useStatement};
class {class} extends {extends}
{
<?php if ($type === 'controller'): ?>
/**
* Return an array of resource objects, themselves in array format
*
* @return mixed
*/
public function index()
{
//
}
/**
* Return the properties of a resource object
*
* @return mixed
*/
public function show($id = null)
{
//
}
/**
* Return a new resource object, with default properties
*
* @return mixed
*/
public function new()
{
//
}
/**
* Create a new resource object, from "posted" parameters
*
* @return mixed
*/
public function create()
{
//
}
/**
* Return the editable properties of a resource object
*
* @return mixed
*/
public function edit($id = null)
{
//
}
/**
* Add or update a model resource, from "posted" properties
*
* @return mixed
*/
public function update($id = null)
{
//
}
/**
* Delete the designated resource object from the model
*
* @return mixed
*/
public function delete($id = null)
{
//
}
<?php elseif ($type === 'presenter'): ?>
/**
* Present a view of resource objects
*
* @return mixed
*/
public function index()
{
//
}
/**
* Present a view to present a specific resource object
*
* @param mixed $id
*
* @return mixed
*/
public function show($id = null)
{
//
}
/**
* Present a view to present a new single resource object
*
* @return mixed
*/
public function new()
{
//
}
/**
* Process the creation/insertion of a new resource object.
* This should be a POST.
*
* @return mixed
*/
public function create()
{
//
}
/**
* Present a view to edit the properties of a specific resource object
*
* @param mixed $id
*
* @return mixed
*/
public function edit($id = null)
{
//
}
/**
* Process the updating, full or partial, of a specific resource object.
* This should be a POST.
*
* @param mixed $id
*
* @return mixed
*/
public function update($id = null)
{
//
}
/**
* Present a view to confirm the deletion of a specific resource object
*
* @param mixed $id
*
* @return mixed
*/
public function remove($id = null)
{
//
}
/**
* Process the deletion of a specific resource object
*
* @param mixed $id
*
* @return mixed
*/
public function delete($id = null)
{
//
}
<?php else: ?>
public function index()
{
//
}
<?php endif ?>
}
@@ -0,0 +1,16 @@
<@php
namespace {namespace};
use CodeIgniter\Entity\Entity;
class {class} extends Entity
{
protected $datamap = [];
protected $dates = [
'created_at',
'updated_at',
'deleted_at',
];
protected $casts = [];
}
@@ -0,0 +1,47 @@
<@php
namespace {namespace};
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
class {class} implements FilterInterface
{
/**
* Do whatever processing this filter needs to do.
* By default it should not return anything during
* normal execution. However, when an abnormal state
* is found, it should return an instance of
* CodeIgniter\HTTP\Response. If it does, script
* execution will end and that Response will be
* sent back to the client, allowing for error pages,
* redirects, etc.
*
* @param RequestInterface $request
* @param array|null $arguments
*
* @return mixed
*/
public function before(RequestInterface $request, $arguments = null)
{
//
}
/**
* Allows After filters to inspect and modify the response
* object as needed. This method does not allow any way
* to stop execution of other after filters, short of
* throwing an Exception or Error.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param array|null $arguments
*
* @return mixed
*/
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null)
{
//
}
}
@@ -0,0 +1,44 @@
<@php
namespace {namespace};
use CodeIgniter\Database\Migration;
class {class} extends Migration
{
<?php if ($session): ?>
protected $DBGroup = '<?= $DBGroup ?>';
public function up()
{
$this->forge->addField([
'id' => ['type' => 'VARCHAR', 'constraint' => 128, 'null' => false],
'ip_address' => ['type' => 'VARCHAR', 'constraint' => 45, 'null' => false],
'timestamp' => ['type' => 'INT', 'unsigned' => true, 'null' => false, 'default' => 0],
'data' => ['type' => 'TEXT', 'null' => false, 'default' => ''],
]);
<?php if ($matchIP) : ?>
$this->forge->addKey(['id', 'ip_address'], true);
<?php else: ?>
$this->forge->addKey('id', true);
<?php endif ?>
$this->forge->addKey('timestamp');
$this->forge->createTable('<?= $table ?>', true);
}
public function down()
{
$this->forge->dropTable('<?= $table ?>', true);
}
<?php else: ?>
public function up()
{
//
}
public function down()
{
//
}
<?php endif ?>
}
@@ -0,0 +1,42 @@
<@php
namespace {namespace};
use CodeIgniter\Model;
class {class} extends Model
{
protected $DBGroup = '{DBGroup}';
protected $table = '{table}';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $insertID = 0;
protected $returnType = '{return}';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = [];
// Dates
protected $useTimestamps = false;
protected $dateFormat = 'datetime';
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $deletedField = 'deleted_at';
// Validation
protected $validationRules = [];
protected $validationMessages = [];
protected $skipValidation = false;
protected $cleanValidationRules = true;
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = [];
protected $afterInsert = [];
protected $beforeUpdate = [];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
}
@@ -0,0 +1,13 @@
<@php
namespace {namespace};
use CodeIgniter\Database\Seeder;
class {class} extends Seeder
{
public function run()
{
//
}
}
@@ -0,0 +1,11 @@
<@php
namespace {namespace};
class {class}
{
// public function custom_rule(): bool
// {
// return true;
// }
}
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands;
use CodeIgniter\CLI\BaseCommand;
/**
* CI Help command for the spark script.
*
* Lists the basic usage information for the spark script,
* and provides a way to list help for other commands.
*/
class Help extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'help';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Displays basic usage information.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'help command_name';
/**
* the Command's Arguments
*
* @var array
*/
protected $arguments = [
'command_name' => 'The command name [default: "help"]',
];
/**
* the Command's Options
*
* @var array
*/
protected $options = [];
//--------------------------------------------------------------------
/**
* Displays the help for spark commands.
*
* @param array $params
*/
public function run(array $params)
{
$command = array_shift($params);
$command = $command ?? 'help';
$commands = $this->commands->getCommands();
if (! $this->commands->verifyCommand($command, $commands))
{
return;
}
$class = new $commands[$command]['class']($this->logger, $this->commands);
$class->showHelp();
}
}
@@ -0,0 +1,74 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Housekeeping;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* ClearDebugbar Command
*/
class ClearDebugbar extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Housekeeping';
/**
* The Command's name
*
* @var string
*/
protected $name = 'debugbar:clear';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'debugbar:clear';
/**
* The Command's short description.
*
* @var string
*/
protected $description = 'Clears all debugbar JSON files.';
/**
* Actually runs the command.
*
* @param array $params
*
* @return void
*/
public function run(array $params)
{
helper('filesystem');
if (! delete_files(WRITEPATH . 'debugbar'))
{
// @codeCoverageIgnoreStart
CLI::error('Error deleting the debugbar JSON files.');
CLI::newLine();
return;
// @codeCoverageIgnoreEnd
}
CLI::write('Debugbar cleared.', 'green');
CLI::newLine();
}
}
@@ -0,0 +1,93 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Housekeeping;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* ClearLogs command.
*/
class ClearLogs extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'Housekeeping';
/**
* The Command's name
*
* @var string
*/
protected $name = 'logs:clear';
/**
* The Command's short description
*
* @var string
*/
protected $description = 'Clears all log files.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'logs:clear [option';
/**
* The Command's options
*
* @var array
*/
protected $options = [
'--force' => 'Force delete of all logs files without prompting.',
];
/**
* Actually execute a command.
*
* @param array $params
*/
public function run(array $params)
{
$force = array_key_exists('force', $params) || CLI::getOption('force');
if (! $force && CLI::prompt('Are you sure you want to delete the logs?', ['n', 'y']) === 'n')
{
// @codeCoverageIgnoreStart
CLI::error('Deleting logs aborted.', 'light_gray', 'red');
CLI::error('If you want, use the "-force" option to force delete all log files.', 'light_gray', 'red');
CLI::newLine();
return;
// @codeCoverageIgnoreEnd
}
helper('filesystem');
if (! delete_files(WRITEPATH . 'logs', false, true))
{
// @codeCoverageIgnoreStart
CLI::error('Error in deleting the logs files.', 'light_gray', 'red');
CLI::newLine();
return;
// @codeCoverageIgnoreEnd
}
CLI::write('Logs cleared.', 'green');
CLI::newLine();
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* CI Help command for the spark script.
*
* Lists the basic usage information for the spark script,
* and provides a way to list help for other commands.
*/
class ListCommands extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'list';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Lists the available commands.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'list';
/**
* the Command's Arguments
*
* @var array
*/
protected $arguments = [];
/**
* the Command's Options
*
* @var array
*/
protected $options = [
'--simple' => 'Prints a list of the commands with no other info',
];
//--------------------------------------------------------------------
/**
* Displays the help for the spark cli script itself.
*
* @param array $params
*/
public function run(array $params)
{
$commands = $this->commands->getCommands();
ksort($commands);
// Check for 'simple' format
return array_key_exists('simple', $params) || CLI::getOption('simple')
? $this->listSimple($commands)
: $this->listFull($commands);
}
/**
* Lists the commands with accompanying info.
*
* @param array $commands
*/
protected function listFull(array $commands)
{
// Sort into buckets by group
$groups = [];
foreach ($commands as $title => $command)
{
if (! isset($groups[$command['group']]))
{
$groups[$command['group']] = [];
}
$groups[$command['group']][$title] = $command;
}
$length = max(array_map('strlen', array_keys($commands)));
ksort($groups);
// Display it all...
foreach ($groups as $group => $commands)
{
CLI::write($group, 'yellow');
foreach ($commands as $name => $command)
{
$name = $this->setPad($name, $length, 2, 2);
$output = CLI::color($name, 'green');
if (isset($command['description']))
{
$output .= CLI::wrap($command['description'], 125, strlen($name));
}
CLI::write($output);
}
if ($group !== array_key_last($groups))
{
CLI::newLine();
}
}
}
/**
* Lists the commands only.
*
* @param array $commands
*/
protected function listSimple(array $commands)
{
foreach (array_keys($commands) as $title)
{
CLI::write($title);
}
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Server;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Launch the PHP development server
*
* Not testable, as it throws phpunit for a loop :-/
*
* @codeCoverageIgnore
*/
class Serve extends BaseCommand
{
/**
* Minimum PHP version
*
* @var string
*/
protected $minPHPVersion = '7.3';
/**
* Group
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* Name
*
* @var string
*/
protected $name = 'serve';
/**
* Description
*
* @var string
*/
protected $description = 'Launches the CodeIgniter PHP-Development Server.';
/**
* Usage
*
* @var string
*/
protected $usage = 'serve';
/**
* Arguments
*
* @var array
*/
protected $arguments = [];
/**
* The current port offset.
*
* @var integer
*/
protected $portOffset = 0;
/**
* The max number of ports to attempt to serve from
*
* @var integer
*/
protected $tries = 10;
/**
* Options
*
* @var array
*/
protected $options = [
'--php' => 'The PHP Binary [default: "PHP_BINARY"]',
'--host' => 'The HTTP Host [default: "localhost"]',
'--port' => 'The HTTP Host Port [default: "8080"]',
];
/**
* Run the server
*
* @param array $params Parameters
*
* @return void
*/
public function run(array $params)
{
// Collect any user-supplied options and apply them.
$php = escapeshellarg(CLI::getOption('php') ?? PHP_BINARY);
$host = CLI::getOption('host') ?? 'localhost';
$port = (int) (CLI::getOption('port') ?? 8080) + $this->portOffset;
// Get the party started.
CLI::write('CodeIgniter development server started on http://' . $host . ':' . $port, 'green');
CLI::write('Press Control-C to stop.');
// Set the Front Controller path as Document Root.
$docroot = escapeshellarg(FCPATH);
// Mimic Apache's mod_rewrite functionality with user settings.
$rewrite = escapeshellarg(__DIR__ . '/rewrite.php');
// Call PHP's built-in webserver, making sure to set our
// base path to the public folder, and to use the rewrite file
// to ensure our environment is set and it simulates basic mod_rewrite.
passthru($php . ' -S ' . $host . ':' . $port . ' -t ' . $docroot . ' ' . $rewrite, $status);
if ($status && $this->portOffset < $this->tries)
{
$this->portOffset++;
$this->run($params);
}
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
/**
* 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.
*/
/*
* CodeIgniter PHP-Development Server Rewrite Rules
*
* This script works with the CLI serve command to help run a seamless
* development server based around PHP's built-in development
* server. This file simply tries to mimic Apache's mod_rewrite
* functionality so the site will operate as normal.
*/
// @codeCoverageIgnoreStart
// Avoid this file run when listing commands
if (PHP_SAPI === 'cli')
{
return;
}
$uri = urldecode(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH));
// Front Controller path - expected to be in the default folder
$fcpath = $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR;
// Full path
$path = $fcpath . ltrim($uri, '/');
// If $path is an existing file or folder within the public folder
// then let the request handle it like normal.
if ($uri !== '/' && (is_file($path) || is_dir($path)))
{
return false;
}
// Otherwise, we'll load the index file and let
// the framework handle the request from here.
require_once $fcpath . 'index.php';
// @codeCoverageIgnoreEnd
+160
View File
@@ -0,0 +1,160 @@
<?php
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Config\DotEnv;
/**
* Command to display the current environment,
* or set a new one in the `.env` file.
*/
final class Environment extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'env';
/**
* The Command's short description
*
* @var string
*/
protected $description = 'Retrieves the current environment, or set a new one.';
/**
* The Command's usage
*
* @var string
*/
protected $usage = 'env [<environment>]';
/**
* The Command's arguments
*
* @var array<string, string>
*/
protected $arguments = [
'environment' => '[Optional] The new environment to set. If none is provided, this will print the current environment.',
];
/**
* The Command's options
*
* @var array
*/
protected $options = [];
/**
* Allowed values for environment. `testing` is excluded
* since spark won't work on it.
*
* @var array<int, string>
*/
private static $knownTypes = [
'production',
'development',
];
/**
* @inheritDoc
*
* @param array<string, mixed> $params
*
* @return void
*/
public function run(array $params)
{
if ($params === [])
{
CLI::write(sprintf('Your environment is currently set as %s.', CLI::color($_SERVER['CI_ENVIRONMENT'] ?? ENVIRONMENT, 'green')));
CLI::newLine();
return;
}
$env = strtolower(array_shift($params));
if ($env === 'testing')
{
CLI::error('The "testing" environment is reserved for PHPUnit testing.', 'light_gray', 'red');
CLI::error('You will not be able to run spark under a "testing" environment.', 'light_gray', 'red');
CLI::newLine();
return;
}
if (! in_array($env, self::$knownTypes, true))
{
CLI::error(sprintf('Invalid environment type "%s". Expected one of "%s".', $env, implode('" and "', self::$knownTypes)), 'light_gray', 'red');
CLI::newLine();
return;
}
if (! $this->writeNewEnvironmentToEnvFile($env))
{
CLI::error('Error in writing new environment to .env file.', 'light_gray', 'red');
CLI::newLine();
return;
}
// force DotEnv to reload the new environment
// however we cannot redefine the ENVIRONMENT constant
putenv('CI_ENVIRONMENT');
unset($_ENV['CI_ENVIRONMENT'], $_SERVER['CI_ENVIRONMENT']);
(new DotEnv(ROOTPATH))->load();
CLI::write(sprintf('Environment is successfully changed to "%s".', $env), 'green');
CLI::write('The ENVIRONMENT constant will be changed in the next script execution.');
CLI::newLine();
}
/**
* @see https://regex101.com/r/4sSORp/1 for the regex in action
*
* @param string $newEnv
*
* @return boolean
*/
private function writeNewEnvironmentToEnvFile(string $newEnv): bool
{
$baseEnv = ROOTPATH . 'env';
$envFile = ROOTPATH . '.env';
if (! is_file($envFile))
{
if (! is_file($baseEnv))
{
CLI::write('Both default shipped `env` file and custom `.env` are missing.', 'yellow');
CLI::write('It is impossible to write the new environment type.', 'yellow');
CLI::newLine();
return false;
}
copy($baseEnv, $envFile);
}
$pattern = preg_quote($_SERVER['CI_ENVIRONMENT'] ?? ENVIRONMENT, '/');
$pattern = sprintf('/^[#\s]*CI_ENVIRONMENT[=\s]+%s$/m', $pattern);
return file_put_contents(
$envFile,
preg_replace($pattern, "\nCI_ENVIRONMENT = {$newEnv}", file_get_contents($envFile), -1, $count)
) !== false && $count > 0;
}
}
+99
View File
@@ -0,0 +1,99 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Autoload;
/**
* Lists namespaces set in Config\Autoload with their
* full server path. Helps you to verify that you have
* the namespaces setup correctly.
*/
class Namespaces extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'namespaces';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Verifies your namespaces are setup correctly.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'namespaces';
/**
* the Command's Arguments
*
* @var array
*/
protected $arguments = [];
/**
* the Command's Options
*
* @var array
*/
protected $options = [];
//--------------------------------------------------------------------
/**
* Displays the help for the spark cli script itself.
*
* @param array $params
*/
public function run(array $params)
{
$config = new Autoload();
$tbody = [];
foreach ($config->psr4 as $ns => $path)
{
$path = realpath($path) ?: $path;
$tbody[] = [
$ns,
realpath($path) ?: $path,
is_dir($path) ? 'Yes' : 'MISSING',
];
}
$thead = [
'Namespace',
'Path',
'Found?',
];
CLI::table($tbody, $thead);
}
}
+118
View File
@@ -0,0 +1,118 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Commands\Utilities;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
use Config\Services;
/**
* Lists all of the user-defined routes. This will include any Routes files
* that can be discovered, but will NOT include any routes that are not defined
* in a routes file, but are instead discovered through auto-routing.
*/
class Routes extends BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'routes';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Displays all of user-defined routes. Does NOT display auto-detected routes.';
/**
* the Command's usage
*
* @var string
*/
protected $usage = 'routes';
/**
* the Command's Arguments
*
* @var array
*/
protected $arguments = [];
/**
* the Command's Options
*
* @var array
*/
protected $options = [];
//--------------------------------------------------------------------
/**
* Displays the help for the spark cli script itself.
*
* @param array $params
*/
public function run(array $params)
{
$collection = Services::routes(true);
$methods = [
'get',
'head',
'post',
'patch',
'put',
'delete',
'options',
'trace',
'connect',
'cli',
];
$tbody = [];
foreach ($methods as $method)
{
$routes = $collection->getRoutes($method);
foreach ($routes as $route => $handler)
{
// filter for strings, as callbacks aren't displayable
if (is_string($handler))
{
$tbody[] = [
strtoupper($method),
$route,
$handler,
];
}
}
}
$thead = [
'Method',
'Route',
'Handler',
];
CLI::table($tbody, $thead);
}
}
+1373
View File
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
<?php
/**
* 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.
*/
namespace CodeIgniter;
use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
/**
* This class is used by Composer during installs and updates
* to move files to locations within the system folder so that end-users
* do not need to use Composer to install a package, but can simply
* download.
*
* @codeCoverageIgnore
*
* @internal
*/
final class ComposerScripts
{
/**
* Path to the ThirdParty directory.
*
* @var string
*/
private static $path = __DIR__ . '/ThirdParty/';
/**
* Direct dependencies of CodeIgniter to copy
* contents to `system/ThirdParty/`.
*
* @var array<string, array<string, string>>
*/
private static $dependencies = [
'kint-src' => [
'from' => __DIR__ . '/../vendor/kint-php/kint/src/',
'to' => __DIR__ . '/ThirdParty/Kint/',
],
'kint-resources' => [
'from' => __DIR__ . '/../vendor/kint-php/kint/resources/',
'to' => __DIR__ . '/ThirdParty/Kint/resources/',
],
'escaper' => [
'from' => __DIR__ . '/../vendor/laminas/laminas-escaper/src/',
'to' => __DIR__ . '/ThirdParty/Escaper/',
],
'psr-log' => [
'from' => __DIR__ . '/../vendor/psr/log/Psr/Log/',
'to' => __DIR__ . '/ThirdParty/PSR/Log/',
],
];
/**
* This static method is called by Composer after every update event,
* i.e., `composer install`, `composer update`, `composer remove`.
*
* @return void
*/
public static function postUpdate()
{
self::recursiveDelete(self::$path);
foreach (self::$dependencies as $dependency)
{
self::recursiveMirror($dependency['from'], $dependency['to']);
}
self::copyKintInitFiles();
self::recursiveDelete(self::$dependencies['psr-log']['to'] . 'Test/');
}
/**
* Recursively remove the contents of the previous `system/ThirdParty`.
*
* @param string $directory
*
* @return void
*/
private static function recursiveDelete(string $directory): void
{
if (! is_dir($directory))
{
echo sprintf('Cannot recursively delete "%s" as it does not exist.', $directory);
}
/** @var SplFileInfo $file */
foreach (new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(rtrim($directory, '\\/'), FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
) as $file)
{
$path = $file->getPathname();
if ($file->isDir())
{
@rmdir($path);
}
else
{
@unlink($path);
}
}
}
/**
* Recursively copy the files and directories of the origin directory
* into the target directory, i.e. "mirror" its contents.
*
* @param string $originDir
* @param string $targetDir
*
* @return void
*/
private static function recursiveMirror(string $originDir, string $targetDir): void
{
$originDir = rtrim($originDir, '\\/');
$targetDir = rtrim($targetDir, '\\/');
if (! is_dir($originDir))
{
echo sprintf('The origin directory "%s" was not found.', $originDir);
exit(1);
}
if (is_dir($targetDir))
{
echo sprintf('The target directory "%s" is existing. Run %s::recursiveDelete(\'%s\') first.', $targetDir, self::class, $targetDir);
exit(1);
}
@mkdir($targetDir, 0755, true);
$dirLen = strlen($originDir);
/** @var SplFileInfo $file */
foreach (new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($originDir, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::SELF_FIRST
) as $file)
{
$origin = $file->getPathname();
$target = $targetDir . substr($origin, $dirLen);
if ($file->isDir())
{
@mkdir($target, 0755);
}
else
{
@copy($origin, $target);
}
}
}
/**
* Copy Kint's init files into `system/ThirdParty/Kint/`
*
* @return void
*/
private static function copyKintInitFiles(): void
{
$originDir = self::$dependencies['kint-src']['from'] . '../';
$targetDir = self::$dependencies['kint-src']['to'];
foreach (['init.php', 'init_helpers.php'] as $kintInit)
{
@copy($originDir . $kintInit, $targetDir . $kintInit);
}
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
/**
* AUTOLOADER CONFIGURATION
*
* This file defines the namespaces and class maps so the Autoloader
* can find the files as needed.
*/
class AutoloadConfig
{
/**
* -------------------------------------------------------------------
* Namespaces
* -------------------------------------------------------------------
* This maps the locations of any namespaces in your application to
* their location on the file system. These are used by the autoloader
* to locate files the first time they have been instantiated.
*
* The '/app' and '/system' directories are already mapped for you.
* you may change the name of the 'App' namespace if you wish,
* but this should be done prior to creating any namespaced classes,
* else you will need to modify all of those classes for this to work.
*
* @var array<string, string>
*/
public $psr4 = [];
/**
* -------------------------------------------------------------------
* Class Map
* -------------------------------------------------------------------
* The class map provides a map of class names and their exact
* location on the drive. Classes loaded in this manner will have
* slightly faster performance because they will not have to be
* searched for within one or more directories as they would if they
* were being autoloaded through a namespace.
*
* @var array<string, string>
*/
public $classmap = [];
/**
* -------------------------------------------------------------------
* Files
* -------------------------------------------------------------------
* The files array provides a list of paths to __non-class__ files
* that will be autoloaded. This can be useful for bootstrap operations
* or for loading functions.
*
* @var array<int, string>
*/
public $files = [];
/**
* -------------------------------------------------------------------
* Namespaces
* -------------------------------------------------------------------
* This maps the locations of any namespaces in your application to
* their location on the file system. These are used by the autoloader
* to locate files the first time they have been instantiated.
*
* Do not change the name of the CodeIgniter namespace or your application
* will break.
*
* @var array<string, string>
*/
protected $corePsr4 = [
'CodeIgniter' => SYSTEMPATH,
'App' => APPPATH, // To ensure filters, etc still found,
];
/**
* -------------------------------------------------------------------
* Class Map
* -------------------------------------------------------------------
* The class map provides a map of class names and their exact
* location on the drive. Classes loaded in this manner will have
* slightly faster performance because they will not have to be
* searched for within one or more directories as they would if they
* were being autoloaded through a namespace.
*
* @var array<string, string>
*/
protected $coreClassmap = [
'Psr\Log\AbstractLogger' => SYSTEMPATH . 'ThirdParty/PSR/Log/AbstractLogger.php',
'Psr\Log\InvalidArgumentException' => SYSTEMPATH . 'ThirdParty/PSR/Log/InvalidArgumentException.php',
'Psr\Log\LoggerAwareInterface' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareInterface.php',
'Psr\Log\LoggerAwareTrait' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerAwareTrait.php',
'Psr\Log\LoggerInterface' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerInterface.php',
'Psr\Log\LoggerTrait' => SYSTEMPATH . 'ThirdParty/PSR/Log/LoggerTrait.php',
'Psr\Log\LogLevel' => SYSTEMPATH . 'ThirdParty/PSR/Log/LogLevel.php',
'Psr\Log\NullLogger' => SYSTEMPATH . 'ThirdParty/PSR/Log/NullLogger.php',
'Laminas\Escaper\Escaper' => SYSTEMPATH . 'ThirdParty/Escaper/Escaper.php',
];
/**
* -------------------------------------------------------------------
* Core Files
* -------------------------------------------------------------------
* List of files from the framework to be autoloaded early.
*
* @var array<int, string>
*/
protected $coreFiles = [];
/**
* Constructor.
*
* Merge the built-in and developer-configured psr4 and classmap,
* with preference to the developer ones.
*/
public function __construct()
{
if (isset($_SERVER['CI_ENVIRONMENT']) && $_SERVER['CI_ENVIRONMENT'] === 'testing')
{
$this->psr4['Tests\Support'] = SUPPORTPATH;
$this->classmap['CodeIgniter\Log\TestLogger'] = SYSTEMPATH . 'Test/TestLogger.php';
$this->classmap['CIDatabaseTestCase'] = SYSTEMPATH . 'Test/CIDatabaseTestCase.php';
}
$this->psr4 = array_merge($this->corePsr4, $this->psr4);
$this->classmap = array_merge($this->coreClassmap, $this->classmap);
$this->files = array_merge($this->coreFiles, $this->files);
}
}
+216
View File
@@ -0,0 +1,216 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
use Config\Encryption;
use Config\Modules;
use Config\Services;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
/**
* 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.
*/
class BaseConfig
{
/**
* An optional array of classes that will act as Registrars
* for rapidly setting config class properties.
*
* @var array
*/
public static $registrars = [];
/**
* Has module discovery happened yet?
*
* @var boolean
*/
protected static $didDiscovery = false;
/**
* The modules configuration.
*
* @var Modules
*/
protected static $moduleConfig;
/**
* 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()
{
static::$moduleConfig = config('Modules');
$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')
{
// Handle hex2bin prefix
if (strpos($this->$property, 'hex2bin:') === 0)
{
$this->$property = hex2bin(substr($this->$property, 8));
}
// Handle base64 prefix
elseif (strpos($this->$property, 'base64:') === 0)
{
$this->$property = base64_decode(substr($this->$property, 7), true);
}
}
}
}
/**
* Initialization an environment-specific configuration setting
*
* @param mixed $property
* @param string $name
* @param string $prefix
* @param string $shortPrefix
*
* @return mixed
*/
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 && ! is_null($value))
{
if ($value === 'false')
{
$value = false;
}
elseif ($value === 'true')
{
$value = true;
}
$property = is_bool($value) ? $value : trim($value, '\'"');
}
return $property;
}
/**
* Retrieve an environment-specific configuration setting
*
* @param string $property
* @param string $prefix
* @param string $shortPrefix
*
* @return mixed
*/
protected function getEnvValue(string $property, string $prefix, string $shortPrefix)
{
$shortPrefix = ltrim($shortPrefix, '\\');
switch (true)
{
case array_key_exists("{$shortPrefix}.{$property}", $_ENV):
return $_ENV["{$shortPrefix}.{$property}"];
case array_key_exists("{$shortPrefix}.{$property}", $_SERVER):
return $_SERVER["{$shortPrefix}.{$property}"];
case array_key_exists("{$prefix}.{$property}", $_ENV):
return $_ENV["{$prefix}.{$property}"];
case array_key_exists("{$prefix}.{$property}", $_SERVER):
return $_SERVER["{$prefix}.{$property}"];
default:
$value = getenv("{$shortPrefix}.{$property}");
$value = $value === false ? getenv("{$prefix}.{$property}") : $value;
return $value === false ? null : $value;
}
}
/**
* Provides external libraries a simple way to register one or more
* options into a config file.
*
* @throws ReflectionException
*/
protected function registerProperties()
{
if (! static::$moduleConfig->shouldDiscover('registrars'))
{
return;
}
if (! static::$didDiscovery)
{
$locator = Services::locator();
$registrarsFiles = $locator->search('Config/Registrar.php');
foreach ($registrarsFiles as $file)
{
$className = $locator->getClassname($file);
static::$registrars[] = new $className();
}
static::$didDiscovery = true;
}
$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))
{
// @codeCoverageIgnoreStart
continue;
// @codeCoverageIgnoreEnd
}
$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;
}
}
}
}
}
+420
View File
@@ -0,0 +1,420 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
use CodeIgniter\Autoloader\Autoloader;
use CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Cache\CacheInterface;
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\Filters\Filters;
use CodeIgniter\Format\Format;
use CodeIgniter\Honeypot\Honeypot;
use CodeIgniter\HTTP\CLIRequest;
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\Response;
use CodeIgniter\HTTP\ResponseInterface;
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\Throttle\Throttler;
use CodeIgniter\Typography\Typography;
use CodeIgniter\Validation\Validation;
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\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\Pager as ConfigPager;
use Config\Services as AppServices;
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 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, IncomingRequest $request = null, Response $response = 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 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 View renderer($viewPath = null, ConfigView $config = null, $getShared = true)
* @method static IncomingRequest request(App $config = null, $getShared = true)
* @method static Response response(App $config = null, $getShared = true)
* @method static RedirectResponse redirectresponse(App $config = null, $getShared = true)
* @method static RouteCollection routes($getShared = true)
* @method static Router router(RouteCollectionInterface $routes = null, Request $request = null, $getShared = true)
* @method static Security security(App $config = null, $getShared = true)
* @method static Session session(App $config = null, $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 URI uri($uri = null, $getShared = true)
* @method static Validation validation(ConfigValidation $config = null, $getShared = true)
* @method static Cell viewcell($getShared = true)
* @method static Typography typography($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
*/
protected static $instances = [];
/**
* Mock objects for testing which are returned if exist.
*
* @var array
*/
protected static $mocks = [];
/**
* Have we already discovered other Services?
*
* @var boolean
*/
protected static $discovered = false;
/**
* A cache of other service classes we've found.
*
* @var array
*/
protected static $services = [];
/**
* A cache of the names of services classes found.
*
* @var array<string>
*/
private static $serviceNames = [];
/**
* Returns a shared instance of any of the class' services.
*
* $key must be a name matching a service.
*
* @param string $key
* @param mixed ...$params
*
* @return mixed
*/
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.
*
* @param boolean $getShared
*
* @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'.
*
* @param boolean $getShared
*
* @return FileLocator
*/
public static function locator(bool $getShared = true)
{
if ($getShared)
{
if (empty(static::$instances['locator']))
{
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.
*
* @param string $name
* @param array $arguments
*
* @return mixed
*/
public static function __callStatic(string $name, array $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.
*
* @param string $name
*
* @return string|null
*/
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))
{
return $service;
}
}
return null;
}
/**
* Reset shared instances and mocks for testing.
*
* @param boolean $initAutoloader Initializes autoloader instance
*/
public static function reset(bool $initAutoloader = false)
{
static::$mocks = [];
static::$instances = [];
if ($initAutoloader)
{
static::autoloader()->initialize(new Autoload(), new Modules());
}
}
/**
* Resets any mock and shared instances for a single service.
*
* @param string $name
*/
public static function resetSingle(string $name)
{
unset(static::$mocks[$name], static::$instances[$name]);
}
/**
* Inject mock object for testing.
*
* @param string $name
* @param mixed $mock
*/
public static function injectMock(string $name, $mock)
{
static::$mocks[strtolower($name)] = $mock;
}
/**
* Will scan all psr4 namespaces registered with system to look
* for new Config\Services files. Caches a copy of each one, then
* looks for the service method in each, returning an instance of
* the service, if available.
*
* @param string $name
* @param array $arguments
*
* @return mixed
*
* @deprecated
*
* @codeCoverageIgnore
*/
protected static function discoverServices(string $name, array $arguments)
{
if (! static::$discovered)
{
$config = config('Modules');
if ($config->shouldDiscover('services'))
{
$locator = static::locator();
$files = $locator->search('Config/Services');
if (empty($files))
{
// no files at all found - this would be really, really bad
return null;
}
// Get instances of all service classes and cache them locally.
foreach ($files as $file)
{
$classname = $locator->getClassname($file);
if (! in_array($classname, ['CodeIgniter\\Config\\Services'], true))
{
static::$services[] = new $classname();
}
}
}
static::$discovered = true;
}
if (! static::$services)
{
// we found stuff, but no services - this would be really bad
return null;
}
// Try to find the desired service method
foreach (static::$services as $class)
{
if (method_exists($class, $name))
{
return $class::$name(...$arguments);
}
}
return null;
}
protected static function buildServicesCache(): void
{
if (! static::$discovered)
{
$config = config('Modules');
if ($config->shouldDiscover('services'))
{
$locator = static::locator();
$files = $locator->search('Config/Services');
// Get instances of all service classes and cache them locally.
foreach ($files as $file)
{
$classname = $locator->getClassname($file);
if ($classname !== 'CodeIgniter\\Config\\Services')
{
self::$serviceNames[] = $classname;
static::$services[] = new $classname();
}
}
}
static::$discovered = true;
}
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
/**
* Class Config
*
* @deprecated Use CodeIgniter\Config\Factories::config()
*/
class Config
{
/**
* Create new configuration instances or return
* a shared instance
*
* @param string $name Configuration name
* @param boolean $getShared Use shared instance
*
* @return mixed|null
*/
public static function get(string $name, bool $getShared = true)
{
return Factories::config($name, ['getShared' => $getShared]);
}
/**
* Helper method for injecting mock instances while testing.
*
* @param string $name
* @param object $instance
*/
public static function injectMock(string $name, $instance)
{
Factories::injectMock('config', $name, $instance);
}
/**
* Resets the static arrays
*/
public static function reset()
{
Factories::reset('config');
}
}
+293
View File
@@ -0,0 +1,293 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
use InvalidArgumentException;
/**
* Environment-specific configuration
*/
class DotEnv
{
/**
* The directory where the .env file can be located.
*
* @var string
*/
protected $path;
//--------------------------------------------------------------------
/**
* Builds the path to our file.
*
* @param string $path
* @param string $file
*/
public function __construct(string $path, string $file = '.env')
{
$this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file;
}
//--------------------------------------------------------------------
/**
* The main entry point, will load the .env file and process it
* so that we end up with all settings in the PHP environment vars
* (i.e. getenv(), $_ENV, and $_SERVER)
*
* @return boolean
*/
public function load(): bool
{
$vars = $this->parse();
return $vars !== null;
}
//--------------------------------------------------------------------
/**
* Parse the .env file into an array of key => value
*
* @return array|null
*/
public function parse(): ?array
{
// We don't want to enforce the presence of a .env file, they should be optional.
if (! is_file($this->path))
{
return null;
}
// Ensure the file is readable
if (! is_readable($this->path))
{
throw new InvalidArgumentException("The .env file is not readable: {$this->path}");
}
$vars = [];
$lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line)
{
// Is it a comment?
if (strpos(trim($line), '#') === 0)
{
continue;
}
// If there is an equal sign, then we know we are assigning a variable.
if (strpos($line, '=') !== false)
{
[$name, $value] = $this->normaliseVariable($line);
$vars[$name] = $value;
$this->setVariable($name, $value);
}
}
return $vars;
}
//--------------------------------------------------------------------
/**
* Sets the variable into the environment. Will parse the string
* first to look for {name}={value} pattern, ensure that nested
* variables are handled, and strip it of single and double quotes.
*
* @param string $name
* @param string $value
*/
protected function setVariable(string $name, string $value = '')
{
if (! getenv($name, true))
{
putenv("$name=$value");
}
if (empty($_ENV[$name]))
{
$_ENV[$name] = $value;
}
if (empty($_SERVER[$name]))
{
$_SERVER[$name] = $value;
}
}
//--------------------------------------------------------------------
/**
* Parses for assignment, cleans the $name and $value, and ensures
* that nested variables are handled.
*
* @param string $name
* @param string $value
*
* @return array
*/
public function normaliseVariable(string $name, string $value = ''): array
{
// Split our compound string into its parts.
if (strpos($name, '=') !== false)
{
[$name, $value] = explode('=', $name, 2);
}
$name = trim($name);
$value = trim($value);
// Sanitize the name
$name = str_replace(['export', '\'', '"'], '', $name);
// Sanitize the value
$value = $this->sanitizeValue($value);
$value = $this->resolveNestedVariables($value);
return [
$name,
$value,
];
}
//--------------------------------------------------------------------
/**
* Strips quotes from the environment variable value.
*
* This was borrowed from the excellent phpdotenv with very few changes.
* https://github.com/vlucas/phpdotenv
*
* @param string $value
*
* @return string
* @throws InvalidArgumentException
*/
protected function sanitizeValue(string $value): string
{
if (! $value)
{
return $value;
}
// Does it begin with a quote?
if (strpbrk($value[0], '"\'') !== false)
{
// value starts with a quote
$quote = $value[0];
$regexPattern = sprintf(
'/^
%1$s # match a quote at the start of the value
( # capturing sub-pattern used
(?: # we do not need to capture this
[^%1$s\\\\] # any character other than a quote or backslash
|\\\\\\\\ # or two backslashes together
|\\\\%1$s # or an escaped quote e.g \"
)* # as many characters that match the previous rules
) # end of the capturing sub-pattern
%1$s # and the closing quote
.*$ # and discard any string after the closing quote
/mx', $quote
);
$value = preg_replace($regexPattern, '$1', $value);
$value = str_replace("\\$quote", $quote, $value);
$value = str_replace('\\\\', '\\', $value);
}
else
{
$parts = explode(' #', $value, 2);
$value = trim($parts[0]);
// Unquoted values cannot contain whitespace
if (preg_match('/\s+/', $value) > 0)
{
throw new InvalidArgumentException('.env values containing spaces must be surrounded by quotes.');
}
}
return $value;
}
//--------------------------------------------------------------------
/**
* Resolve the nested variables.
*
* Look for ${varname} patterns in the variable value and replace with an existing
* environment variable.
*
* This was borrowed from the excellent phpdotenv with very few changes.
* https://github.com/vlucas/phpdotenv
*
* @param string $value
*
* @return string
*/
protected function resolveNestedVariables(string $value): string
{
if (strpos($value, '$') !== false)
{
$value = preg_replace_callback(
'/\${([a-zA-Z0-9_\.]+)}/',
function ($matchedPatterns) {
$nestedVariable = $this->getVariable($matchedPatterns[1]);
if (is_null($nestedVariable))
{
return $matchedPatterns[0];
}
return $nestedVariable;
},
$value
);
}
return $value;
}
//--------------------------------------------------------------------
/**
* Search the different places for environment variables and return first value found.
*
* This was borrowed from the excellent phpdotenv with very few changes.
* https://github.com/vlucas/phpdotenv
*
* @param string $name
*
* @return string|null
*/
protected function getVariable(string $name)
{
switch (true)
{
case array_key_exists($name, $_ENV):
return $_ENV[$name];
case array_key_exists($name, $_SERVER):
return $_SERVER[$name];
default:
$value = getenv($name);
// switch getenv default to null
return $value === false ? null : $value;
}
}
//--------------------------------------------------------------------
}
+354
View File
@@ -0,0 +1,354 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
use CodeIgniter\Model;
use Config\Services;
/**
* Factories for creating instances.
*
* Factories allows 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 Model models(...$arguments)
* @method static BaseConfig config(...$arguments)
*/
class Factories
{
/**
* Store of component-specific options, usually
* from CodeIgniter\Config\Factory.
*
* @var array<string, array>
*/
protected static $options = [];
/**
* Explicit options for the Config
* component to prevent logic loops.
*
* @var array<string, mixed>
*/
private static $configOptions = [
'component' => 'config',
'path' => 'Config',
'instanceOf' => null,
'getShared' => true,
'preferApp' => true,
];
/**
* Mapping of class basenames (no namespace) to
* their instances.
*
* @var array<string, string[]>
*/
protected static $basenames = [];
/**
* 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.
*
* @var array<string, array>
*/
protected static $instances = [];
//--------------------------------------------------------------------
/**
* Loads instances based on the method component name. Either
* creates a new instance or returns an existing shared instance.
*
* @param string $component
* @param array $arguments
*
* @return mixed
*/
public static function __callStatic(string $component, array $arguments)
{
// First argument is the name, second is options
$name = trim(array_shift($arguments), '\\ ');
$options = array_shift($arguments) ?? [];
// Determine the component-specific options
$options = array_merge(self::getOptions(strtolower($component)), $options);
if (! $options['getShared'])
{
if ($class = self::locateClass($options, $name))
{
return new $class(...$arguments);
}
return null;
}
$basename = self::getBasename($name);
// Check for an existing instance
if (isset(self::$basenames[$options['component']][$basename]))
{
$class = self::$basenames[$options['component']][$basename];
// Need to verify if the shared instance matches the request
if (self::verifyInstanceOf($options, $class))
{
return self::$instances[$options['component']][$class];
}
}
// Try to locate the class
if (! $class = self::locateClass($options, $name))
{
return null;
}
self::$instances[$options['component']][$class] = new $class(...$arguments);
self::$basenames[$options['component']][$basename] = $class;
return self::$instances[$options['component']][$class];
}
/**
* Finds a component class
*
* @param array $options The array of component-specific directives
* @param string $name Class name, namespace optional
*
* @return string|null
*/
protected static function locateClass(array $options, string $name): ?string
{
// Check for low-hanging fruit
if (class_exists($name, false) && self::verifyPreferApp($options, $name) && self::verifyInstanceOf($options, $name))
{
return $name;
}
// Determine the relative class names we need
$basename = self::getBasename($name);
$appname = $options['component'] === 'config'
? 'Config\\' . $basename
: rtrim(APP_NAMESPACE, '\\') . '\\' . $options['path'] . '\\' . $basename;
// If an App version was requested then see if it verifies
if ($options['preferApp'] && class_exists($appname) && self::verifyInstanceOf($options, $name))
{
return $appname;
}
// If we have ruled out an App version and the class exists then try it
if (class_exists($name) && self::verifyInstanceOf($options, $name))
{
return $name;
}
// Have to do this the hard way...
$locator = Services::locator();
// Check if the class was namespaced
if (strpos($name, '\\') !== false)
{
if (! $file = $locator->locateFile($name, $options['path']))
{
return null;
}
$files = [$file];
}
// No namespace? Search for it
// Check all namespaces, prioritizing App and modules
elseif (! $files = $locator->search($options['path'] . DIRECTORY_SEPARATOR . $name))
{
return null;
}
// Check all files for a valid class
foreach ($files as $file)
{
$class = $locator->getClassname($file);
if ($class && self::verifyInstanceOf($options, $class))
{
return $class;
}
}
return null;
}
//--------------------------------------------------------------------
/**
* Verifies that a class & config satisfy the "preferApp" option
*
* @param array $options The array of component-specific directives
* @param string $name Class name, namespace optional
*
* @return boolean
*/
protected static function verifyPreferApp(array $options, string $name): bool
{
// Anything without that restriction passes
if (! $options['preferApp'])
{
return true;
}
// Special case for Config since its App namespace is actually \Config
if ($options['component'] === 'config')
{
return strpos($name, 'Config') === 0;
}
return strpos($name, APP_NAMESPACE) === 0;
}
/**
* Verifies that a class & config satisfy the "instanceOf" option
*
* @param array $options The array of component-specific directives
* @param string $name Class name, namespace optional
*
* @return boolean
*/
protected static function verifyInstanceOf(array $options, string $name): bool
{
// Anything without that restriction passes
if (! $options['instanceOf'])
{
return true;
}
return is_a($name, $options['instanceOf'], true);
}
//--------------------------------------------------------------------
/**
* Returns the component-specific configuration
*
* @param string $component Lowercase, plural component name
*
* @return array<string, mixed>
*/
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 = $component === 'config'
// 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 ?? [];
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
*
* @return array<string, mixed> The result after applying defaults and normalization
*/
public static function setOptions(string $component, array $values): array
{
// 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 $component Lowercase, plural component name
*/
public static function reset(string $component = null)
{
if ($component)
{
unset(static::$options[$component]);
unset(static::$basenames[$component]);
unset(static::$instances[$component]);
return;
}
static::$options = [];
static::$basenames = [];
static::$instances = [];
}
/**
* Helper method for injecting mock instances
*
* @param string $component Lowercase, plural component name
* @param string $name The name of the instance
* @param object $instance
*/
public static function injectMock(string $component, string $name, object $instance)
{
// Force a configuration to exist for this component
$component = strtolower($component);
self::getOptions($component);
$class = get_class($instance);
$basename = self::getBasename($name);
self::$instances[$component][$class] = $instance;
self::$basenames[$component][$basename] = $class;
}
/**
* Gets a basename from a class name, namespaced or not.
*
* @param string $name
*
* @return string
*/
public static function getBasename(string $name): string
{
// Determine the basename
if ($basename = strrchr($name, '\\'))
{
return substr($basename, 1);
}
return $name;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
/**
* 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.
*/
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,
];
}
+113
View File
@@ -0,0 +1,113 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
/**
* Describes foreign characters for transliteration with the text helper.
*/
class ForeignCharacters
{
/**
* Without further ado, the list of foreign characters.
*/
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',
];
}
+40
View File
@@ -0,0 +1,40 @@
<?php
/**
* 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.
*/
use CodeIgniter\Exceptions\PageNotFoundException;
/**
* System URI Routing
*
* This file contains any routing to system tools, such as command-line
* tools for migrations, etc.
*
* It is called by Config\Routes, and has the $routes RouteCollection
* already loaded up and ready for us to use.
*/
// Prevent access to BaseController
$routes->add('BaseController(:any)', function () {
throw PageNotFoundException::forPageNotFound();
});
// Prevent access to initController method
$routes->add('(:any)/initController', function () {
throw PageNotFoundException::forPageNotFound();
});
// Migrations
$routes->cli('migrations/(:segment)/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1/$2');
$routes->cli('migrations/(:segment)', '\CodeIgniter\Commands\MigrationsCommand::$1');
$routes->cli('migrations', '\CodeIgniter\Commands\MigrationsCommand::index');
// CLI Catchall - uses a _remap to call Commands
$routes->cli('ci(:any)', '\CodeIgniter\CLI\CommandRunner::index/$1');
+884
View File
@@ -0,0 +1,884 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\Cache\CacheInterface;
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\Encryption\Encryption;
use CodeIgniter\Filters\Filters;
use CodeIgniter\Format\Format;
use CodeIgniter\Honeypot\Honeypot;
use CodeIgniter\HTTP\CLIRequest;
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\Response;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
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\Throttle\Throttler;
use CodeIgniter\Typography\Typography;
use CodeIgniter\Validation\Validation;
use CodeIgniter\View\Cell;
use CodeIgniter\View\Parser;
use CodeIgniter\View\RendererInterface;
use CodeIgniter\View\View;
use Config\App;
use Config\Cache;
use Config\Email as EmailConfig;
use Config\Encryption as EncryptionConfig;
use Config\Exceptions as ExceptionsConfig;
use Config\Filters as FiltersConfig;
use Config\Format as FormatConfig;
use Config\Honeypot as HoneypotConfig;
use Config\Images;
use Config\Migrations;
use Config\Pager as PagerConfig;
use Config\Toolbar as ToolbarConfig;
use Config\Validation as ValidationConfig;
use Config\View as ViewConfig;
use Config\Services as AppServices;
/**
* 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
*/
class Services extends BaseService
{
/**
* The cache class provides a simple way to store and retrieve
* complex data for later.
*
* @param Cache|null $config
* @param boolean $getShared
*
* @return CacheInterface
*/
public static function cache(Cache $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('cache', $config);
}
$config = $config ?? new Cache();
return CacheFactory::getHandler($config);
}
//--------------------------------------------------------------------
/**
* The CLI Request class provides for ways to interact with
* a command line request.
*
* @param App|null $config
* @param boolean $getShared
*
* @return CLIRequest
*/
public static function clirequest(App $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('clirequest', $config);
}
$config = $config ?? config('App');
return new CLIRequest($config);
}
//--------------------------------------------------------------------
/**
* CodeIgniter, the core of the framework.
*
* @param App|null $config
* @param boolean $getShared
*
* @return CodeIgniter
*/
public static function codeigniter(App $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('codeigniter', $config);
}
$config = $config ?? config('App');
return new CodeIgniter($config);
}
/**
* The commands utility for running and working with CLI commands.
*
* @param boolean $getShared
*
* @return Commands
*/
public static function commands(bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('commands');
}
return new Commands();
}
/**
* The CURL Request class acts as a simple HTTP client for interacting
* with other servers, typically through APIs.
*
* @param array $options
* @param ResponseInterface|null $response
* @param App|null $config
* @param boolean $getShared
*
* @return CURLRequest
*/
public static function curlrequest(array $options = [], ResponseInterface $response = null, App $config = null, bool $getShared = true)
{
if ($getShared === true)
{
return static::getSharedInstance('curlrequest', $options, $response, $config);
}
$config = $config ?? config('App');
$response = $response ?? new Response($config);
return new CURLRequest(
$config,
new URI($options['base_uri'] ?? null),
$response,
$options
);
}
//--------------------------------------------------------------------
/**
* The Email class allows you to send email via mail, sendmail, SMTP.
*
* @param EmailConfig|array|null $config
* @param boolean $getShared
*
* @return Email
*/
public static function email($config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('email', $config);
}
if (empty($config) || ! (is_array($config) || $config instanceof EmailConfig))
{
$config = config('Email');
}
return new Email($config);
}
/**
* The Encryption class provides two-way encryption.
*
* @param EncryptionConfig|null $config
* @param boolean $getShared
*
* @return EncrypterInterface Encryption handler
*/
public static function encrypter(EncryptionConfig $config = null, $getShared = false)
{
if ($getShared === true)
{
return static::getSharedInstance('encrypter', $config);
}
$config = $config ?? config('Encryption');
$encryption = new Encryption($config);
return $encryption->initialize($config);
}
//--------------------------------------------------------------------
/**
* The Exceptions class holds the methods that handle:
*
* - set_exception_handler
* - set_error_handler
* - register_shutdown_function
*
* @param ExceptionsConfig|null $config
* @param IncomingRequest|null $request
* @param Response|null $response
* @param boolean $getShared
*
* @return Exceptions
*/
public static function exceptions(
ExceptionsConfig $config = null,
IncomingRequest $request = null,
Response $response = null,
bool $getShared = true
)
{
if ($getShared)
{
return static::getSharedInstance('exceptions', $config, $request, $response);
}
$config = $config ?? config('Exceptions');
$request = $request ?? AppServices::request();
$response = $response ?? AppServices::response();
return new Exceptions($config, $request, $response);
}
//--------------------------------------------------------------------
/**
* Filters allow you to run tasks before and/or after a controller
* is executed. During before filters, the request can be modified,
* and actions taken based on the request, while after filters can
* act on or modify the response itself before it is sent to the client.
*
* @param FiltersConfig|null $config
* @param boolean $getShared
*
* @return Filters
*/
public static function filters(FiltersConfig $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('filters', $config);
}
$config = $config ?? config('Filters');
return new Filters($config, AppServices::request(), AppServices::response());
}
//--------------------------------------------------------------------
/**
* The Format class is a convenient place to create Formatters.
*
* @param FormatConfig|null $config
* @param boolean $getShared
*
* @return Format
*/
public static function format(FormatConfig $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('format', $config);
}
$config = $config ?? config('Format');
return new Format($config);
}
//--------------------------------------------------------------------
/**
* The Honeypot provides a secret input on forms that bots should NOT
* fill in, providing an additional safeguard when accepting user input.
*
* @param HoneypotConfig|null $config
* @param boolean $getShared
*
* @return Honeypot
*/
public static function honeypot(HoneypotConfig $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('honeypot', $config);
}
$config = $config ?? config('Honeypot');
return new Honeypot($config);
}
//--------------------------------------------------------------------
/**
* Acts as a factory for ImageHandler classes and returns an instance
* of the handler. Used like Services::image()->withFile($path)->rotate(90)->save();
*
* @param string|null $handler
* @param Images|null $config
* @param boolean $getShared
*
* @return BaseHandler
*/
public static function image(string $handler = null, Images $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('image', $handler, $config);
}
$config = $config ?? config('Images');
$handler = $handler ?: $config->defaultHandler;
$class = $config->handlers[$handler];
return new $class($config);
}
//--------------------------------------------------------------------
/**
* The Iterator class provides a simple way of looping over a function
* and timing the results and memory usage. Used when debugging and
* optimizing applications.
*
* @param boolean $getShared
*
* @return Iterator
*/
public static function iterator(bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('iterator');
}
return new Iterator();
}
//--------------------------------------------------------------------
/**
* Responsible for loading the language string translations.
*
* @param string|null $locale
* @param boolean $getShared
*
* @return Language
*/
public static function language(string $locale = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('language', $locale)->setLocale($locale);
}
// Use '?:' for empty string check
$locale = $locale ?: AppServices::request()->getLocale();
return new Language($locale);
}
//--------------------------------------------------------------------
/**
* The Logger class is a PSR-3 compatible Logging class that supports
* multiple handlers that process the actual logging.
*
* @param boolean $getShared
*
* @return Logger
*/
public static function logger(bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('logger');
}
return new Logger(config('Logger'));
}
//--------------------------------------------------------------------
/**
* Return the appropriate Migration runner.
*
* @param Migrations|null $config
* @param ConnectionInterface|null $db
* @param boolean $getShared
*
* @return MigrationRunner
*/
public static function migrations(Migrations $config = null, ConnectionInterface $db = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('migrations', $config, $db);
}
$config = $config ?? config('Migrations');
return new MigrationRunner($config, $db);
}
//--------------------------------------------------------------------
/**
* The Negotiate class provides the content negotiation features for
* working the request to determine correct language, encoding, charset,
* and more.
*
* @param RequestInterface|null $request
* @param boolean $getShared
*
* @return Negotiate
*/
public static function negotiator(RequestInterface $request = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('negotiator', $request);
}
$request = $request ?? AppServices::request();
return new Negotiate($request);
}
//--------------------------------------------------------------------
/**
* Return the appropriate pagination handler.
*
* @param PagerConfig|null $config
* @param RendererInterface|null $view
* @param boolean $getShared
*
* @return Pager
*/
public static function pager(PagerConfig $config = null, RendererInterface $view = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('pager', $config, $view);
}
$config = $config ?? config('Pager');
$view = $view ?? AppServices::renderer();
return new Pager($config, $view);
}
//--------------------------------------------------------------------
/**
* The Parser is a simple template parser.
*
* @param string|null $viewPath
* @param ViewConfig|null $config
* @param boolean $getShared
*
* @return Parser
*/
public static function parser(string $viewPath = null, ViewConfig $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('parser', $viewPath, $config);
}
$viewPath = $viewPath ?: config('Paths')->viewDirectory;
$config = $config ?? config('View');
return new Parser($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());
}
//--------------------------------------------------------------------
/**
* The Renderer class is the class that actually displays a file to the user.
* The default View class within CodeIgniter is intentionally simple, but this
* service could easily be replaced by a template engine if the user needed to.
*
* @param string|null $viewPath
* @param ViewConfig|null $config
* @param boolean $getShared
*
* @return View
*/
public static function renderer(string $viewPath = null, ViewConfig $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('renderer', $viewPath, $config);
}
$viewPath = $viewPath ?: config('Paths')->viewDirectory;
$config = $config ?? config('View');
return new View($config, $viewPath, AppServices::locator(), CI_DEBUG, AppServices::logger());
}
//--------------------------------------------------------------------
/**
* The Request class models an HTTP request.
*
* @param App|null $config
* @param boolean $getShared
*
* @return IncomingRequest
*/
public static function request(App $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('request', $config);
}
$config = $config ?? config('App');
return new IncomingRequest(
$config,
AppServices::uri(),
'php://input',
new UserAgent()
);
}
//--------------------------------------------------------------------
/**
* The Response class models an HTTP response.
*
* @param App|null $config
* @param boolean $getShared
*
* @return Response
*/
public static function response(App $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('response', $config);
}
$config = $config ?? config('App');
return new Response($config);
}
//--------------------------------------------------------------------
/**
* The Redirect class provides nice way of working with redirects.
*
* @param App|null $config
* @param boolean $getShared
*
* @return RedirectResponse
*/
public static function redirectresponse(App $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('redirectresponse', $config);
}
$config = $config ?? config('App');
$response = new RedirectResponse($config);
$response->setProtocolVersion(AppServices::request()->getProtocolVersion());
return $response;
}
//--------------------------------------------------------------------
/**
* The Routes service is a class that allows for easily building
* a collection of routes.
*
* @param boolean $getShared
*
* @return RouteCollection
*/
public static function routes(bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('routes');
}
return new RouteCollection(AppServices::locator(), config('Modules'));
}
//--------------------------------------------------------------------
/**
* The Router class uses a RouteCollection's array of routes, and determines
* the correct Controller and Method to execute.
*
* @param RouteCollectionInterface|null $routes
* @param Request|null $request
* @param boolean $getShared
*
* @return Router
*/
public static function router(RouteCollectionInterface $routes = null, Request $request = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('router', $routes, $request);
}
$routes = $routes ?? AppServices::routes();
$request = $request ?? AppServices::request();
return new Router($routes, $request);
}
//--------------------------------------------------------------------
/**
* The Security class provides a few handy tools for keeping the site
* secure, most notably the CSRF protection tools.
*
* @param App|null $config
* @param boolean $getShared
*
* @return Security
*/
public static function security(App $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('security', $config);
}
$config = $config ?? config('App');
return new Security($config);
}
//--------------------------------------------------------------------
/**
* Return the session manager.
*
* @param App|null $config
* @param boolean $getShared
*
* @return Session
*/
public static function session(App $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('session', $config);
}
$config = $config ?? config('App');
$logger = AppServices::logger();
$driverName = $config->sessionDriver;
$driver = new $driverName($config, AppServices::request()->getIPAddress());
$driver->setLogger($logger);
$session = new Session($driver, $config);
$session->setLogger($logger);
if (session_status() === PHP_SESSION_NONE)
{
$session->start();
}
return $session;
}
//--------------------------------------------------------------------
/**
* The Throttler class provides a simple method for implementing
* rate limiting in your applications.
*
* @param boolean $getShared
*
* @return Throttler
*/
public static function throttler(bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('throttler');
}
return new Throttler(AppServices::cache());
}
//--------------------------------------------------------------------
/**
* The Timer class provides a simple way to Benchmark portions of your
* application.
*
* @param boolean $getShared
*
* @return Timer
*/
public static function timer(bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('timer');
}
return new Timer();
}
//--------------------------------------------------------------------
/**
* Return the debug toolbar.
*
* @param ToolbarConfig|null $config
* @param boolean $getShared
*
* @return Toolbar
*/
public static function toolbar(ToolbarConfig $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('toolbar', $config);
}
$config = $config ?? config('Toolbar');
return new Toolbar($config);
}
//--------------------------------------------------------------------
/**
* The URI class provides a way to model and manipulate URIs.
*
* @param string $uri
* @param boolean $getShared
*
* @return URI
*/
public static function uri(string $uri = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('uri', $uri);
}
return new URI($uri);
}
//--------------------------------------------------------------------
/**
* The Validation class provides tools for validating input data.
*
* @param ValidationConfig|null $config
* @param boolean $getShared
*
* @return Validation
*/
public static function validation(ValidationConfig $config = null, bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('validation', $config);
}
$config = $config ?? config('Validation');
return new Validation($config, AppServices::renderer());
}
//--------------------------------------------------------------------
/**
* View cells are intended to let you insert HTML into view
* that has been generated by any callable in the system.
*
* @param boolean $getShared
*
* @return Cell
*/
public static function viewcell(bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('viewcell');
}
return new Cell(AppServices::cache());
}
//--------------------------------------------------------------------
/**
* The Typography class provides a way to format text in semantically relevant ways.
*
* @param boolean $getShared
*
* @return Typography
*/
public static function typography(bool $getShared = true)
{
if ($getShared)
{
return static::getSharedInstance('typography');
}
return new Typography();
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Config;
/**
* View configuration
*/
class View extends BaseConfig
{
/**
* When false, the view method will clear the data between each
* call.
*
* @var boolean
*/
public $saveData = true;
/**
* Parser Filters map a filter name with any PHP callable. When the
* Parser prepares a variable for display, it will chain it
* through the filters in the order defined, inserting any parameters.
*
* To prevent potential abuse, all filters MUST be defined here
* in order for them to be available for use within the Parser.
*/
public $filters = [];
/**
* Parser Plugins provide a way to extend the functionality provided
* by the core Parser by creating aliases that will be replaced with
* any callable. Can be single or tag pair.
*/
public $plugins = [];
/**
* Built-in View filters.
*
* @var array
*/
protected $coreFilters = [
'abs' => '\abs',
'capitalize' => '\CodeIgniter\View\Filters::capitalize',
'date' => '\CodeIgniter\View\Filters::date',
'date_modify' => '\CodeIgniter\View\Filters::date_modify',
'default' => '\CodeIgniter\View\Filters::default',
'esc' => '\CodeIgniter\View\Filters::esc',
'excerpt' => '\CodeIgniter\View\Filters::excerpt',
'highlight' => '\CodeIgniter\View\Filters::highlight',
'highlight_code' => '\CodeIgniter\View\Filters::highlight_code',
'limit_words' => '\CodeIgniter\View\Filters::limit_words',
'limit_chars' => '\CodeIgniter\View\Filters::limit_chars',
'local_currency' => '\CodeIgniter\View\Filters::local_currency',
'local_number' => '\CodeIgniter\View\Filters::local_number',
'lower' => '\strtolower',
'nl2br' => '\CodeIgniter\View\Filters::nl2br',
'number_format' => '\number_format',
'prose' => '\CodeIgniter\View\Filters::prose',
'round' => '\CodeIgniter\View\Filters::round',
'strip_tags' => '\strip_tags',
'title' => '\CodeIgniter\View\Filters::title',
'upper' => '\strtoupper',
];
/**
* Built-in View plugins.
*
* @var array
*/
protected $corePlugins = [
'current_url' => '\CodeIgniter\View\Plugins::currentURL',
'previous_url' => '\CodeIgniter\View\Plugins::previousURL',
'mailto' => '\CodeIgniter\View\Plugins::mailto',
'safe_mailto' => '\CodeIgniter\View\Plugins::safeMailto',
'lang' => '\CodeIgniter\View\Plugins::lang',
'validation_errors' => '\CodeIgniter\View\Plugins::validationErrors',
'route' => '\CodeIgniter\View\Plugins::route',
'siteURL' => '\CodeIgniter\View\Plugins::siteURL',
];
/**
* Constructor.
*
* Merge the built-in and developer-configured filters and plugins,
* with preference to the developer ones.
*/
public function __construct()
{
$this->filters = array_merge($this->coreFilters, $this->filters);
$this->plugins = array_merge($this->corePlugins, $this->plugins);
parent::__construct();
}
}
+185
View File
@@ -0,0 +1,185 @@
<?php
/**
* 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.
*/
namespace CodeIgniter;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Validation\Exceptions\ValidationException;
use CodeIgniter\Validation\Validation;
use Config\Services;
use Psr\Log\LoggerInterface;
/**
* Class Controller
*/
class Controller
{
/**
* Helpers that will be automatically loaded on class instantiation.
*
* @var array
*/
protected $helpers = [];
/**
* Instance of the main Request object.
*
* @var RequestInterface
*/
protected $request;
/**
* Instance of the main response object.
*
* @var ResponseInterface
*/
protected $response;
/**
* Instance of logger to use.
*
* @var LoggerInterface
*/
protected $logger;
/**
* Should enforce HTTPS access for all methods in this controller.
*
* @var integer Number of seconds to set HSTS header
*/
protected $forceHTTPS = 0;
/**
* Once validation has been run, will hold the Validation instance.
*
* @var Validation
*/
protected $validator;
//--------------------------------------------------------------------
/**
* Constructor.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param LoggerInterface $logger
*
* @throws HTTPException
*/
public function initController(RequestInterface $request, ResponseInterface $response, LoggerInterface $logger)
{
$this->request = $request;
$this->response = $response;
$this->logger = $logger;
if ($this->forceHTTPS > 0)
{
$this->forceHTTPS($this->forceHTTPS);
}
// Autoload helper files.
helper($this->helpers);
}
//--------------------------------------------------------------------
/**
* A convenience method to use when you need to ensure that a single
* method is reached only via HTTPS. If it isn't, then a redirect
* will happen back to this method and HSTS header will be sent
* to have modern browsers transform requests automatically.
*
* @param integer $duration The number of seconds this link should be
* considered secure for. Only with HSTS header.
* Default value is 1 year.
*
* @throws HTTPException
*/
protected function forceHTTPS(int $duration = 31536000)
{
force_https($duration, $this->request, $this->response);
}
//--------------------------------------------------------------------
/**
* Provides a simple way to tie into the main CodeIgniter class and
* tell it how long to cache the current page for.
*
* @param integer $time
*/
protected function cachePage(int $time)
{
CodeIgniter::cache($time);
}
//--------------------------------------------------------------------
/**
* Handles "auto-loading" helper files.
*
* @deprecated Use `helper` function instead of using this method.
*
* @codeCoverageIgnore
*/
protected function loadHelpers()
{
if (empty($this->helpers))
{
return;
}
helper($this->helpers);
}
//--------------------------------------------------------------------
/**
* A shortcut to performing validation on input data. If validation
* is not successful, a $errors property will be set on this class.
*
* @param array|string $rules
* @param array $messages An array of custom error messages
*
* @return boolean
*/
protected function validate($rules, array $messages = []): bool
{
$this->validator = Services::validation();
// If you replace the $rules array with the name of the group
if (is_string($rules))
{
$validation = config('Validation');
// If the rule wasn't found in the \Config\Validation, we
// should throw an exception so the developer can find it.
if (! isset($validation->$rules))
{
throw ValidationException::forRuleNotFound($rules);
}
// If no error message is defined, use the error message in the Config\Validation file
if (! $messages)
{
$errorName = $rules . '_errors';
$messages = $validation->$errorName ?? [];
}
$rules = $validation->$rules;
}
return $this->validator->withRequest($this->request)->setRules($rules, $messages)->run();
}
}
+125
View File
@@ -0,0 +1,125 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cookie;
use DateTimeInterface;
/**
* Interface for a fresh Cookie instance with selected attribute(s)
* only changed from the original instance.
*/
interface CloneableCookieInterface extends CookieInterface
{
/**
* Creates a new Cookie with a new cookie prefix.
*
* @param string $prefix
*
* @return static
*/
public function withPrefix(string $prefix = '');
/**
* Creates a new Cookie with a new name.
*
* @param string $name
*
* @return static
*/
public function withName(string $name);
/**
* Creates a new Cookie with new value.
*
* @param string $value
*
* @return static
*/
public function withValue(string $value);
/**
* Creates a new Cookie with a new cookie expires time.
*
* @param DateTimeInterface|integer|string $expires
*
* @return static
*/
public function withExpires($expires);
/**
* Creates a new Cookie that will expire the cookie from the browser.
*
* @return static
*/
public function withExpired();
/**
* Creates a new Cookie that will virtually never expire from the browser.
*
* @return static
*/
public function withNeverExpiring();
/**
* Creates a new Cookie with a new path on the server the cookie is available.
*
* @param string|null $path
*
* @return static
*/
public function withPath(?string $path);
/**
* Creates a new Cookie with a new domain the cookie is available.
*
* @param string|null $domain
*
* @return static
*/
public function withDomain(?string $domain);
/**
* Creates a new Cookie with a new "Secure" attribute.
*
* @param boolean $secure
*
* @return static
*/
public function withSecure(bool $secure = true);
/**
* Creates a new Cookie with a new "HttpOnly" attribute
*
* @param boolean $httponly
*
* @return static
*/
public function withHTTPOnly(bool $httponly = true);
/**
* Creates a new Cookie with a new "SameSite" attribute.
*
* @param string $samesite
*
* @return static
*/
public function withSameSite(string $samesite);
/**
* Creates a new Cookie with URL encoding option updated.
*
* @param boolean $raw
*
* @return static
*/
public function withRaw(bool $raw = true);
}
+842
View File
@@ -0,0 +1,842 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cookie;
use ArrayAccess;
use CodeIgniter\Cookie\Exceptions\CookieException;
use Config\Cookie as CookieConfig;
use DateTimeInterface;
use InvalidArgumentException;
use LogicException;
/**
* A `Cookie` class represents an immutable HTTP cookie value object.
*
* Being immutable, modifying one or more of its attributes will return
* a new `Cookie` instance, rather than modifying itself. Users should
* reassign this new instance to a new variable to capture it.
*
* ```php
* $cookie = new Cookie('test_cookie', 'test_value');
* $cookie->getName(); // test_cookie
*
* $cookie->withName('prod_cookie');
* $cookie->getName(); // test_cookie
*
* $cookie2 = $cookie->withName('prod_cookie');
* $cookie2->getName(); // prod_cookie
* ```
*/
class Cookie implements ArrayAccess, CloneableCookieInterface
{
/**
* @var string
*/
protected $prefix = '';
/**
* @var string
*/
protected $name;
/**
* @var string
*/
protected $value;
/**
* @var integer
*/
protected $expires;
/**
* @var string
*/
protected $path = '/';
/**
* @var string
*/
protected $domain = '';
/**
* @var boolean
*/
protected $secure = false;
/**
* @var boolean
*/
protected $httponly = true;
/**
* @var string
*/
protected $samesite = self::SAMESITE_LAX;
/**
* @var boolean
*/
protected $raw = false;
/**
* Default attributes for a Cookie object. The keys here are the
* lowercase attribute names. Do not camelCase!
*
* @var array<string, mixed>
*/
private static $defaults = [
'prefix' => '',
'expires' => 0,
'path' => '/',
'domain' => '',
'secure' => false,
'httponly' => true,
'samesite' => self::SAMESITE_LAX,
'raw' => false,
];
/**
* A cookie name can be any US-ASCII characters, except control characters,
* spaces, tabs, or separator characters.
*
* @var string
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
* @see https://tools.ietf.org/html/rfc2616#section-2.2
*/
private static $reservedCharsList = "=,; \t\r\n\v\f()<>@:\\\"/[]?{}";
/**
* Set the default attributes to a Cookie instance by injecting
* the values from the `CookieConfig` config or an array.
*
* @param CookieConfig|array<string, mixed> $config
*
* @return array<string, mixed> The old defaults array. Useful for resetting.
*/
public static function setDefaults($config = [])
{
$oldDefaults = self::$defaults;
$newDefaults = [];
if ($config instanceof CookieConfig)
{
$newDefaults = [
'prefix' => $config->prefix,
'expires' => $config->expires,
'path' => $config->path,
'domain' => $config->domain,
'secure' => $config->secure,
'httponly' => $config->httponly,
'samesite' => $config->samesite,
'raw' => $config->raw,
];
}
elseif (is_array($config))
{
$newDefaults = $config;
}
// This array union ensures that even if passed `$config` is not
// `CookieConfig` or `array`, no empty defaults will occur.
self::$defaults = $newDefaults + $oldDefaults;
return $oldDefaults;
}
//=========================================================================
// CONSTRUCTORS
//=========================================================================
/**
* Create a new Cookie instance from a `Set-Cookie` header.
*
* @param string $cookie
* @param boolean $raw
*
* @throws CookieException
*
* @return static
*/
public static function fromHeaderString(string $cookie, bool $raw = false)
{
$data = self::$defaults;
$data['raw'] = $raw;
$parts = preg_split('/\;[\s]*/', $cookie);
$part = explode('=', array_shift($parts), 2);
$name = $raw ? $part[0] : urldecode($part[0]);
$value = isset($part[1]) ? ($raw ? $part[1] : urldecode($part[1])) : '';
unset($part);
foreach ($parts as $part)
{
if (strpos($part, '=') !== false)
{
[$attr, $val] = explode('=', $part);
}
else
{
$attr = $part;
$val = true;
}
$data[strtolower($attr)] = $val;
}
return new static($name, $value, $data);
}
/**
* Construct a new Cookie instance.
*
* @param string $name The cookie's name
* @param string $value The cookie's value
* @param array<string, mixed> $options The cookie's options
*
* @throws CookieException
*/
final public function __construct(string $name, string $value = '', array $options = [])
{
$options += self::$defaults;
$options['expires'] = static::convertExpiresTimestamp($options['expires']);
// If both `Expires` and `Max-Age` are set, `Max-Age` has precedence.
if (isset($options['max-age']) && is_numeric($options['max-age']))
{
$options['expires'] = time() + (int) $options['max-age'];
unset($options['max-age']);
}
// to preserve backward compatibility with array-based cookies in previous CI versions
$prefix = $options['prefix'] ?: self::$defaults['prefix'];
$path = $options['path'] ?: self::$defaults['path'];
$domain = $options['domain'] ?: self::$defaults['domain'];
// empty string SameSite should use the default for browsers
$samesite = $options['samesite'] ?: self::$defaults['samesite'];
$raw = $options['raw'];
$secure = $options['secure'];
$httponly = $options['httponly'];
$this->validateName($name, $raw);
$this->validatePrefix($prefix, $secure, $path, $domain);
$this->validateSameSite($samesite, $secure);
$this->prefix = $prefix;
$this->name = $name;
$this->value = $value;
$this->expires = static::convertExpiresTimestamp($options['expires']);
$this->path = $path;
$this->domain = $domain;
$this->secure = $secure;
$this->httponly = $httponly;
$this->samesite = ucfirst(strtolower($samesite));
$this->raw = $raw;
}
//=========================================================================
// GETTERS
//=========================================================================
/**
* {@inheritDoc}
*/
public function getId(): string
{
return implode(';', [$this->getPrefixedName(), $this->getPath(), $this->getDomain()]);
}
/**
* {@inheritDoc}
*/
public function getPrefix(): string
{
return $this->prefix;
}
/**
* {@inheritDoc}
*/
public function getName(): string
{
return $this->name;
}
/**
* {@inheritDoc}
*/
public function getPrefixedName(): string
{
$name = $this->getPrefix();
if ($this->isRaw())
{
$name .= $this->getName();
}
else
{
$search = str_split(self::$reservedCharsList);
$replace = array_map('rawurlencode', $search);
$name .= str_replace($search, $replace, $this->getName());
}
return $name;
}
/**
* {@inheritDoc}
*/
public function getValue(): string
{
return $this->value;
}
/**
* {@inheritDoc}
*/
public function getExpiresTimestamp(): int
{
return $this->expires;
}
/**
* {@inheritDoc}
*/
public function getExpiresString(): string
{
return gmdate(self::EXPIRES_FORMAT, $this->expires);
}
/**
* {@inheritDoc}
*/
public function isExpired(): bool
{
return $this->expires === 0 || $this->expires < time();
}
/**
* {@inheritDoc}
*/
public function getMaxAge(): int
{
$maxAge = $this->expires - time();
return $maxAge >= 0 ? $maxAge : 0;
}
/**
* {@inheritDoc}
*/
public function getPath(): string
{
return $this->path;
}
/**
* {@inheritDoc}
*/
public function getDomain(): string
{
return $this->domain;
}
/**
* {@inheritDoc}
*/
public function isSecure(): bool
{
return $this->secure;
}
/**
* {@inheritDoc}
*/
public function isHTTPOnly(): bool
{
return $this->httponly;
}
/**
* {@inheritDoc}
*/
public function getSameSite(): string
{
return $this->samesite;
}
/**
* {@inheritDoc}
*/
public function isRaw(): bool
{
return $this->raw;
}
/**
* {@inheritDoc}
*/
public function getOptions(): array
{
// This is the order of options in `setcookie`. DO NOT CHANGE.
return [
'expires' => $this->expires,
'path' => $this->path,
'domain' => $this->domain,
'secure' => $this->secure,
'httponly' => $this->httponly,
'samesite' => $this->samesite ?: ucfirst(self::SAMESITE_LAX),
];
}
//=========================================================================
// CLONING
//=========================================================================
/**
* {@inheritDoc}
*/
public function withPrefix(string $prefix = '')
{
$this->validatePrefix($prefix, $this->secure, $this->path, $this->domain);
$cookie = clone $this;
$cookie->prefix = $prefix;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withName(string $name)
{
$this->validateName($name, $this->raw);
$cookie = clone $this;
$cookie->name = $name;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withValue(string $value)
{
$cookie = clone $this;
$cookie->value = $value;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withExpires($expires)
{
$cookie = clone $this;
$cookie->expires = static::convertExpiresTimestamp($expires);
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withExpired()
{
$cookie = clone $this;
$cookie->expires = 0;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withNeverExpiring()
{
$cookie = clone $this;
$cookie->expires = time() + 5 * YEAR;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withPath(?string $path)
{
$path = $path ?: self::$defaults['path'];
$this->validatePrefix($this->prefix, $this->secure, $path, $this->domain);
$cookie = clone $this;
$cookie->path = $path;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withDomain(?string $domain)
{
$domain = $domain ?? self::$defaults['domain'];
$this->validatePrefix($this->prefix, $this->secure, $this->path, $domain);
$cookie = clone $this;
$cookie->domain = $domain;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withSecure(bool $secure = true)
{
$this->validatePrefix($this->prefix, $secure, $this->path, $this->domain);
$this->validateSameSite($this->samesite, $secure);
$cookie = clone $this;
$cookie->secure = $secure;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withHTTPOnly(bool $httponly = true)
{
$cookie = clone $this;
$cookie->httponly = $httponly;
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withSameSite(string $samesite)
{
$this->validateSameSite($samesite, $this->secure);
$cookie = clone $this;
$cookie->samesite = ucfirst(strtolower($samesite));
return $cookie;
}
/**
* {@inheritDoc}
*/
public function withRaw(bool $raw = true)
{
$this->validateName($this->name, $raw);
$cookie = clone $this;
$cookie->raw = $raw;
return $cookie;
}
//=========================================================================
// ARRAY ACCESS FOR BC
//=========================================================================
/**
* Whether an offset exists.
*
* @param string $offset
*
* @return boolean
*/
public function offsetExists($offset)
{
return $offset === 'expire' ? true : property_exists($this, $offset);
}
/**
* Offset to retrieve.
*
* @param string $offset
*
* @throws InvalidArgumentException
*
* @return mixed
*/
public function offsetGet($offset)
{
if (! $this->offsetExists($offset))
{
throw new InvalidArgumentException(sprintf('Undefined offset "%s".', $offset));
}
return $offset === 'expire' ? $this->expires : $this->{$offset};
}
/**
* Offset to set.
*
* @param string $offset
* @param mixed $value
*
* @throws LogicException
*
* @return void
*/
public function offsetSet($offset, $value)
{
throw new LogicException(sprintf('Cannot set values of properties of %s as it is immutable.', static::class));
}
/**
* Offset to unset.
*
* @param string $offset
*
* @throws LogicException
*
* @return void
*/
public function offsetUnset($offset)
{
throw new LogicException(sprintf('Cannot unset values of properties of %s as it is immutable.', static::class));
}
//=========================================================================
// CONVERTERS
//=========================================================================
/**
* {@inheritDoc}
*/
public function toHeaderString(): string
{
return $this->__toString();
}
/**
* {@inheritDoc}
*/
public function __toString()
{
$cookieHeader = [];
if ($this->getValue() === '')
{
$cookieHeader[] = $this->getPrefixedName() . '=deleted';
$cookieHeader[] = 'Expires=' . gmdate(self::EXPIRES_FORMAT, 0);
$cookieHeader[] = 'Max-Age=0';
}
else
{
$value = $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue());
$cookieHeader[] = sprintf('%s=%s', $this->getPrefixedName(), $value);
if ($this->getExpiresTimestamp() !== 0)
{
$cookieHeader[] = 'Expires=' . $this->getExpiresString();
$cookieHeader[] = 'Max-Age=' . $this->getMaxAge();
}
}
if ($this->getPath() !== '')
{
$cookieHeader[] = 'Path=' . $this->getPath();
}
if ($this->getDomain() !== '')
{
$cookieHeader[] = 'Domain=' . $this->getDomain();
}
if ($this->isSecure())
{
$cookieHeader[] = 'Secure';
}
if ($this->isHTTPOnly())
{
$cookieHeader[] = 'HttpOnly';
}
$samesite = $this->getSameSite();
if ($samesite === '')
{
// modern browsers warn in console logs that an empty SameSite attribute
// will be given the `Lax` value
$samesite = self::SAMESITE_LAX;
}
$cookieHeader[] = 'SameSite=' . ucfirst(strtolower($samesite));
return implode('; ', $cookieHeader);
}
/**
* {@inheritDoc}
*/
public function toArray(): array
{
return [
'name' => $this->name,
'value' => $this->value,
'prefix' => $this->prefix,
'raw' => $this->raw,
] + $this->getOptions();
}
/**
* Converts expires time to Unix format.
*
* @param DateTimeInterface|integer|string $expires
*
* @return integer
*/
protected static function convertExpiresTimestamp($expires = 0): int
{
if ($expires instanceof DateTimeInterface)
{
$expires = $expires->format('U');
}
if (! is_string($expires) && ! is_int($expires))
{
throw CookieException::forInvalidExpiresTime(gettype($expires));
}
if (! is_numeric($expires))
{
$expires = strtotime($expires);
if ($expires === false)
{
throw CookieException::forInvalidExpiresValue();
}
}
return $expires > 0 ? (int) $expires : 0;
}
//=========================================================================
// VALIDATION
//=========================================================================
/**
* Validates the cookie name per RFC 2616.
*
* If `$raw` is true, names should not contain invalid characters
* as `setrawcookie()` will reject this.
*
* @param string $name
* @param boolean $raw
*
* @throws CookieException
*
* @return void
*/
protected function validateName(string $name, bool $raw): void
{
if ($raw && strpbrk($name, self::$reservedCharsList) !== false)
{
throw CookieException::forInvalidCookieName($name);
}
if ($name === '')
{
throw CookieException::forEmptyCookieName();
}
}
/**
* Validates the special prefixes if some attribute requirements are met.
*
* @param string $prefix
* @param boolean $secure
* @param string $path
* @param string $domain
*
* @throws CookieException
*
* @return void
*/
protected function validatePrefix(string $prefix, bool $secure, string $path, string $domain): void
{
if (strpos($prefix, '__Secure-') === 0 && ! $secure)
{
throw CookieException::forInvalidSecurePrefix();
}
if (strpos($prefix, '__Host-') === 0 && (! $secure || $domain !== '' || $path !== '/'))
{
throw CookieException::forInvalidHostPrefix();
}
}
/**
* Validates the `SameSite` to be within the allowed types.
*
* @param string $samesite
* @param boolean $secure
*
* @throws CookieException
*
* @return void
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
*/
protected function validateSameSite(string $samesite, bool $secure): void
{
if ($samesite === '')
{
$samesite = self::$defaults['samesite'];
}
if ($samesite === '')
{
$samesite = self::SAMESITE_LAX;
}
if (! in_array(strtolower($samesite), self::ALLOWED_SAMESITE_VALUES, true))
{
throw CookieException::forInvalidSameSite($samesite);
}
if (strtolower($samesite) === self::SAMESITE_NONE && ! $secure)
{
throw CookieException::forInvalidSameSiteNone();
}
}
}
+200
View File
@@ -0,0 +1,200 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cookie;
/**
* Interface for a value object representation of an HTTP cookie.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
*/
interface CookieInterface
{
/**
* Cookies will be sent in all contexts, i.e in responses to both
* first-party and cross-origin requests. If `SameSite=None` is set,
* the cookie `Secure` attribute must also be set (or the cookie will be blocked).
*/
public const SAMESITE_NONE = 'none';
/**
* Cookies are not sent on normal cross-site subrequests (for example to
* load images or frames into a third party site), but are sent when a
* user is navigating to the origin site (i.e. when following a link).
*/
public const SAMESITE_LAX = 'lax';
/**
* Cookies will only be sent in a first-party context and not be sent
* along with requests initiated by third party websites.
*/
public const SAMESITE_STRICT = 'strict';
/**
* RFC 6265 allowed values for the "SameSite" attribute.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
*/
public const ALLOWED_SAMESITE_VALUES = [
self::SAMESITE_NONE,
self::SAMESITE_LAX,
self::SAMESITE_STRICT,
];
/**
* Expires date format.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date
* @see https://tools.ietf.org/html/rfc7231#section-7.1.1.2
*/
public const EXPIRES_FORMAT = 'D, d-M-Y H:i:s T';
/**
* Returns a unique identifier for the cookie consisting
* of its prefixed name, path, and domain.
*
* @return string
*/
public function getId(): string;
/**
* Gets the cookie prefix.
*
* @return string
*/
public function getPrefix(): string;
/**
* Gets the cookie name.
*
* @return string
*/
public function getName(): string;
/**
* Gets the cookie name prepended with the prefix, if any.
*
* @return string
*/
public function getPrefixedName(): string;
/**
* Gets the cookie value.
*
* @return string
*/
public function getValue(): string;
/**
* Gets the time in Unix timestamp the cookie expires.
*
* @return integer
*/
public function getExpiresTimestamp(): int;
/**
* Gets the formatted expires time.
*
* @return string
*/
public function getExpiresString(): string;
/**
* Checks if the cookie is expired.
*
* @return boolean
*/
public function isExpired(): bool;
/**
* Gets the "Max-Age" cookie attribute.
*
* @return integer
*/
public function getMaxAge(): int;
/**
* Gets the "Path" cookie attribute.
*
* @return string
*/
public function getPath(): string;
/**
* Gets the "Domain" cookie attribute.
*
* @return string
*/
public function getDomain(): string;
/**
* Gets the "Secure" cookie attribute.
*
* Checks if the cookie is only sent to the server when a request is made
* with the `https:` scheme (except on `localhost`), and therefore is more
* resistent to man-in-the-middle attacks.
*
* @return boolean
*/
public function isSecure(): bool;
/**
* Gets the "HttpOnly" cookie attribute.
*
* Checks if JavaScript is forbidden from accessing the cookie.
*
* @return boolean
*/
public function isHTTPOnly(): bool;
/**
* Gets the "SameSite" cookie attribute.
*
* @return string
*/
public function getSameSite(): string;
/**
* Checks if the cookie should be sent with no URL encoding.
*
* @return boolean
*/
public function isRaw(): bool;
/**
* Gets the options that are passable to the `setcookie` variant
* available on PHP 7.3+
*
* @return array<string, mixed>
*/
public function getOptions(): array;
/**
* Returns the Cookie as a header value.
*
* @return string
*/
public function toHeaderString(): string;
/**
* Returns the string representation of the Cookie object.
*
* @return string
*/
public function __toString();
/**
* Returns the array representation of the Cookie object.
*
* @return array<string, mixed>
*/
public function toArray(): array;
}
+305
View File
@@ -0,0 +1,305 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cookie;
use ArrayIterator;
use CodeIgniter\Cookie\Exceptions\CookieException;
use Countable;
use IteratorAggregate;
use Traversable;
/**
* The CookieStore object represents an immutable collection of `Cookie` value objects.
*
* @implements IteratorAggregate<string, Cookie>
*/
class CookieStore implements Countable, IteratorAggregate
{
/**
* The cookie collection.
*
* @var array<string, Cookie>
*/
protected $cookies = [];
/**
* Creates a CookieStore from an array of `Set-Cookie` headers.
*
* @param string[] $headers
* @param boolean $raw
*
* @throws CookieException
*
* @return static
*/
public static function fromCookieHeaders(array $headers, bool $raw = false)
{
/**
* @var Cookie[] $cookies
*/
$cookies = array_filter(array_map(function (string $header) use ($raw) {
try
{
return Cookie::fromHeaderString($header, $raw);
}
catch (CookieException $e)
{
log_message('error', $e->getMessage());
return false;
}
}, $headers));
return new static($cookies);
}
/**
* @param Cookie[] $cookies
*
* @throws CookieException
*/
final public function __construct(array $cookies)
{
$this->validateCookies($cookies);
foreach ($cookies as $cookie)
{
$this->cookies[$cookie->getId()] = $cookie;
}
}
/**
* Checks if a `Cookie` object identified by name and
* prefix is present in the collection.
*
* @param string $name
* @param string $prefix
* @param string|null $value
*
* @return boolean
*/
public function has(string $name, string $prefix = '', string $value = null): bool
{
$name = $prefix . $name;
foreach ($this->cookies as $cookie)
{
if ($cookie->getPrefixedName() !== $name)
{
continue;
}
if ($value === null)
{
return true; // for BC
}
return $cookie->getValue() === $value;
}
return false;
}
/**
* Retrieves an instance of `Cookie` identified by a name and prefix.
* This throws an exception if not found.
*
* @param string $name
* @param string $prefix
*
* @throws CookieException
*
* @return Cookie
*/
public function get(string $name, string $prefix = ''): Cookie
{
$name = $prefix . $name;
foreach ($this->cookies as $cookie)
{
if ($cookie->getPrefixedName() === $name)
{
return $cookie;
}
}
throw CookieException::forUnknownCookieInstance([$name, $prefix]);
}
/**
* Store a new cookie and return a new collection. The original collection
* is left unchanged.
*
* @param Cookie $cookie
*
* @return static
*/
public function put(Cookie $cookie)
{
$store = clone $this;
$store->cookies[$cookie->getId()] = $cookie;
return $store;
}
/**
* Removes a cookie from a collection and returns an updated collection.
* The original collection is left unchanged.
*
* Removing a cookie from the store **DOES NOT** delete it from the browser.
* If you intend to delete a cookie *from the browser*, you must put an empty
* value cookie with the same name to the store.
*
* @param string $name
* @param string $prefix
*
* @return static
*/
public function remove(string $name, string $prefix = '')
{
$default = Cookie::setDefaults();
$id = implode(';', [$prefix . $name, $default['path'], $default['domain']]);
$store = clone $this;
foreach (array_keys($store->cookies) as $index)
{
if ($index === $id)
{
unset($store->cookies[$index]);
}
}
return $store;
}
/**
* Dispatches all cookies in store.
*
* @return void
*/
public function dispatch(): void
{
foreach ($this->cookies as $cookie)
{
$name = $cookie->getPrefixedName();
$value = $cookie->getValue();
$options = $cookie->getOptions();
if ($cookie->isRaw())
{
$this->setRawCookie($name, $value, $options);
}
else
{
$this->setCookie($name, $value, $options);
}
}
$this->clear();
}
/**
* Returns all cookie instances in store.
*
* @return array<string, Cookie>
*/
public function display(): array
{
return $this->cookies;
}
/**
* Clears the cookie collection.
*
* @return void
*/
public function clear(): void
{
$this->cookies = [];
}
/**
* Gets the Cookie count in this collection.
*
* @return integer
*/
public function count(): int
{
return count($this->cookies);
}
/**
* Gets the iterator for the cookie collection.
*
* @return Traversable<string, Cookie>
*/
public function getIterator()
{
return new ArrayIterator($this->cookies);
}
/**
* Validates all cookies passed to be instances of Cookie.
*
* @param array $cookies
*
* @throws CookieException
*
* @return void
*/
protected function validateCookies(array $cookies): void
{
foreach ($cookies as $index => $cookie)
{
$type = is_object($cookie) ? get_class($cookie) : gettype($cookie);
if (! $cookie instanceof Cookie)
{
throw CookieException::forInvalidCookieInstance([static::class, Cookie::class, $type, $index]);
}
}
}
/**
* Extracted call to `setrawcookie()` in order to run unit tests on it.
*
* @codeCoverageIgnore
*
* @param string $name
* @param string $value
* @param array $options
*
* @return void
*/
protected function setRawCookie(string $name, string $value, array $options): void
{
setrawcookie($name, $value, $options);
}
/**
* Extracted call to `setcookie()` in order to run unit tests on it.
*
* @codeCoverageIgnore
*
* @param string $name
* @param string $value
* @param array $options
*
* @return void
*/
protected function setCookie(string $name, string $value, array $options): void
{
setcookie($name, $value, $options);
}
}
@@ -0,0 +1,133 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Cookie\Exceptions;
use CodeIgniter\Exceptions\FrameworkException;
/**
* CookieException is thrown for invalid cookies initialization and management.
*/
class CookieException extends FrameworkException
{
/**
* Thrown for invalid type given for the "Expires" attribute.
*
* @param string $type
*
* @return static
*/
public static function forInvalidExpiresTime(string $type)
{
return new static(lang('Cookie.invalidExpiresTime', [$type]));
}
/**
* Thrown when the value provided for "Expires" is invalid.
*
* @return static
*/
public static function forInvalidExpiresValue()
{
return new static(lang('Cookie.invalidExpiresValue'));
}
/**
* Thrown when the cookie name contains invalid characters per RFC 2616.
*
* @param string $name
*
* @return static
*/
public static function forInvalidCookieName(string $name)
{
return new static(lang('Cookie.invalidCookieName', [$name]));
}
/**
* Thrown when the cookie name is empty.
*
* @return static
*/
public static function forEmptyCookieName()
{
return new static(lang('Cookie.emptyCookieName'));
}
/**
* Thrown when using the `__Secure-` prefix but the `Secure` attribute
* is not set to true.
*
* @return static
*/
public static function forInvalidSecurePrefix()
{
return new static(lang('Cookie.invalidSecurePrefix'));
}
/**
* Thrown when using the `__Host-` prefix but the `Secure` flag is not
* set, the `Domain` is set, and the `Path` is not `/`.
*
* @return static
*/
public static function forInvalidHostPrefix()
{
return new static(lang('Cookie.invalidHostPrefix'));
}
/**
* Thrown when the `SameSite` attribute given is not of the valid types.
*
* @param string $sameSite
*
* @return static
*/
public static function forInvalidSameSite(string $sameSite)
{
return new static(lang('Cookie.invalidSameSite', [$sameSite]));
}
/**
* Thrown when the `SameSite` attribute is set to `None` but the `Secure`
* attribute is not set.
*
* @return static
*/
public static function forInvalidSameSiteNone()
{
return new static(lang('Cookie.invalidSameSiteNone'));
}
/**
* Thrown when the `CookieStore` class is filled with invalid Cookie objects.
*
* @param array<string|integer> $data
*
* @return static
*/
public static function forInvalidCookieInstance(array $data)
{
return new static(lang('Cookie.invalidCookieInstance', $data));
}
/**
* Thrown when the queried Cookie object does not exist in the cookie collection.
*
* @param string[] $data
*
* @return static
*/
public static function forUnknownCookieInstance(array $data)
{
return new static(lang('Cookie.unknownCookieInstance', $data));
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+244
View File
@@ -0,0 +1,244 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database;
use BadMethodCallException;
use CodeIgniter\Events\Events;
/**
* Base prepared query
*/
abstract class BasePreparedQuery implements PreparedQueryInterface
{
/**
* The prepared statement itself.
*
* @var object|resource
*/
protected $statement;
/**
* The error code, if any.
*
* @var integer
*/
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
*/
protected $db;
//--------------------------------------------------------------------
/**
* Constructor.
*
* @param BaseConnection $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.
*
* @param string $sql
* @param array $options Passed to the connection's prepare statement.
* @param string $queryClass
*
* @return mixed
*/
public function prepare(string $sql, array $options = [], string $queryClass = 'CodeIgniter\\Database\\Query')
{
// 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.
*
* @param string $sql
* @param array $options Passed to the connection's prepare statement.
*
* @return mixed
*/
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.
*
* @param array $data
*
* @return ResultInterface
*/
public function execute(...$data)
{
// Execute the Query.
$startTime = microtime(true);
$result = $this->_execute($data);
// Update our query object
$query = clone $this->query;
$query->setBinds($data);
$query->setDuration($startTime);
// Let others do something with this query
Events::trigger('DBQuery', $query);
// Return a result object
$resultClass = str_replace('PreparedQuery', 'Result', get_class($this));
$resultID = $this->_getResult();
return new $resultClass($this->db->connID, $resultID);
}
//--------------------------------------------------------------------
/**
* The database dependant version of the execute method.
*
* @param array $data
*
* @return boolean
*/
abstract public function _execute(array $data): bool;
//--------------------------------------------------------------------
/**
* Returns the result object for the prepared query.
*
* @return mixed
*/
abstract public function _getResult();
//--------------------------------------------------------------------
/**
* Explicitly closes the statement.
*
* @return void
*/
public function close()
{
if (! is_object($this->statement))
{
return;
}
$this->statement->close();
}
//--------------------------------------------------------------------
/**
* Returns the SQL that has been prepared.
*
* @return string
*/
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.
*
* @return boolean
*/
public function hasError(): bool
{
return ! empty($this->errorString);
}
//--------------------------------------------------------------------
/**
* Returns the error code created while executing this statement.
*
* @return integer
*/
public function getErrorCode(): int
{
return $this->errorCode;
}
//--------------------------------------------------------------------
/**
* Returns the error message created while executing this statement.
*
* @return string
*/
public function getErrorMessage(): string
{
return $this->errorString;
}
//--------------------------------------------------------------------
}
+635
View File
@@ -0,0 +1,635 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Entity\Entity;
/**
* Class BaseResult
*/
abstract class BaseResult implements ResultInterface
{
/**
* Connection ID
*
* @var resource|object
*/
public $connID;
/**
* Result ID
*
* @var resource|object|boolean
*/
public $resultID;
/**
* Result Array
*
* @var array[]
*/
public $resultArray = [];
/**
* Result Object
*
* @var object[]
*/
public $resultObject = [];
/**
* Custom Result Object
*
* @var array
*/
public $customResultObject = [];
/**
* Current Row index
*
* @var integer
*/
public $currentRow = 0;
/**
* The number of records in the query result
*
* @var integer|null
*/
protected $numRows = null;
/**
* Row data
*
* @var array|null
*/
public $rowData;
//--------------------------------------------------------------------
/**
* Constructor
*
* @param object|resource $connID
* @param object|resource $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
*
* @return array
*/
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 string $className The name of the class to use.
*
* @return mixed
*/
public function getCustomResultObject(string $className)
{
if (isset($this->customResultObject[$className]))
{
return $this->customResultObject[$className];
}
if (is_bool($this->resultID) || ! $this->resultID)
{
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];
}
is_null($this->rowData) || $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;
}
// @phpstan-ignore-next-line
return $this->customResultObject[$className];
}
//--------------------------------------------------------------------
/**
* Returns the results as an array of arrays.
*
* If no results, an empty array is returned.
*
* @return array
*/
public function getResultArray(): array
{
if (! empty($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 (is_bool($this->resultID) || ! $this->resultID)
{
return [];
}
if ($this->resultObject)
{
foreach ($this->resultObject as $row)
{
$this->resultArray[] = (array) $row;
}
return $this->resultArray;
}
is_null($this->rowData) || $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 array
*/
public function getResultObject(): array
{
if (! empty($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 (is_bool($this->resultID) || ! $this->resultID)
{
return [];
}
if ($this->resultArray)
{
foreach ($this->resultArray as $row)
{
$this->resultObject[] = (object) $row;
}
return $this->resultObject;
}
is_null($this->rowData) || $this->dataSeek();
while ($row = $this->fetchObject())
{
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal'))
{
$row->syncOriginal();
}
$this->resultObject[] = $row;
}
// @phpstan-ignore-next-line
return $this->resultObject;
}
//--------------------------------------------------------------------
/**
* Wrapper object to return a row as either an array, an object, or
* a custom class.
*
* If row doesn't exist, returns null.
*
* @param mixed $n The index of the results to return
* @param string $type The type of result object. 'array', 'object' or class name.
*
* @return mixed
*/
public function getRow($n = 0, string $type = 'object')
{
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 row doesn't exists, returns null.
*
* @param integer $n
* @param string $className
*
* @return mixed
*/
public function getCustomRowObject(int $n, string $className)
{
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.
*
* @param integer $n
*
* @return mixed
*/
public function getRowArray(int $n = 0)
{
$result = $this->getResultArray();
if (empty($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.
*
* @param integer $n
*
* @return mixed
*/
public function getRowObject(int $n = 0)
{
$result = $this->getResultObject();
if (empty($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 mixed $key
* @param mixed $value
*
* @return mixed
*/
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.
*
* @param string $type
*
* @return mixed
*/
public function getFirstRow(string $type = 'object')
{
$result = $this->getResult($type);
return (empty($result)) ? null : $result[0];
}
//--------------------------------------------------------------------
/**
* Returns the "last" row of the current results.
*
* @param string $type
*
* @return mixed
*/
public function getLastRow(string $type = 'object')
{
$result = $this->getResult($type);
return (empty($result)) ? null : $result[count($result) - 1];
}
//--------------------------------------------------------------------
/**
* Returns the "next" row of the current results.
*
* @param string $type
*
* @return mixed
*/
public function getNextRow(string $type = 'object')
{
$result = $this->getResult($type);
if (empty($result))
{
return null;
}
return isset($result[$this->currentRow + 1]) ? $result[++ $this->currentRow] : null;
}
//--------------------------------------------------------------------
/**
* Returns the "previous" row of the current results.
*
* @param string $type
*
* @return mixed
*/
public function getPreviousRow(string $type = 'object')
{
$result = $this->getResult($type);
if (empty($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.
*
* @param string $type
*
* @return mixed
*/
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
*
* @return integer
*/
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());
}
/**
* Gets the number of fields in the result set.
*
* @return integer
*/
abstract public function getFieldCount(): int;
//--------------------------------------------------------------------
/**
* Generates an array of column names in the result set.
*
* @return array
*/
abstract public function getFieldNames(): array;
//--------------------------------------------------------------------
/**
* Generates an array of objects representing field meta-data.
*
* @return array
*/
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.
*
* @param integer $n
*
* @return mixed
*/
abstract public function dataSeek(int $n = 0);
//--------------------------------------------------------------------
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return mixed
*/
abstract protected function fetchAssoc();
//--------------------------------------------------------------------
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @param string $className
*
* @return object
*/
abstract protected function fetchObject(string $className = 'stdClass');
//--------------------------------------------------------------------
}
+393
View File
@@ -0,0 +1,393 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Class BaseUtils
*/
abstract class BaseUtils
{
/**
* Database object
*
* @var object
*/
protected $db;
//--------------------------------------------------------------------
/**
* List databases statement
*
* @var string|boolean
*/
protected $listDatabases = false;
/**
* OPTIMIZE TABLE statement
*
* @var string|boolean
*/
protected $optimizeTable = false;
/**
* REPAIR TABLE statement
*
* @var string|boolean
*/
protected $repairTable = false;
//--------------------------------------------------------------------
/**
* Class constructor
*
* @param ConnectionInterface $db
*/
public function __construct(ConnectionInterface &$db)
{
$this->db = & $db;
}
//--------------------------------------------------------------------
/**
* List databases
*
* @return array|boolean
* @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
*
* @param string $databaseName
* @return boolean
*/
public function databaseExists(string $databaseName): bool
{
return in_array($databaseName, $this->listDatabases(), true);
}
//--------------------------------------------------------------------
/**
* Optimize Table
*
* @param string $tableName
* @return boolean
* @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
*
* @param string $tableName
* @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
*
* @param ResultInterface $query Query result object
* @param string $delim Delimiter (default: ,)
* @param string $newline Newline character (default: \n)
* @param string $enclosure Enclosure (default: ")
*
* @return string
*/
public function getCSVFromResult(ResultInterface $query, string $delim = ',', string $newline = "\n", string $enclosure = '"')
{
$out = '';
// First generate the headings from the table column names
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, $item) . $enclosure;
}
$out .= implode($delim, $line) . $newline;
}
return $out;
}
//--------------------------------------------------------------------
/**
* Generate XML data from a query result object
*
* @param ResultInterface $query Query result object
* @param array $params Any preferences
*
* @return string
*/
public function getXMLFromResult(ResultInterface $query, array $params = []): string
{
// Set our default values
foreach (['root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t"] as $key => $val)
{
if (! isset($params[$key]))
{
$params[$key] = $val;
}
}
// Create variables for convenience
$root = $params['root'];
$newline = $params['newline'];
$tab = $params['tab'];
$element = $params['element'];
// Load the xml helper
helper('xml');
// Generate the result
$xml = '<' . $root . '>' . $newline;
while ($row = $query->getUnbufferedRow())
{
$xml .= $tab . '<' . $element . '>' . $newline;
foreach ($row as $key => $val)
{
$val = (! empty($val)) ? xml_convert($val) : '';
$xml .= $tab . $tab . '<' . $key . '>' . $val . '</' . $key . '>' . $newline;
}
$xml .= $tab . '</' . $element . '>' . $newline;
}
return $xml . '</' . $root . '>' . $newline;
}
//--------------------------------------------------------------------
/**
* Database Backup
*
* @param array|string $params
* @return mixed
* @throws DatabaseException
*/
public function backup($params = [])
{
// If the parameters have not been submitted as an
// array then we know that it is simply the table
// name, which is a valid short cut.
if (is_string($params))
{
$params = ['tables' => $params];
}
// Set up our default preferences
$prefs = [
'tables' => [],
'ignore' => [],
'filename' => '',
'format' => 'gzip', // gzip, txt
'add_drop' => true,
'add_insert' => true,
'newline' => "\n",
'foreign_key_checks' => true,
];
// Did the user submit any preferences? If so set them....
if (! empty($params))
{
foreach (array_keys($prefs) as $key)
{
if (isset($params[$key]))
{
$prefs[$key] = $params[$key];
}
}
}
// Are we backing up a complete database or individual tables?
// If no table names were submitted we'll fetch the entire table list
if (empty($prefs['tables']))
{
$prefs['tables'] = $this->db->listTables();
}
// Validate the format
if (! in_array($prefs['format'], ['gzip', 'txt'], true))
{
$prefs['format'] = 'txt';
}
// Is the encoder supported? If not, we'll either issue an
// error or use plain text depending on the debug settings
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') // Was a text file requested?
{
return $this->_backup($prefs);
}
return gzencode($this->_backup($prefs));
}
//--------------------------------------------------------------------
/**
* Platform dependent version of the backup function.
*
* @param array|null $prefs
*
* @return mixed
*/
abstract public function _backup(array $prefs = null);
//--------------------------------------------------------------------
}
+166
View File
@@ -0,0 +1,166 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Config\BaseConfig;
use InvalidArgumentException;
/**
* Class Config
*/
class Config extends BaseConfig
{
/**
* Cache for instance of any connections that
* have been requested as a "shared" instance.
*
* @var array
*/
static protected $instances = [];
/**
* The main instance used to manage all of
* our open database connections.
*
* @var Database|null
*/
static protected $factory;
//--------------------------------------------------------------------
/**
* Creates the default
*
* @param string|array $group The name of the connection group to use,
* or an array of configuration settings.
* @param boolean $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));
}
$config = $config ?? config('Database');
if (empty($group))
{
$group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup;
}
if (is_string($group) && ! isset($config->$group) && strpos($group, 'custom-') !== 0)
{
throw new InvalidArgumentException($group . ' is not a valid database connection group.');
}
if ($getShared && isset(static::$instances[$group]))
{
return static::$instances[$group];
}
static::ensureFactory();
if (isset($config->$group))
{
$config = $config->$group;
}
$connection = static::$factory->load($config, $group);
static::$instances[$group] = &$connection;
return $connection;
}
//--------------------------------------------------------------------
/**
* Returns an array of all db connections currently made.
*
* @return array
*/
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 ConnectionInterface|string|array|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 string|array|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 string|null $group
*
* @return Seeder
*/
public static function seeder(string $group = null)
{
$config = config('Database');
return new Seeder($config, static::connect($group));
}
//--------------------------------------------------------------------
/**
* Ensures the database Connection Manager/Factory is loaded and ready to use.
*/
protected static function ensureFactory()
{
if (static::$factory instanceof Database)
{
return;
}
static::$factory = new Database();
}
//--------------------------------------------------------------------
}
+202
View File
@@ -0,0 +1,202 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database;
/**
* Interface ConnectionInterface
*/
interface ConnectionInterface
{
/**
* Initializes the database connection/settings.
*
* @return mixed
*/
public function initialize();
//--------------------------------------------------------------------
/**
* Connect to the database.
*
* @param boolean $persistent
* @return mixed
*/
public function connect(bool $persistent = false);
//--------------------------------------------------------------------
/**
* Create a persistent database connection.
*
* @return mixed
*/
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 mixed
*/
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.
*
* @param string|null $alias
*
* @return mixed
*/
public function getConnection(string $alias = null);
//--------------------------------------------------------------------
/**
* Select a specific database table to use.
*
* @param string $databaseName
*
* @return mixed
*/
public function setDatabase(string $databaseName);
//--------------------------------------------------------------------
/**
* Returns the name of the current database being used.
*
* @return string
*/
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,string|int>
*/
public function error(): array;
//--------------------------------------------------------------------
/**
* The name of the platform in use (MySQLi, mssql, etc)
*
* @return string
*/
public function getPlatform(): string;
//--------------------------------------------------------------------
/**
* Returns a string containing the version of the database being used.
*
* @return string
*/
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 string $sql
* @param mixed ...$binds
*
* @return BaseResult|Query|boolean
*/
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.
*
* @param string $sql
*
* @return mixed
*/
public function simpleQuery(string $sql);
//--------------------------------------------------------------------
/**
* Returns an instance of the query builder for this connection.
*
* @param string|array $tableName Table name.
*
* @return BaseBuilder Builder.
*/
public function table($tableName);
//--------------------------------------------------------------------
/**
* Returns the last query's statement object.
*
* @return mixed
*/
public function getLastQuery();
//--------------------------------------------------------------------
/**
* "Smart" Escaping
*
* Escapes data based on type.
* Sets boolean and null types.
*
* @param mixed $str
*
* @return mixed
*/
public function escape($str);
//--------------------------------------------------------------------
/**
* Allows for custom calls to the database engine that are not
* supported through our database layer.
*
* @param string $functionName
* @param array ...$params
*
* @return mixed
*/
public function callFunction(string $functionName, ...$params);
//--------------------------------------------------------------------
/**
* Determines if the statement is a write-type query or not.
*
* @param string $sql
* @return boolean
*/
public function isWriteType($sql): bool;
}
+183
View File
@@ -0,0 +1,183 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database;
use InvalidArgumentException;
/**
* Database Connection Factory
*
* Creates and returns an instance of the appropriate DatabaseConnection
*/
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 returns an instance of the driver
* ready to go.
*
* @param array $params
* @param string $alias
*
* @return mixed
*
* @throws InvalidArgumentException
*
* @internal param bool $useBuilder
*/
public function load(array $params = [], string $alias = '')
{
if ($alias === '')
{
throw new InvalidArgumentException('You must supply the parameter: alias.');
}
// Handle universal DSN connection string
if (! empty($params['DSN']) && strpos($params['DSN'], '://') !== false)
{
$params = $this->parseDSN($params);
}
// No DB specified? Beat them senseless...
if (empty($params['DBDriver']))
{
throw new InvalidArgumentException('You have not selected a database type to connect to.');
}
// Store the connection
$this->connections[$alias] = $this->initDriver($params['DBDriver'], 'Connection', $params);
return $this->connections[$alias];
}
//--------------------------------------------------------------------
/**
* Creates a Forge instance for the current database type.
*
* @param ConnectionInterface $db
*
* @return object
*/
public function loadForge(ConnectionInterface $db): object
{
// Initialize database connection if not exists.
if (! $db->connID)
{
$db->initialize();
}
return $this->initDriver($db->DBDriver, 'Forge', $db);
}
//--------------------------------------------------------------------
/**
* Creates a Utils instance for the current database type.
*
* @param ConnectionInterface $db
*
* @return object
*/
public function loadUtils(ConnectionInterface $db): object
{
// Initialize database connection if not exists.
if (! $db->connID)
{
$db->initialize();
}
return $this->initDriver($db->DBDriver, 'Utils', $db);
}
//--------------------------------------------------------------------
/**
* Parse universal DSN string
*
* @param array $params
*
* @return array
*
* @throws InvalidArgumentException
*/
protected function parseDSN(array $params): array
{
$dsn = parse_url($params['DSN']);
if (! $dsn)
{
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)) : '',
];
// Do we have additional config items set?
if (! empty($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);
}
//--------------------------------------------------------------------
/**
* Initialize database driver.
*
* @param string $driver Database driver name (e.g. 'MySQLi')
* @param string $class Database class name (e.g. 'Forge')
* @param array|object $argument
*
* @return object
*/
protected function initDriver(string $driver, string $class, $argument): object
{
$class = $driver . '\\' . $class;
if (strpos($driver, '\\') === false)
{
$class = "CodeIgniter\Database\\{$class}";
}
return new $class($argument);
}
}
@@ -0,0 +1,93 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database\Exceptions;
use CodeIgniter\Exceptions\DebugTraceableTrait;
use RuntimeException;
class DataException extends RuntimeException implements ExceptionInterface
{
use DebugTraceableTrait;
/**
* Used by the Model's trigger() method when the callback cannot be found.
*
* @param string $method
*
* @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.
*
* @param string $mode
*
* @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.
*
* @param string $mode
*
* @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.
*
* @param string $argument
*
* @return DataException
*/
public static function forInvalidArgument(string $argument)
{
return new static(lang('Database.invalidArgument', [$argument]));
}
public static function forInvalidAllowedFields(string $model)
{
return new static(lang('Database.invalidAllowedFields', [$model]));
}
public static function forTableNotFound(string $table)
{
return new static(lang('Database.tableNotFound', [$table]));
}
public static function forEmptyInputGiven(string $argument)
{
return new static(lang('Database.forEmptyInputGiven', [$argument]));
}
public static function forFindColumnHaveMultipleColumns()
{
return new static(lang('Database.forFindColumnHaveMultipleColumns'));
}
}
@@ -0,0 +1,24 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database\Exceptions;
use Error;
class DatabaseException extends Error implements ExceptionInterface
{
/**
* Exit status code
*
* @var integer
*/
protected $code = 8;
}
@@ -0,0 +1,22 @@
<?php
/**
* 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.
*/
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
+82
View File
@@ -0,0 +1,82 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database;
use Config\Database;
/**
* Class Migration
*/
abstract class Migration
{
/**
* The name of the database group to use.
*
* @var string
*/
protected $DBGroup;
/**
* Database Connection instance
*
* @var ConnectionInterface
*/
protected $db;
/**
* Database Forge instance.
*
* @var Forge
*/
protected $forge;
//--------------------------------------------------------------------
/**
* Constructor.
*
* @param Forge $forge
*/
public function __construct(Forge $forge = null)
{
$this->forge = ! is_null($forge) ? $forge : Database::forge($this->DBGroup ?? config('Database')->defaultGroup);
$this->db = $this->forge->getConnection();
}
//--------------------------------------------------------------------
/**
* Returns the database group name this migration uses.
*
* @return string
*/
public function getDBGroup(): ?string
{
return $this->DBGroup;
}
//--------------------------------------------------------------------
/**
* Perform a migration step.
*/
abstract public function up();
//--------------------------------------------------------------------
/**
* Revert a migration step.
*/
abstract public function down();
//--------------------------------------------------------------------
}
File diff suppressed because it is too large Load Diff
+57
View File
@@ -0,0 +1,57 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Config\Factories;
/**
* Returns new or shared Model instances
*
* @deprecated Use CodeIgniter\Config\Factories::models()
*
* @codeCoverageIgnore
*/
class ModelFactory
{
/**
* Creates new Model instances or returns a shared instance
*
* @param string $name Model name, namespace optional
* @param boolean $getShared Use shared instance
* @param ConnectionInterface $connection
*
* @return mixed|null
*/
public static function get(string $name, bool $getShared = true, ConnectionInterface $connection = null)
{
return Factories::models($name, ['getShared' => $getShared], $connection);
}
/**
* Helper method for injecting mock instances while testing.
*
* @param string $name
* @param object $instance
*/
public static function injectMock(string $name, $instance)
{
Factories::injectMock('models', $name, $instance);
}
/**
* Resets the static arrays
*/
public static function reset()
{
Factories::reset('models');
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseBuilder;
/**
* Builder for MySQLi
*/
class Builder extends BaseBuilder
{
/**
* Identifier escape character
*
* @var string
*/
protected $escapeChar = '`';
/**
* Specifies which sql statements
* support the ignore option.
*
* @var array
*/
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.
*
* @return string
*/
protected function _fromTables(): string
{
if (! empty($this->QBJoin) && count($this->QBFrom) > 1)
{
return '(' . implode(', ', $this->QBFrom) . ')';
}
return implode(', ', $this->QBFrom);
}
}
+728
View File
@@ -0,0 +1,728 @@
<?php
/**
* 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.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use LogicException;
use MySQLi;
use mysqli_sql_exception;
use stdClass;
use Throwable;
/**
* Connection for MySQLi
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
public $DBDriver = 'MySQLi';
/**
* DELETE hack flag
*
* Whether to use the MySQL "delete hack" which allows the number
* of affected rows to be shown. Uses a preg_replace when enabled,
* adding a bit more processing to all queries.
*
* @var boolean
*/
public $deleteHack = true;
// --------------------------------------------------------------------
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '`';
// --------------------------------------------------------------------
/**
* MySQLi object
*
* Has to be preserved without being assigned to $conn_id.
*
* @var MySQLi
*/
public $mysqli;
//--------------------------------------------------------------------
/**
* Connect to the database.
*
* @param boolean $persistent
*
* @return mixed
* @throws DatabaseException
*/
public function connect(bool $persistent = false)
{
// Do we have a socket path?
if ($this->hostname[0] === '/')
{
$hostname = null;
$port = null;
$socket = $this->hostname;
}
else
{
$hostname = ($persistent === true) ? 'p:' . $this->hostname : $this->hostname;
$port = empty($this->port) ? null : $this->port;
$socket = '';
}
$clientFlags = ($this->compress === true) ? MYSQLI_CLIENT_COMPRESS : 0;
$this->mysqli = mysqli_init();
mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX);
$this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
if (isset($this->strictOn))
{
if ($this->strictOn)
{
$this->mysqli->options(MYSQLI_INIT_COMMAND,
'SET SESSION sql_mode = CONCAT(@@sql_mode, ",", "STRICT_ALL_TABLES")');
}
else
{
$this->mysqli->options(MYSQLI_INIT_COMMAND, 'SET SESSION sql_mode =
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
@@sql_mode,
"STRICT_ALL_TABLES,", ""),
",STRICT_ALL_TABLES", ""),
"STRICT_ALL_TABLES", ""),
"STRICT_TRANS_TABLES,", ""),
",STRICT_TRANS_TABLES", ""),
"STRICT_TRANS_TABLES", "")'
);
}
}
if (is_array($this->encrypt))
{
$ssl = [];
if (! empty($this->encrypt['ssl_key']))
{
$ssl['key'] = $this->encrypt['ssl_key'];
}
if (! empty($this->encrypt['ssl_cert']))
{
$ssl['cert'] = $this->encrypt['ssl_cert'];
}
if (! empty($this->encrypt['ssl_ca']))
{
$ssl['ca'] = $this->encrypt['ssl_ca'];
}
if (! empty($this->encrypt['ssl_capath']))
{
$ssl['capath'] = $this->encrypt['ssl_capath'];
}
if (! empty($this->encrypt['ssl_cipher']))
{
$ssl['cipher'] = $this->encrypt['ssl_cipher'];
}
if (! empty($ssl))
{
if (isset($this->encrypt['ssl_verify']))
{
if ($this->encrypt['ssl_verify'])
{
defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT') &&
$this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
}
// Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT
// to FALSE didn't do anything, so PHP 5.6.16 introduced yet another
// constant ...
//
// https://secure.php.net/ChangeLog-5.php#5.6.16
// https://bugs.php.net/bug.php?id=68344
elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT') && version_compare($this->mysqli->client_info, 'mysqlnd 5.6', '>='))
{
$clientFlags += MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
}
}
$clientFlags += MYSQLI_CLIENT_SSL;
$this->mysqli->ssl_set(
$ssl['key'] ?? null, $ssl['cert'] ?? null, $ssl['ca'] ?? null,
$ssl['capath'] ?? null, $ssl['cipher'] ?? null
);
}
}
try
{
if ($this->mysqli->real_connect($hostname, $this->username, $this->password,
$this->database, $port, $socket, $clientFlags)
)
{
// Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails
if (($clientFlags & MYSQLI_CLIENT_SSL) && version_compare($this->mysqli->client_info, 'mysqlnd 5.7.3', '<=')
&& empty($this->mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")
->fetch_object()->Value)
)
{
$this->mysqli->close();
$message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!';
log_message('error', $message);
if ($this->DBDebug)
{
throw new DatabaseException($message);
}
return false;
}
if (! $this->mysqli->set_charset($this->charset))
{
log_message('error',
"Database: Unable to set the configured connection charset ('{$this->charset}').");
$this->mysqli->close();
if ($this->DBDebug)
{
throw new DatabaseException('Unable to set client connection character set: ' . $this->charset);
}
return false;
}
return $this->mysqli;
}
}
catch (Throwable $e)
{
// Clean sensitive information from errors.
$msg = $e->getMessage();
$msg = str_replace($this->username, '****', $msg);
$msg = str_replace($this->password, '****', $msg);
throw new DatabaseException($msg, $e->getCode(), $e);
}
return false;
}
//--------------------------------------------------------------------
/**
* 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()
{
$this->close();
$this->initialize();
}
//--------------------------------------------------------------------
/**
* Close the database connection.
*
* @return void
*/
protected function _close()
{
$this->connID->close();
}
//--------------------------------------------------------------------
/**
* Select a specific database table to use.
*
* @param string $databaseName
*
* @return boolean
*/
public function setDatabase(string $databaseName): bool
{
if ($databaseName === '')
{
$databaseName = $this->database;
}
if (empty($this->connID))
{
$this->initialize();
}
if ($this->connID->select_db($databaseName))
{
$this->database = $databaseName;
return true;
}
return false;
}
//--------------------------------------------------------------------
/**
* Returns a string containing the version of the database being used.
*
* @return string
*/
public function getVersion(): string
{
if (isset($this->dataCache['version']))
{
return $this->dataCache['version'];
}
if (empty($this->mysqli))
{
$this->initialize();
}
return $this->dataCache['version'] = $this->mysqli->server_info;
}
//--------------------------------------------------------------------
/**
* Executes the query against the database.
*
* @param string $sql
*
* @return mixed
*/
public function execute(string $sql)
{
while ($this->connID->more_results())
{
$this->connID->next_result();
if ($res = $this->connID->store_result())
{
$res->free();
}
}
try
{
return $this->connID->query($this->prepQuery($sql));
}
catch (mysqli_sql_exception $e)
{
log_message('error', $e->getMessage());
if ($this->DBDebug)
{
throw $e;
}
}
return false;
}
//--------------------------------------------------------------------
/**
* Prep the query
*
* If needed, each database adapter can prep the query string
*
* @param string $sql an SQL query
*
* @return string
*/
protected function prepQuery(string $sql): string
{
// mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
// modifies the query so that it a proper number of affected rows is returned.
if ($this->deleteHack === true && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql))
{
return trim($sql) . ' WHERE 1=1';
}
return $sql;
}
//--------------------------------------------------------------------
/**
* Returns the total number of rows affected by this query.
*
* @return integer
*/
public function affectedRows(): int
{
return $this->connID->affected_rows ?? 0;
}
//--------------------------------------------------------------------
/**
* Platform-dependant string escape
*
* @param string $str
* @return string
*/
protected function _escapeString(string $str): string
{
if (! $this->connID)
{
$this->initialize();
}
return $this->connID->real_escape_string($str);
}
//--------------------------------------------------------------------
/**
* Escape Like String Direct
* There are a few instances where MySQLi queries cannot take the
* additional "ESCAPE x" parameter for specifying the escape character
* in "LIKE" strings, and this handles those directly with a backslash.
*
* @param string|string[] $str Input string
* @return string|string[]
*/
public function escapeLikeStringDirect($str)
{
if (is_array($str))
{
foreach ($str as $key => $val)
{
$str[$key] = $this->escapeLikeStringDirect($val);
}
return $str;
}
$str = $this->_escapeString($str);
// Escape LIKE condition wildcards
return str_replace([
$this->likeEscapeChar,
'%',
'_',
], [
'\\' . $this->likeEscapeChar,
'\\' . '%',
'\\' . '_',
], $str
);
}
//--------------------------------------------------------------------
/**
* Generates the SQL for listing tables in a platform-dependent manner.
* Uses escapeLikeStringDirect().
*
* @param boolean $prefixLimit
*
* @return string
*/
protected function _listTables(bool $prefixLimit = false): string
{
$sql = 'SHOW TABLES FROM ' . $this->escapeIdentifiers($this->database);
if ($prefixLimit !== false && $this->DBPrefix !== '')
{
return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'";
}
return $sql;
}
//--------------------------------------------------------------------
/**
* Generates a platform-specific query string so that the column names can be fetched.
*
* @param string $table
*
* @return string
*/
protected function _listColumns(string $table = ''): string
{
return 'SHOW COLUMNS FROM ' . $this->protectIdentifiers($table, true, null, false);
}
//--------------------------------------------------------------------
/**
* Returns an array of objects with field data
*
* @param string $table
* @return stdClass[]
* @throws DatabaseException
*/
public function _fieldData(string $table): array
{
$table = $this->protectIdentifiers($table, true, null, false);
if (($query = $this->query('SHOW COLUMNS FROM ' . $table)) === false)
{
throw new DatabaseException(lang('Database.failGetFieldData'));
}
$query = $query->getResultObject();
$retVal = [];
for ($i = 0, $c = count($query); $i < $c; $i++)
{
$retVal[$i] = new stdClass();
$retVal[$i]->name = $query[$i]->Field;
sscanf($query[$i]->Type, '%[a-z](%d)', $retVal[$i]->type, $retVal[$i]->max_length);
$retVal[$i]->nullable = $query[$i]->Null === 'YES';
$retVal[$i]->default = $query[$i]->Default;
$retVal[$i]->primary_key = (int) ($query[$i]->Key === 'PRI');
}
return $retVal;
}
//--------------------------------------------------------------------
/**
* Returns an array of objects with index data
*
* @param string $table
* @return stdClass[]
* @throws DatabaseException
* @throws LogicException
*/
public function _indexData(string $table): array
{
$table = $this->protectIdentifiers($table, true, null, false);
if (($query = $this->query('SHOW INDEX FROM ' . $table)) === false)
{
throw new DatabaseException(lang('Database.failGetIndexData'));
}
if (! $indexes = $query->getResultArray())
{
return [];
}
$keys = [];
foreach ($indexes as $index)
{
if (empty($keys[$index['Key_name']]))
{
$keys[$index['Key_name']] = new stdClass();
$keys[$index['Key_name']]->name = $index['Key_name'];
if ($index['Key_name'] === 'PRIMARY')
{
$type = 'PRIMARY';
}
elseif ($index['Index_type'] === 'FULLTEXT')
{
$type = 'FULLTEXT';
}
elseif ($index['Non_unique'])
{
$type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
}
else
{
$type = 'UNIQUE';
}
$keys[$index['Key_name']]->type = $type;
}
$keys[$index['Key_name']]->fields[] = $index['Column_name'];
}
return $keys;
}
//--------------------------------------------------------------------
/**
* Returns an array of objects with Foreign key data
*
* @param string $table
* @return stdClass[]
* @throws DatabaseException
*/
public function _foreignKeyData(string $table): array
{
$sql = '
SELECT
tc.CONSTRAINT_NAME,
tc.TABLE_NAME,
kcu.COLUMN_NAME,
rc.REFERENCED_TABLE_NAME,
kcu.REFERENCED_COLUMN_NAME
FROM information_schema.TABLE_CONSTRAINTS AS tc
INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc
ON tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
INNER JOIN information_schema.KEY_COLUMN_USAGE AS kcu
ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
WHERE
tc.CONSTRAINT_TYPE = ' . $this->escape('FOREIGN KEY') . ' AND
tc.TABLE_SCHEMA = ' . $this->escape($this->database) . ' AND
tc.TABLE_NAME = ' . $this->escape($table);
if (($query = $this->query($sql)) === false)
{
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
}
$query = $query->getResultObject();
$retVal = [];
foreach ($query as $row)
{
$obj = new stdClass();
$obj->constraint_name = $row->CONSTRAINT_NAME;
$obj->table_name = $row->TABLE_NAME;
$obj->column_name = $row->COLUMN_NAME;
$obj->foreign_table_name = $row->REFERENCED_TABLE_NAME;
$obj->foreign_column_name = $row->REFERENCED_COLUMN_NAME;
$retVal[] = $obj;
}
return $retVal;
}
//--------------------------------------------------------------------
/**
* Returns platform-specific SQL to disable foreign key checks.
*
* @return string
*/
protected function _disableForeignKeyChecks()
{
return 'SET FOREIGN_KEY_CHECKS=0';
}
//--------------------------------------------------------------------
/**
* Returns platform-specific SQL to enable foreign key checks.
*
* @return string
*/
protected function _enableForeignKeyChecks()
{
return 'SET FOREIGN_KEY_CHECKS=1';
}
//--------------------------------------------------------------------
/**
* Returns the last error code and message.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string,string|int>
*/
public function error(): array
{
if (! empty($this->mysqli->connect_errno))
{
return [
'code' => $this->mysqli->connect_errno,
'message' => $this->mysqli->connect_error,
];
}
return [
'code' => $this->connID->errno,
'message' => $this->connID->error,
];
}
//--------------------------------------------------------------------
/**
* Insert ID
*
* @return integer
*/
public function insertID(): int
{
return $this->connID->insert_id;
}
//--------------------------------------------------------------------
/**
* Begin Transaction
*
* @return boolean
*/
protected function _transBegin(): bool
{
$this->connID->autocommit(false);
return $this->connID->begin_transaction();
}
//--------------------------------------------------------------------
/**
* Commit Transaction
*
* @return boolean
*/
protected function _transCommit(): bool
{
if ($this->connID->commit())
{
$this->connID->autocommit(true);
return true;
}
return false;
}
//--------------------------------------------------------------------
/**
* Rollback Transaction
*
* @return boolean
*/
protected function _transRollback(): bool
{
if ($this->connID->rollback())
{
$this->connID->autocommit(true);
return true;
}
return false;
}
//--------------------------------------------------------------------
}
+255
View File
@@ -0,0 +1,255 @@
<?php
/**
* 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.
*/
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 boolean
*/
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
* @return string
*/
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 (! empty($this->db->charset) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET'))
{
$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
}
if (! empty($this->db->DBCollat) && ! strpos($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 mixed $field Column definition
* @return string|string[]
*/
protected function _alterTable(string $alterType, string $table, $field)
{
if ($alterType === 'DROP')
{
return parent::_alterTable($alterType, $table, $field);
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
foreach ($field as $i => $data)
{
if ($data['_literal'] !== false)
{
$field[$i] = ($alterType === 'ADD') ? "\n\tADD " . $data['_literal'] : "\n\tMODIFY " . $data['_literal'];
}
else
{
if ($alterType === 'ADD')
{
$field[$i]['_literal'] = "\n\tADD ";
}
else
{
$field[$i]['_literal'] = empty($data['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
}
$field[$i] = $field[$i]['_literal'] . $this->_processColumn($field[$i]);
}
}
return [$sql . implode(',', $field)];
}
//--------------------------------------------------------------------
/**
* Process column
*
* @param array $field
* @return string
*/
protected function _processColumn(array $field): string
{
$extraClause = isset($field['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($field['after']) : '';
if (empty($extraClause) && isset($field['first']) && $field['first'] === true)
{
$extraClause = ' FIRST';
}
return $this->db->escapeIdentifiers($field['name'])
. (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name']))
. ' ' . $field['type'] . $field['length']
. $field['unsigned']
. $field['null']
. $field['default']
. $field['auto_increment']
. $field['unique']
. (empty($field['comment']) ? '' : ' COMMENT ' . $field['comment'])
. $extraClause;
}
//--------------------------------------------------------------------
/**
* Process indexes
*
* @param string $table (ignored)
* @return string
*/
protected function _processIndexes(string $table): string
{
$sql = '';
for ($i = 0, $c = count($this->keys); $i < $c; $i ++)
{
if (is_array($this->keys[$i]))
{
for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2 ++)
{
if (! isset($this->fields[$this->keys[$i][$i2]]))
{
unset($this->keys[$i][$i2]);
continue;
}
}
}
elseif (! isset($this->fields[$this->keys[$i]]))
{
unset($this->keys[$i]);
continue;
}
if (! is_array($this->keys[$i]))
{
$this->keys[$i] = [$this->keys[$i]];
}
$unique = in_array($i, $this->uniqueKeys, true) ? 'UNIQUE ' : '';
$sql .= ",\n\t{$unique}KEY " . $this->db->escapeIdentifiers(implode('_', $this->keys[$i]))
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ')';
}
$this->keys = [];
return $sql;
}
}

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