mirror of
https://github.com/codeigniter4/CodeIgniter4.git
synced 2025-02-20 11:44:28 +08:00
1264 lines
37 KiB
PHP
1264 lines
37 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* This file is part of CodeIgniter 4 framework.
|
|
*
|
|
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
|
*
|
|
* For the full copyright and license information, please view
|
|
* the LICENSE file that was distributed with this source code.
|
|
*/
|
|
|
|
use CodeIgniter\Cache\CacheInterface;
|
|
use CodeIgniter\Config\BaseConfig;
|
|
use CodeIgniter\Config\Factories;
|
|
use CodeIgniter\Cookie\Cookie;
|
|
use CodeIgniter\Cookie\CookieStore;
|
|
use CodeIgniter\Cookie\Exceptions\CookieException;
|
|
use CodeIgniter\Database\BaseConnection;
|
|
use CodeIgniter\Database\ConnectionInterface;
|
|
use CodeIgniter\Debug\Timer;
|
|
use CodeIgniter\Exceptions\InvalidArgumentException;
|
|
use CodeIgniter\Exceptions\RuntimeException;
|
|
use CodeIgniter\Files\Exceptions\FileNotFoundException;
|
|
use CodeIgniter\HTTP\CLIRequest;
|
|
use CodeIgniter\HTTP\Exceptions\HTTPException;
|
|
use CodeIgniter\HTTP\Exceptions\RedirectException;
|
|
use CodeIgniter\HTTP\IncomingRequest;
|
|
use CodeIgniter\HTTP\RedirectResponse;
|
|
use CodeIgniter\HTTP\RequestInterface;
|
|
use CodeIgniter\HTTP\ResponseInterface;
|
|
use CodeIgniter\Language\Language;
|
|
use CodeIgniter\Model;
|
|
use CodeIgniter\Session\Session;
|
|
use CodeIgniter\Test\TestLogger;
|
|
use Config\App;
|
|
use Config\Database;
|
|
use Config\DocTypes;
|
|
use Config\Logger;
|
|
use Config\Services;
|
|
use Config\View;
|
|
use Laminas\Escaper\Escaper;
|
|
|
|
// Services Convenience Functions
|
|
|
|
if (! function_exists('app_timezone')) {
|
|
/**
|
|
* Returns the timezone the application has been set to display
|
|
* dates in. This might be different than the timezone set
|
|
* at the server level, as you often want to stores dates in UTC
|
|
* and convert them on the fly for the user.
|
|
*/
|
|
function app_timezone(): string
|
|
{
|
|
$config = config(App::class);
|
|
|
|
return $config->appTimezone;
|
|
}
|
|
}
|
|
|
|
if (! function_exists('cache')) {
|
|
/**
|
|
* A convenience method that provides access to the Cache
|
|
* object. If no parameter is provided, will return the object,
|
|
* otherwise, will attempt to return the cached value.
|
|
*
|
|
* Examples:
|
|
* cache()->save('foo', 'bar');
|
|
* $foo = cache('bar');
|
|
*
|
|
* @return array|bool|CacheInterface|float|int|object|string|null
|
|
* @phpstan-return ($key is null ? CacheInterface : array|bool|float|int|object|string|null)
|
|
*/
|
|
function cache(?string $key = null)
|
|
{
|
|
$cache = service('cache');
|
|
|
|
// No params - return cache object
|
|
if ($key === null) {
|
|
return $cache;
|
|
}
|
|
|
|
// Still here? Retrieve the value.
|
|
return $cache->get($key);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('clean_path')) {
|
|
/**
|
|
* A convenience method to clean paths for
|
|
* a nicer looking output. Useful for exception
|
|
* handling, error logging, etc.
|
|
*/
|
|
function clean_path(string $path): string
|
|
{
|
|
// Resolve relative paths
|
|
try {
|
|
$path = realpath($path) ?: $path;
|
|
} catch (ErrorException|ValueError) {
|
|
$path = 'error file path: ' . urlencode($path);
|
|
}
|
|
|
|
return match (true) {
|
|
str_starts_with($path, APPPATH) => 'APPPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(APPPATH)),
|
|
str_starts_with($path, SYSTEMPATH) => 'SYSTEMPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(SYSTEMPATH)),
|
|
str_starts_with($path, FCPATH) => 'FCPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(FCPATH)),
|
|
defined('VENDORPATH') && str_starts_with($path, VENDORPATH) => 'VENDORPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(VENDORPATH)),
|
|
str_starts_with($path, ROOTPATH) => 'ROOTPATH' . DIRECTORY_SEPARATOR . substr($path, strlen(ROOTPATH)),
|
|
default => $path,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (! function_exists('command')) {
|
|
/**
|
|
* Runs a single command.
|
|
* Input expected in a single string as would
|
|
* be used on the command line itself:
|
|
*
|
|
* > command('migrate:create SomeMigration');
|
|
*
|
|
* @return false|string
|
|
*/
|
|
function command(string $command)
|
|
{
|
|
$runner = service('commands');
|
|
$regexString = '([^\s]+?)(?:\s|(?<!\\\\)"|(?<!\\\\)\'|$)';
|
|
$regexQuoted = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')';
|
|
|
|
$args = [];
|
|
$length = strlen($command);
|
|
$cursor = 0;
|
|
|
|
/**
|
|
* Adopted from Symfony's `StringInput::tokenize()` with few changes.
|
|
*
|
|
* @see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Console/Input/StringInput.php
|
|
*/
|
|
while ($cursor < $length) {
|
|
if (preg_match('/\s+/A', $command, $match, 0, $cursor)) {
|
|
// nothing to do
|
|
} elseif (preg_match('/' . $regexQuoted . '/A', $command, $match, 0, $cursor)) {
|
|
$args[] = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2));
|
|
} elseif (preg_match('/' . $regexString . '/A', $command, $match, 0, $cursor)) {
|
|
$args[] = stripcslashes($match[1]);
|
|
} else {
|
|
// @codeCoverageIgnoreStart
|
|
throw new InvalidArgumentException(sprintf(
|
|
'Unable to parse input near "... %s ...".',
|
|
substr($command, $cursor, 10),
|
|
));
|
|
// @codeCoverageIgnoreEnd
|
|
}
|
|
|
|
$cursor += strlen($match[0]);
|
|
}
|
|
|
|
$command = array_shift($args);
|
|
$params = [];
|
|
$optionValue = false;
|
|
|
|
foreach ($args as $i => $arg) {
|
|
if (mb_strpos($arg, '-') !== 0) {
|
|
if ($optionValue) {
|
|
// if this was an option value, it was already
|
|
// included in the previous iteration
|
|
$optionValue = false;
|
|
} else {
|
|
// add to segments if not starting with '-'
|
|
// and not an option value
|
|
$params[] = $arg;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
$arg = ltrim($arg, '-');
|
|
$value = null;
|
|
|
|
if (isset($args[$i + 1]) && mb_strpos($args[$i + 1], '-') !== 0) {
|
|
$value = $args[$i + 1];
|
|
$optionValue = true;
|
|
}
|
|
|
|
$params[$arg] = $value;
|
|
}
|
|
|
|
ob_start();
|
|
$runner->run($command, $params);
|
|
|
|
return ob_get_clean();
|
|
}
|
|
}
|
|
|
|
if (! function_exists('config')) {
|
|
/**
|
|
* More simple way of getting config instances from Factories
|
|
*
|
|
* @template ConfigTemplate of BaseConfig
|
|
*
|
|
* @param class-string<ConfigTemplate>|string $name
|
|
*
|
|
* @return ConfigTemplate|null
|
|
* @phpstan-return ($name is class-string<ConfigTemplate> ? ConfigTemplate : object|null)
|
|
*/
|
|
function config(string $name, bool $getShared = true)
|
|
{
|
|
if ($getShared) {
|
|
return Factories::get('config', $name);
|
|
}
|
|
|
|
return Factories::config($name, ['getShared' => $getShared]);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('cookie')) {
|
|
/**
|
|
* Simpler way to create a new Cookie instance.
|
|
*
|
|
* @param string $name Name of the cookie
|
|
* @param string $value Value of the cookie
|
|
* @param array $options Array of options to be passed to the cookie
|
|
*
|
|
* @throws CookieException
|
|
*/
|
|
function cookie(string $name, string $value = '', array $options = []): Cookie
|
|
{
|
|
return new Cookie($name, $value, $options);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('cookies')) {
|
|
/**
|
|
* Fetches the global `CookieStore` instance held by `Response`.
|
|
*
|
|
* @param list<Cookie> $cookies If `getGlobal` is false, this is passed to CookieStore's constructor
|
|
* @param bool $getGlobal If false, creates a new instance of CookieStore
|
|
*/
|
|
function cookies(array $cookies = [], bool $getGlobal = true): CookieStore
|
|
{
|
|
if ($getGlobal) {
|
|
return service('response')->getCookieStore();
|
|
}
|
|
|
|
return new CookieStore($cookies);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('csrf_token')) {
|
|
/**
|
|
* Returns the CSRF token name.
|
|
* Can be used in Views when building hidden inputs manually,
|
|
* or used in javascript vars when using APIs.
|
|
*/
|
|
function csrf_token(): string
|
|
{
|
|
return service('security')->getTokenName();
|
|
}
|
|
}
|
|
|
|
if (! function_exists('csrf_header')) {
|
|
/**
|
|
* Returns the CSRF header name.
|
|
* Can be used in Views by adding it to the meta tag
|
|
* or used in javascript to define a header name when using APIs.
|
|
*/
|
|
function csrf_header(): string
|
|
{
|
|
return service('security')->getHeaderName();
|
|
}
|
|
}
|
|
|
|
if (! function_exists('csrf_hash')) {
|
|
/**
|
|
* Returns the current hash value for the CSRF protection.
|
|
* Can be used in Views when building hidden inputs manually,
|
|
* or used in javascript vars for API usage.
|
|
*/
|
|
function csrf_hash(): string
|
|
{
|
|
return service('security')->getHash();
|
|
}
|
|
}
|
|
|
|
if (! function_exists('csrf_field')) {
|
|
/**
|
|
* Generates a hidden input field for use within manually generated forms.
|
|
*
|
|
* @param non-empty-string|null $id
|
|
*/
|
|
function csrf_field(?string $id = null): string
|
|
{
|
|
return '<input type="hidden"' . ($id !== null ? ' id="' . esc($id, 'attr') . '"' : '') . ' name="' . csrf_token() . '" value="' . csrf_hash() . '"' . _solidus() . '>';
|
|
}
|
|
}
|
|
|
|
if (! function_exists('csrf_meta')) {
|
|
/**
|
|
* Generates a meta tag for use within javascript calls.
|
|
*
|
|
* @param non-empty-string|null $id
|
|
*/
|
|
function csrf_meta(?string $id = null): string
|
|
{
|
|
return '<meta' . ($id !== null ? ' id="' . esc($id, 'attr') . '"' : '') . ' name="' . csrf_header() . '" content="' . csrf_hash() . '"' . _solidus() . '>';
|
|
}
|
|
}
|
|
|
|
if (! function_exists('csp_style_nonce')) {
|
|
/**
|
|
* Generates a nonce attribute for style tag.
|
|
*/
|
|
function csp_style_nonce(): string
|
|
{
|
|
$csp = service('csp');
|
|
|
|
if (! $csp->enabled()) {
|
|
return '';
|
|
}
|
|
|
|
return 'nonce="' . $csp->getStyleNonce() . '"';
|
|
}
|
|
}
|
|
|
|
if (! function_exists('csp_script_nonce')) {
|
|
/**
|
|
* Generates a nonce attribute for script tag.
|
|
*/
|
|
function csp_script_nonce(): string
|
|
{
|
|
$csp = service('csp');
|
|
|
|
if (! $csp->enabled()) {
|
|
return '';
|
|
}
|
|
|
|
return 'nonce="' . $csp->getScriptNonce() . '"';
|
|
}
|
|
}
|
|
|
|
if (! function_exists('db_connect')) {
|
|
/**
|
|
* Grabs a database connection and returns it to the user.
|
|
*
|
|
* This is a convenience wrapper for \Config\Database::connect()
|
|
* and supports the same parameters. Namely:
|
|
*
|
|
* When passing in $db, you may pass any of the following to connect:
|
|
* - group name
|
|
* - existing connection instance
|
|
* - array of database configuration values
|
|
*
|
|
* If $getShared === false then a new connection instance will be provided,
|
|
* otherwise it will all calls will return the same instance.
|
|
*
|
|
* @param array|ConnectionInterface|string|null $db
|
|
*
|
|
* @return BaseConnection
|
|
*/
|
|
function db_connect($db = null, bool $getShared = true)
|
|
{
|
|
return Database::connect($db, $getShared);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('env')) {
|
|
/**
|
|
* Allows user to retrieve values from the environment
|
|
* variables that have been set. Especially useful for
|
|
* retrieving values set from the .env file for
|
|
* use in config files.
|
|
*
|
|
* @param string|null $default
|
|
*
|
|
* @return bool|string|null
|
|
*/
|
|
function env(string $key, $default = null)
|
|
{
|
|
$value = $_ENV[$key] ?? $_SERVER[$key] ?? getenv($key);
|
|
|
|
// Not found? Return the default value
|
|
if ($value === false) {
|
|
return $default;
|
|
}
|
|
|
|
// Handle any boolean values
|
|
return match (strtolower($value)) {
|
|
'true' => true,
|
|
'false' => false,
|
|
'empty' => '',
|
|
'null' => null,
|
|
default => $value,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (! function_exists('esc')) {
|
|
/**
|
|
* Performs simple auto-escaping of data for security reasons.
|
|
* Might consider making this more complex at a later date.
|
|
*
|
|
* If $data is a string, then it simply escapes and returns it.
|
|
* If $data is an array, then it loops over it, escaping each
|
|
* 'value' of the key/value pairs.
|
|
*
|
|
* @param array|string $data
|
|
* @phpstan-param 'html'|'js'|'css'|'url'|'attr'|'raw' $context
|
|
* @param string|null $encoding Current encoding for escaping.
|
|
* If not UTF-8, we convert strings from this encoding
|
|
* pre-escaping and back to this encoding post-escaping.
|
|
*
|
|
* @return array|string
|
|
*
|
|
* @throws InvalidArgumentException
|
|
*/
|
|
function esc($data, string $context = 'html', ?string $encoding = null)
|
|
{
|
|
$context = strtolower($context);
|
|
|
|
// Provide a way to NOT escape data since
|
|
// this could be called automatically by
|
|
// the View library.
|
|
if ($context === 'raw') {
|
|
return $data;
|
|
}
|
|
|
|
if (is_array($data)) {
|
|
foreach ($data as &$value) {
|
|
$value = esc($value, $context);
|
|
}
|
|
}
|
|
|
|
if (is_string($data)) {
|
|
if (! in_array($context, ['html', 'js', 'css', 'url', 'attr'], true)) {
|
|
throw new InvalidArgumentException('Invalid escape context provided.');
|
|
}
|
|
|
|
$method = $context === 'attr' ? 'escapeHtmlAttr' : 'escape' . ucfirst($context);
|
|
|
|
static $escaper;
|
|
if (! $escaper) {
|
|
$escaper = new Escaper($encoding);
|
|
}
|
|
|
|
if ($encoding !== null && $escaper->getEncoding() !== $encoding) {
|
|
$escaper = new Escaper($encoding);
|
|
}
|
|
|
|
$data = $escaper->{$method}($data);
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|
|
|
|
if (! function_exists('force_https')) {
|
|
/**
|
|
* Used to force a page to be accessed in via HTTPS.
|
|
* Uses a standard redirect, plus will set the HSTS header
|
|
* for modern browsers that support, which gives best
|
|
* protection against man-in-the-middle attacks.
|
|
*
|
|
* @see https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security
|
|
*
|
|
* @param int $duration How long should the SSL header be set for? (in seconds)
|
|
* Defaults to 1 year.
|
|
*
|
|
* @throws HTTPException
|
|
* @throws RedirectException
|
|
*/
|
|
function force_https(
|
|
int $duration = 31_536_000,
|
|
?RequestInterface $request = null,
|
|
?ResponseInterface $response = null,
|
|
): void {
|
|
$request ??= service('request');
|
|
|
|
if (! $request instanceof IncomingRequest) {
|
|
return;
|
|
}
|
|
|
|
$response ??= service('response');
|
|
|
|
if ((ENVIRONMENT !== 'testing' && (is_cli() || $request->isSecure()))
|
|
|| $request->getServer('HTTPS') === 'test'
|
|
) {
|
|
return; // @codeCoverageIgnore
|
|
}
|
|
|
|
// If the session status is active, we should regenerate
|
|
// the session ID for safety sake.
|
|
if (ENVIRONMENT !== 'testing' && session_status() === PHP_SESSION_ACTIVE) {
|
|
service('session')->regenerate(); // @codeCoverageIgnore
|
|
}
|
|
|
|
$uri = $request->getUri()->withScheme('https');
|
|
|
|
// Set an HSTS header
|
|
$response->setHeader('Strict-Transport-Security', 'max-age=' . $duration)
|
|
->redirect((string) $uri)
|
|
->setStatusCode(307)
|
|
->setBody('')
|
|
->getCookieStore()
|
|
->clear();
|
|
|
|
throw new RedirectException($response);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('function_usable')) {
|
|
/**
|
|
* Function usable
|
|
*
|
|
* Executes a function_exists() check, and if the Suhosin PHP
|
|
* extension is loaded - checks whether the function that is
|
|
* checked might be disabled in there as well.
|
|
*
|
|
* This is useful as function_exists() will return FALSE for
|
|
* functions disabled via the *disable_functions* php.ini
|
|
* setting, but not for *suhosin.executor.func.blacklist* and
|
|
* *suhosin.executor.disable_eval*. These settings will just
|
|
* terminate script execution if a disabled function is executed.
|
|
*
|
|
* The above described behavior turned out to be a bug in Suhosin,
|
|
* but even though a fix was committed for 0.9.34 on 2012-02-12,
|
|
* that version is yet to be released. This function will therefore
|
|
* be just temporary, but would probably be kept for a few years.
|
|
*
|
|
* @see http://www.hardened-php.net/suhosin/
|
|
*
|
|
* @param string $functionName Function to check for
|
|
*
|
|
* @return bool TRUE if the function exists and is safe to call,
|
|
* FALSE otherwise.
|
|
*
|
|
* @codeCoverageIgnore This is too exotic
|
|
*/
|
|
function function_usable(string $functionName): bool
|
|
{
|
|
static $_suhosin_func_blacklist;
|
|
|
|
if (function_exists($functionName)) {
|
|
if (! isset($_suhosin_func_blacklist)) {
|
|
$_suhosin_func_blacklist = extension_loaded('suhosin') ? explode(',', trim(ini_get('suhosin.executor.func.blacklist'))) : [];
|
|
}
|
|
|
|
return ! in_array($functionName, $_suhosin_func_blacklist, true);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (! function_exists('helper')) {
|
|
/**
|
|
* Loads a helper file into memory. Supports namespaced helpers,
|
|
* both in and out of the 'Helpers' directory of a namespaced directory.
|
|
*
|
|
* Will load ALL helpers of the matching name, in the following order:
|
|
* 1. app/Helpers
|
|
* 2. {namespace}/Helpers
|
|
* 3. system/Helpers
|
|
*
|
|
* @param array|string $filenames
|
|
*
|
|
* @throws FileNotFoundException
|
|
*/
|
|
function helper($filenames): void
|
|
{
|
|
static $loaded = [];
|
|
|
|
$loader = service('locator');
|
|
|
|
if (! is_array($filenames)) {
|
|
$filenames = [$filenames];
|
|
}
|
|
|
|
// Store a list of all files to include...
|
|
$includes = [];
|
|
|
|
foreach ($filenames as $filename) {
|
|
// Store our system and application helper
|
|
// versions so that we can control the load ordering.
|
|
$systemHelper = '';
|
|
$appHelper = '';
|
|
$localIncludes = [];
|
|
|
|
if (! str_contains($filename, '_helper')) {
|
|
$filename .= '_helper';
|
|
}
|
|
|
|
// Check if this helper has already been loaded
|
|
if (in_array($filename, $loaded, true)) {
|
|
continue;
|
|
}
|
|
|
|
// If the file is namespaced, we'll just grab that
|
|
// file and not search for any others
|
|
if (str_contains($filename, '\\')) {
|
|
$path = $loader->locateFile($filename, 'Helpers');
|
|
|
|
if ($path === false) {
|
|
throw FileNotFoundException::forFileNotFound($filename);
|
|
}
|
|
|
|
$includes[] = $path;
|
|
$loaded[] = $filename;
|
|
} else {
|
|
// No namespaces, so search in all available locations
|
|
$paths = $loader->search('Helpers/' . $filename);
|
|
|
|
foreach ($paths as $path) {
|
|
if (str_starts_with($path, APPPATH . 'Helpers' . DIRECTORY_SEPARATOR)) {
|
|
$appHelper = $path;
|
|
} elseif (str_starts_with($path, SYSTEMPATH . 'Helpers' . DIRECTORY_SEPARATOR)) {
|
|
$systemHelper = $path;
|
|
} else {
|
|
$localIncludes[] = $path;
|
|
$loaded[] = $filename;
|
|
}
|
|
}
|
|
|
|
// App-level helpers should override all others
|
|
if ($appHelper !== '') {
|
|
$includes[] = $appHelper;
|
|
$loaded[] = $filename;
|
|
}
|
|
|
|
// All namespaced files get added in next
|
|
$includes = [...$includes, ...$localIncludes];
|
|
|
|
// And the system default one should be added in last.
|
|
if ($systemHelper !== '') {
|
|
$includes[] = $systemHelper;
|
|
$loaded[] = $filename;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now actually include all of the files
|
|
foreach ($includes as $path) {
|
|
include_once $path;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (! function_exists('is_cli')) {
|
|
/**
|
|
* Check if PHP was invoked from the command line.
|
|
*
|
|
* @codeCoverageIgnore Cannot be tested fully as PHPUnit always run in php-cli
|
|
*/
|
|
function is_cli(): bool
|
|
{
|
|
if (in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) {
|
|
return true;
|
|
}
|
|
|
|
// PHP_SAPI could be 'cgi-fcgi', 'fpm-fcgi'.
|
|
// See https://github.com/codeigniter4/CodeIgniter4/pull/5393
|
|
return ! isset($_SERVER['REMOTE_ADDR']) && ! isset($_SERVER['REQUEST_METHOD']);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('is_really_writable')) {
|
|
/**
|
|
* Tests for file writability
|
|
*
|
|
* is_writable() returns TRUE on Windows servers when you really can't write to
|
|
* the file, based on the read-only attribute. is_writable() is also unreliable
|
|
* on Unix servers if safe_mode is on.
|
|
*
|
|
* @see https://bugs.php.net/bug.php?id=54709
|
|
*
|
|
* @throws Exception
|
|
*
|
|
* @codeCoverageIgnore Not practical to test, as travis runs on linux
|
|
*/
|
|
function is_really_writable(string $file): bool
|
|
{
|
|
// If we're on a Unix server we call is_writable
|
|
if (! is_windows()) {
|
|
return is_writable($file);
|
|
}
|
|
|
|
/* For Windows servers and safe_mode "on" installations we'll actually
|
|
* write a file then read it. Bah...
|
|
*/
|
|
if (is_dir($file)) {
|
|
$file = rtrim($file, '/') . '/' . bin2hex(random_bytes(16));
|
|
if (($fp = @fopen($file, 'ab')) === false) {
|
|
return false;
|
|
}
|
|
|
|
fclose($fp);
|
|
@chmod($file, 0777);
|
|
@unlink($file);
|
|
|
|
return true;
|
|
}
|
|
|
|
if (! is_file($file) || ($fp = @fopen($file, 'ab')) === false) {
|
|
return false;
|
|
}
|
|
|
|
fclose($fp);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (! function_exists('is_windows')) {
|
|
/**
|
|
* Detect if platform is running in Windows.
|
|
*/
|
|
function is_windows(?bool $mock = null): bool
|
|
{
|
|
static $mocked;
|
|
|
|
if (func_num_args() === 1) {
|
|
$mocked = $mock;
|
|
}
|
|
|
|
return $mocked ?? DIRECTORY_SEPARATOR === '\\';
|
|
}
|
|
}
|
|
|
|
if (! function_exists('lang')) {
|
|
/**
|
|
* A convenience method to translate a string or array of them and format
|
|
* the result with the intl extension's MessageFormatter.
|
|
*
|
|
* @return list<string>|string
|
|
*/
|
|
function lang(string $line, array $args = [], ?string $locale = null)
|
|
{
|
|
/** @var Language $language */
|
|
$language = service('language');
|
|
|
|
// Get active locale
|
|
$activeLocale = $language->getLocale();
|
|
|
|
if ((string) $locale !== '' && $locale !== $activeLocale) {
|
|
$language->setLocale($locale);
|
|
}
|
|
|
|
$lines = $language->getLine($line, $args);
|
|
|
|
if ((string) $locale !== '' && $locale !== $activeLocale) {
|
|
// Reset to active locale
|
|
$language->setLocale($activeLocale);
|
|
}
|
|
|
|
return $lines;
|
|
}
|
|
}
|
|
|
|
if (! function_exists('log_message')) {
|
|
/**
|
|
* A convenience/compatibility method for logging events through
|
|
* the Log system.
|
|
*
|
|
* Allowed log levels are:
|
|
* - emergency
|
|
* - alert
|
|
* - critical
|
|
* - error
|
|
* - warning
|
|
* - notice
|
|
* - info
|
|
* - debug
|
|
*/
|
|
function log_message(string $level, string $message, array $context = []): void
|
|
{
|
|
// When running tests, we want to always ensure that the
|
|
// TestLogger is running, which provides utilities for
|
|
// for asserting that logs were called in the test code.
|
|
if (ENVIRONMENT === 'testing') {
|
|
$logger = new TestLogger(new Logger());
|
|
|
|
$logger->log($level, $message, $context);
|
|
|
|
return;
|
|
}
|
|
|
|
service('logger')->log($level, $message, $context); // @codeCoverageIgnore
|
|
}
|
|
}
|
|
|
|
if (! function_exists('model')) {
|
|
/**
|
|
* More simple way of getting model instances from Factories
|
|
*
|
|
* @template ModelTemplate of Model
|
|
*
|
|
* @param class-string<ModelTemplate>|string $name
|
|
*
|
|
* @return ModelTemplate|null
|
|
* @phpstan-return ($name is class-string<ModelTemplate> ? ModelTemplate : object|null)
|
|
*/
|
|
function model(string $name, bool $getShared = true, ?ConnectionInterface &$conn = null)
|
|
{
|
|
return Factories::models($name, ['getShared' => $getShared], $conn);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('old')) {
|
|
/**
|
|
* Provides access to "old input" that was set in the session
|
|
* during a redirect()->withInput().
|
|
*
|
|
* @param string|null $default
|
|
* @param false|string $escape
|
|
* @phpstan-param false|'attr'|'css'|'html'|'js'|'raw'|'url' $escape
|
|
*
|
|
* @return array|string|null
|
|
*/
|
|
function old(string $key, $default = null, $escape = 'html')
|
|
{
|
|
// Ensure the session is loaded
|
|
if (session_status() === PHP_SESSION_NONE && ENVIRONMENT !== 'testing') {
|
|
session(); // @codeCoverageIgnore
|
|
}
|
|
|
|
$request = service('request');
|
|
|
|
$value = $request->getOldInput($key);
|
|
|
|
// Return the default value if nothing
|
|
// found in the old input.
|
|
if ($value === null) {
|
|
return $default;
|
|
}
|
|
|
|
return $escape === false ? $value : esc($value, $escape);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('redirect')) {
|
|
/**
|
|
* Convenience method that works with the current global $request and
|
|
* $router instances to redirect using named/reverse-routed routes
|
|
* to determine the URL to go to.
|
|
*
|
|
* If more control is needed, you must use $response->redirect explicitly.
|
|
*
|
|
* @param non-empty-string|null $route Route name or Controller::method
|
|
*/
|
|
function redirect(?string $route = null): RedirectResponse
|
|
{
|
|
$response = service('redirectresponse');
|
|
|
|
if ((string) $route !== '') {
|
|
return $response->route($route);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
}
|
|
|
|
if (! function_exists('_solidus')) {
|
|
/**
|
|
* Generates the solidus character (`/`) depending on the HTML5 compatibility flag in `Config\DocTypes`
|
|
*
|
|
* @param DocTypes|null $docTypesConfig New config. For testing purpose only.
|
|
*
|
|
* @internal
|
|
*/
|
|
function _solidus(?DocTypes $docTypesConfig = null): string
|
|
{
|
|
static $docTypes = null;
|
|
|
|
if ($docTypesConfig instanceof DocTypes) {
|
|
$docTypes = $docTypesConfig;
|
|
}
|
|
|
|
$docTypes ??= new DocTypes();
|
|
|
|
if ($docTypes->html5 ?? false) {
|
|
return '';
|
|
}
|
|
|
|
return ' /';
|
|
}
|
|
}
|
|
|
|
if (! function_exists('remove_invisible_characters')) {
|
|
/**
|
|
* Remove Invisible Characters
|
|
*
|
|
* This prevents sandwiching null characters
|
|
* between ascii characters, like Java\0script.
|
|
*/
|
|
function remove_invisible_characters(string $str, bool $urlEncoded = true): string
|
|
{
|
|
$nonDisplayables = [];
|
|
|
|
// every control character except newline (dec 10),
|
|
// carriage return (dec 13) and horizontal tab (dec 09)
|
|
if ($urlEncoded) {
|
|
$nonDisplayables[] = '/%0[0-8bcef]/'; // url encoded 00-08, 11, 12, 14, 15
|
|
$nonDisplayables[] = '/%1[0-9a-f]/'; // url encoded 16-31
|
|
}
|
|
|
|
$nonDisplayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127
|
|
|
|
do {
|
|
$str = preg_replace($nonDisplayables, '', $str, -1, $count);
|
|
} while ($count);
|
|
|
|
return $str;
|
|
}
|
|
}
|
|
|
|
if (! function_exists('request')) {
|
|
/**
|
|
* Returns the shared Request.
|
|
*
|
|
* @return CLIRequest|IncomingRequest
|
|
*/
|
|
function request()
|
|
{
|
|
return service('request');
|
|
}
|
|
}
|
|
|
|
if (! function_exists('response')) {
|
|
/**
|
|
* Returns the shared Response.
|
|
*/
|
|
function response(): ResponseInterface
|
|
{
|
|
return service('response');
|
|
}
|
|
}
|
|
|
|
if (! function_exists('route_to')) {
|
|
/**
|
|
* Given a route name or controller/method string and any params,
|
|
* will attempt to build the relative URL to the
|
|
* matching route.
|
|
*
|
|
* NOTE: This requires the controller/method to
|
|
* have a route defined in the routes Config file.
|
|
*
|
|
* @param string $method Route name or Controller::method
|
|
* @param int|string ...$params One or more parameters to be passed to the route.
|
|
* The last parameter allows you to set the locale.
|
|
*
|
|
* @return false|string The route (URI path relative to baseURL) or false if not found.
|
|
*/
|
|
function route_to(string $method, ...$params)
|
|
{
|
|
return service('routes')->reverseRoute($method, ...$params);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('session')) {
|
|
/**
|
|
* A convenience method for accessing the session instance,
|
|
* or an item that has been set in the session.
|
|
*
|
|
* Examples:
|
|
* session()->set('foo', 'bar');
|
|
* $foo = session('bar');
|
|
*
|
|
* @return array|bool|float|int|object|Session|string|null
|
|
* @phpstan-return ($val is null ? Session : array|bool|float|int|object|string|null)
|
|
*/
|
|
function session(?string $val = null)
|
|
{
|
|
$session = service('session');
|
|
|
|
// Returning a single item?
|
|
if (is_string($val)) {
|
|
return $session->get($val);
|
|
}
|
|
|
|
return $session;
|
|
}
|
|
}
|
|
|
|
if (! function_exists('service')) {
|
|
/**
|
|
* Allows cleaner access to the Services Config file.
|
|
* Always returns a SHARED instance of the class, so
|
|
* calling the function multiple times should always
|
|
* return the same instance.
|
|
*
|
|
* These are equal:
|
|
* - $timer = service('timer')
|
|
* - $timer = \CodeIgniter\Config\Services::timer();
|
|
*
|
|
* @param array|bool|float|int|object|string|null ...$params
|
|
*/
|
|
function service(string $name, ...$params): ?object
|
|
{
|
|
if ($params === []) {
|
|
return Services::get($name);
|
|
}
|
|
|
|
return Services::$name(...$params);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('single_service')) {
|
|
/**
|
|
* Always returns a new instance of the class.
|
|
*
|
|
* @param array|bool|float|int|object|string|null ...$params
|
|
*/
|
|
function single_service(string $name, ...$params): ?object
|
|
{
|
|
$service = Services::serviceExists($name);
|
|
|
|
if ($service === null) {
|
|
// The service is not defined anywhere so just return.
|
|
return null;
|
|
}
|
|
|
|
$method = new ReflectionMethod($service, $name);
|
|
$count = $method->getNumberOfParameters();
|
|
$mParam = $method->getParameters();
|
|
|
|
if ($count === 1) {
|
|
// This service needs only one argument, which is the shared
|
|
// instance flag, so let's wrap up and pass false here.
|
|
return $service::$name(false);
|
|
}
|
|
|
|
// Fill in the params with the defaults, but stop before the last
|
|
for ($startIndex = count($params); $startIndex <= $count - 2; $startIndex++) {
|
|
$params[$startIndex] = $mParam[$startIndex]->getDefaultValue();
|
|
}
|
|
|
|
// Ensure the last argument will not create a shared instance
|
|
$params[$count - 1] = false;
|
|
|
|
return $service::$name(...$params);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('slash_item')) {
|
|
// Unlike CI3, this function is placed here because
|
|
// it's not a config, or part of a config.
|
|
/**
|
|
* Fetch a config file item with slash appended (if not empty)
|
|
*
|
|
* @param string $item Config item name
|
|
*
|
|
* @return string|null The configuration item or NULL if
|
|
* the item doesn't exist
|
|
*/
|
|
function slash_item(string $item): ?string
|
|
{
|
|
$config = config(App::class);
|
|
|
|
if (! property_exists($config, $item)) {
|
|
return null;
|
|
}
|
|
|
|
$configItem = $config->{$item};
|
|
|
|
if (! is_scalar($configItem)) {
|
|
throw new RuntimeException(sprintf(
|
|
'Cannot convert "%s::$%s" of type "%s" to type "string".',
|
|
App::class,
|
|
$item,
|
|
gettype($configItem),
|
|
));
|
|
}
|
|
|
|
$configItem = trim((string) $configItem);
|
|
|
|
if ($configItem === '') {
|
|
return $configItem;
|
|
}
|
|
|
|
return rtrim($configItem, '/') . '/';
|
|
}
|
|
}
|
|
|
|
if (! function_exists('stringify_attributes')) {
|
|
/**
|
|
* Stringify attributes for use in HTML tags.
|
|
*
|
|
* Helper function used to convert a string, array, or object
|
|
* of attributes to a string.
|
|
*
|
|
* @param array|object|string $attributes string, array, object that can be cast to array
|
|
*/
|
|
function stringify_attributes($attributes, bool $js = false): string
|
|
{
|
|
$atts = '';
|
|
|
|
if ($attributes === '' || $attributes === [] || $attributes === null) {
|
|
return $atts;
|
|
}
|
|
|
|
if (is_string($attributes)) {
|
|
return ' ' . $attributes;
|
|
}
|
|
|
|
$attributes = (array) $attributes;
|
|
|
|
foreach ($attributes as $key => $val) {
|
|
$atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . esc($val) . '"';
|
|
}
|
|
|
|
return rtrim($atts, ',');
|
|
}
|
|
}
|
|
|
|
if (! function_exists('timer')) {
|
|
/**
|
|
* A convenience method for working with the timer.
|
|
* If no parameter is passed, it will return the timer instance.
|
|
* If callable is passed, it measures time of callable and
|
|
* returns its return value if any.
|
|
* Otherwise will start or stop the timer intelligently.
|
|
*
|
|
* @param non-empty-string|null $name
|
|
* @param (callable(): mixed)|null $callable
|
|
*
|
|
* @return mixed|Timer
|
|
* @phpstan-return ($name is null ? Timer : ($callable is (callable(): mixed) ? mixed : Timer))
|
|
*/
|
|
function timer(?string $name = null, ?callable $callable = null)
|
|
{
|
|
$timer = service('timer');
|
|
|
|
if ($name === null) {
|
|
return $timer;
|
|
}
|
|
|
|
if ($callable !== null) {
|
|
return $timer->record($name, $callable);
|
|
}
|
|
|
|
if ($timer->has($name)) {
|
|
return $timer->stop($name);
|
|
}
|
|
|
|
return $timer->start($name);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('view')) {
|
|
/**
|
|
* Grabs the current RendererInterface-compatible class
|
|
* and tells it to render the specified view. Simply provides
|
|
* a convenience method that can be used in Controllers,
|
|
* libraries, and routed closures.
|
|
*
|
|
* NOTE: Does not provide any escaping of the data, so that must
|
|
* all be handled manually by the developer.
|
|
*
|
|
* @param array $options Options for saveData or third-party extensions.
|
|
*/
|
|
function view(string $name, array $data = [], array $options = []): string
|
|
{
|
|
$renderer = service('renderer');
|
|
|
|
$config = config(View::class);
|
|
$saveData = $config->saveData;
|
|
|
|
if (array_key_exists('saveData', $options)) {
|
|
$saveData = (bool) $options['saveData'];
|
|
unset($options['saveData']);
|
|
}
|
|
|
|
return $renderer->setData($data, 'raw')->render($name, $options, $saveData);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('view_cell')) {
|
|
/**
|
|
* View cells are used within views to insert HTML chunks that are managed
|
|
* by other classes.
|
|
*
|
|
* @param array|string|null $params
|
|
*
|
|
* @throws ReflectionException
|
|
*/
|
|
function view_cell(string $library, $params = null, int $ttl = 0, ?string $cacheName = null): string
|
|
{
|
|
return service('viewcell')
|
|
->render($library, $params, $ttl, $cacheName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* These helpers come from Laravel so will not be
|
|
* re-tested and can be ignored safely.
|
|
*
|
|
* @see https://github.com/laravel/framework/blob/8.x/src/Illuminate/Support/helpers.php
|
|
*/
|
|
if (! function_exists('class_basename')) {
|
|
/**
|
|
* Get the class "basename" of the given object / class.
|
|
*
|
|
* @param object|string $class
|
|
*
|
|
* @return string
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
function class_basename($class)
|
|
{
|
|
$class = is_object($class) ? $class::class : $class;
|
|
|
|
return basename(str_replace('\\', '/', $class));
|
|
}
|
|
}
|
|
|
|
if (! function_exists('class_uses_recursive')) {
|
|
/**
|
|
* Returns all traits used by a class, its parent classes and trait of their traits.
|
|
*
|
|
* @param object|string $class
|
|
*
|
|
* @return array
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
function class_uses_recursive($class)
|
|
{
|
|
if (is_object($class)) {
|
|
$class = $class::class;
|
|
}
|
|
|
|
$results = [];
|
|
|
|
foreach (array_reverse(class_parents($class)) + [$class => $class] as $class) {
|
|
$results += trait_uses_recursive($class);
|
|
}
|
|
|
|
return array_unique($results);
|
|
}
|
|
}
|
|
|
|
if (! function_exists('trait_uses_recursive')) {
|
|
/**
|
|
* Returns all traits used by a trait and its traits.
|
|
*
|
|
* @param string $trait
|
|
*
|
|
* @return array
|
|
*
|
|
* @codeCoverageIgnore
|
|
*/
|
|
function trait_uses_recursive($trait)
|
|
{
|
|
$traits = class_uses($trait) ?: [];
|
|
|
|
foreach ($traits as $trait) {
|
|
$traits += trait_uses_recursive($trait);
|
|
}
|
|
|
|
return $traits;
|
|
}
|
|
}
|