add trashes

This commit is contained in:
Diskette Guy
2025-07-21 15:40:51 +07:00
parent c8f2944770
commit 07171f5b5a
848 changed files with 134166 additions and 0 deletions
@@ -0,0 +1,318 @@
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Helpers\Array;
use CodeIgniter\Exceptions\InvalidArgumentException;
/**
* @interal This is internal implementation for the framework.
*
* If there are any methods that should be provided, make them
* public APIs via helper functions.
*
* @see \CodeIgniter\Helpers\Array\ArrayHelperDotKeyExistsTest
* @see \CodeIgniter\Helpers\Array\ArrayHelperRecursiveDiffTest
* @see \CodeIgniter\Helpers\Array\ArrayHelperSortValuesByNaturalTest
*/
final class ArrayHelper
{
/**
* Searches an array through dot syntax. Supports wildcard searches,
* like `foo.*.bar`.
*
* @used-by dot_array_search()
*
* @param string $index The index as dot array syntax.
*
* @return array|bool|int|object|string|null
*/
public static function dotSearch(string $index, array $array)
{
return self::arraySearchDot(self::convertToArray($index), $array);
}
/**
* @param string $index The index as dot array syntax.
*
* @return list<string> The index as an array.
*/
private static function convertToArray(string $index): array
{
// See https://regex101.com/r/44Ipql/1
$segments = preg_split(
'/(?<!\\\\)\./',
rtrim($index, '* '),
0,
PREG_SPLIT_NO_EMPTY,
);
return array_map(
static fn ($key): string => str_replace('\.', '.', $key),
$segments,
);
}
/**
* Recursively search the array with wildcards.
*
* @used-by dotSearch()
*
* @return array|bool|float|int|object|string|null
*/
private static function arraySearchDot(array $indexes, array $array)
{
// If index is empty, returns null.
if ($indexes === []) {
return null;
}
// Grab the current index
$currentIndex = array_shift($indexes);
if (! isset($array[$currentIndex]) && $currentIndex !== '*') {
return null;
}
// Handle Wildcard (*)
if ($currentIndex === '*') {
$answer = [];
foreach ($array as $value) {
if (! is_array($value)) {
return null;
}
$answer[] = self::arraySearchDot($indexes, $value);
}
$answer = array_filter($answer, static fn ($value): bool => $value !== null);
if ($answer !== []) {
// If array only has one element, we return that element for BC.
return count($answer) === 1 ? current($answer) : $answer;
}
return null;
}
// If this is the last index, make sure to return it now,
// and not try to recurse through things.
if ($indexes === []) {
return $array[$currentIndex];
}
// Do we need to recursively search this value?
if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) {
return self::arraySearchDot($indexes, $array[$currentIndex]);
}
// Otherwise, not found.
return null;
}
/**
* array_key_exists() with dot array syntax.
*
* If wildcard `*` is used, all items for the key after it must have the key.
*/
public static function dotKeyExists(string $index, array $array): bool
{
if (str_ends_with($index, '*') || str_contains($index, '*.*')) {
throw new InvalidArgumentException(
'You must set key right after "*". Invalid index: "' . $index . '"',
);
}
$indexes = self::convertToArray($index);
// If indexes is empty, returns false.
if ($indexes === []) {
return false;
}
$currentArray = $array;
// Grab the current index
while ($currentIndex = array_shift($indexes)) {
if ($currentIndex === '*') {
$currentIndex = array_shift($indexes);
foreach ($currentArray as $item) {
if (! array_key_exists($currentIndex, $item)) {
return false;
}
}
// If indexes is empty, all elements are checked.
if ($indexes === []) {
return true;
}
$currentArray = self::dotSearch('*.' . $currentIndex, $currentArray);
continue;
}
if (! array_key_exists($currentIndex, $currentArray)) {
return false;
}
$currentArray = $currentArray[$currentIndex];
}
return true;
}
/**
* Groups all rows by their index values. Result's depth equals number of indexes
*
* @used-by array_group_by()
*
* @param array $array Data array (i.e. from query result)
* @param array $indexes Indexes to group by. Dot syntax used. Returns $array if empty
* @param bool $includeEmpty If true, null and '' are also added as valid keys to group
*
* @return array Result array where rows are grouped together by indexes values.
*/
public static function groupBy(array $array, array $indexes, bool $includeEmpty = false): array
{
if ($indexes === []) {
return $array;
}
$result = [];
foreach ($array as $row) {
$result = self::arrayAttachIndexedValue($result, $row, $indexes, $includeEmpty);
}
return $result;
}
/**
* Recursively attach $row to the $indexes path of values found by
* `dot_array_search()`.
*
* @used-by groupBy()
*/
private static function arrayAttachIndexedValue(
array $result,
array $row,
array $indexes,
bool $includeEmpty,
): array {
if (($index = array_shift($indexes)) === null) {
$result[] = $row;
return $result;
}
$value = dot_array_search($index, $row);
if (! is_scalar($value)) {
$value = '';
}
if (is_bool($value)) {
$value = (int) $value;
}
if (! $includeEmpty && $value === '') {
return $result;
}
if (! array_key_exists($value, $result)) {
$result[$value] = [];
}
$result[$value] = self::arrayAttachIndexedValue($result[$value], $row, $indexes, $includeEmpty);
return $result;
}
/**
* Compare recursively two associative arrays and return difference as new array.
* Returns keys that exist in `$original` but not in `$compareWith`.
*/
public static function recursiveDiff(array $original, array $compareWith): array
{
$difference = [];
if ($original === []) {
return [];
}
if ($compareWith === []) {
return $original;
}
foreach ($original as $originalKey => $originalValue) {
if ($originalValue === []) {
continue;
}
if (is_array($originalValue)) {
$diffArrays = [];
if (isset($compareWith[$originalKey]) && is_array($compareWith[$originalKey])) {
$diffArrays = self::recursiveDiff($originalValue, $compareWith[$originalKey]);
} else {
$difference[$originalKey] = $originalValue;
}
if ($diffArrays !== []) {
$difference[$originalKey] = $diffArrays;
}
} elseif (is_string($originalValue) && ! array_key_exists($originalKey, $compareWith)) {
$difference[$originalKey] = $originalValue;
}
}
return $difference;
}
/**
* Recursively count all keys.
*/
public static function recursiveCount(array $array, int $counter = 0): int
{
foreach ($array as $value) {
if (is_array($value)) {
$counter = self::recursiveCount($value, $counter);
}
$counter++;
}
return $counter;
}
/**
* Sorts array values in natural order
* If the value is an array, you need to specify the $sortByIndex of the key to sort
*
* @param list<int|list<int|string>|string> $array
* @param int|string|null $sortByIndex
*/
public static function sortValuesByNatural(array &$array, $sortByIndex = null): bool
{
return usort($array, static function ($currentValue, $nextValue) use ($sortByIndex): int {
if ($sortByIndex !== null) {
return strnatcmp((string) $currentValue[$sortByIndex], (string) $nextValue[$sortByIndex]);
}
return strnatcmp((string) $currentValue, (string) $nextValue);
});
}
}
@@ -0,0 +1,165 @@
<?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\Helpers\Array\ArrayHelper;
// CodeIgniter Array Helpers
if (! function_exists('dot_array_search')) {
/**
* Searches an array through dot syntax. Supports
* wildcard searches, like foo.*.bar
*
* @return array|bool|int|object|string|null
*/
function dot_array_search(string $index, array $array)
{
return ArrayHelper::dotSearch($index, $array);
}
}
if (! function_exists('array_deep_search')) {
/**
* Returns the value of an element at a key in an array of uncertain depth.
*
* @param int|string $key
*
* @return array|bool|float|int|object|string|null
*/
function array_deep_search($key, array $array)
{
if (isset($array[$key])) {
return $array[$key];
}
foreach ($array as $value) {
if (is_array($value) && ($result = array_deep_search($key, $value))) {
return $result;
}
}
return null;
}
}
if (! function_exists('array_sort_by_multiple_keys')) {
/**
* Sorts a multidimensional array by its elements values. The array
* columns to be used for sorting are passed as an associative
* array of key names and sorting flags.
*
* Both arrays of objects and arrays of array can be sorted.
*
* Example:
* array_sort_by_multiple_keys($players, [
* 'team.hierarchy' => SORT_ASC,
* 'position' => SORT_ASC,
* 'name' => SORT_STRING,
* ]);
*
* The '.' dot operator in the column name indicates a deeper array or
* object level. In principle, any number of sublevels could be used,
* as long as the level and column exist in every array element.
*
* For information on multi-level array sorting, refer to Example #3 here:
* https://www.php.net/manual/de/function.array-multisort.php
*
* @param array $array the reference of the array to be sorted
* @param array $sortColumns an associative array of columns to sort
* after and their sorting flags
*/
function array_sort_by_multiple_keys(array &$array, array $sortColumns): bool
{
// Check if there really are columns to sort after
if ($sortColumns === [] || $array === []) {
return false;
}
// Group sorting indexes and data
$tempArray = [];
foreach ($sortColumns as $key => $sortFlag) {
// Get sorting values
$carry = $array;
// The '.' operator separates nested elements
foreach (explode('.', $key) as $keySegment) {
// Loop elements if they are objects
if (is_object(reset($carry))) {
// Extract the object attribute
foreach ($carry as $index => $object) {
$carry[$index] = $object->{$keySegment};
}
continue;
}
// Extract the target column if elements are arrays
$carry = array_column($carry, $keySegment);
}
// Store the collected sorting parameters
$tempArray[] = $carry;
$tempArray[] = $sortFlag;
}
// Append the array as reference
$tempArray[] = &$array;
// Pass sorting arrays and flags as an argument list.
return array_multisort(...$tempArray);
}
}
if (! function_exists('array_flatten_with_dots')) {
/**
* Flatten a multidimensional array using dots as separators.
*
* @param iterable $array The multi-dimensional array
* @param string $id Something to initially prepend to the flattened keys
*
* @return array The flattened array
*/
function array_flatten_with_dots(iterable $array, string $id = ''): array
{
$flattened = [];
foreach ($array as $key => $value) {
$newKey = $id . $key;
if (is_array($value) && $value !== []) {
$flattened = array_merge($flattened, array_flatten_with_dots($value, $newKey . '.'));
} else {
$flattened[$newKey] = $value;
}
}
return $flattened;
}
}
if (! function_exists('array_group_by')) {
/**
* Groups all rows by their index values. Result's depth equals number of indexes
*
* @param array $array Data array (i.e. from query result)
* @param array $indexes Indexes to group by. Dot syntax used. Returns $array if empty
* @param bool $includeEmpty If true, null and '' are also added as valid keys to group
*
* @return array Result array where rows are grouped together by indexes values.
*/
function array_group_by(array $array, array $indexes, bool $includeEmpty = false): array
{
return ArrayHelper::groupBy($array, $indexes, $includeEmpty);
}
}
@@ -0,0 +1,109 @@
<?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\Cookie\Cookie;
use Config\Cookie as CookieConfig;
// =============================================================================
// CodeIgniter Cookie Helpers
// =============================================================================
if (! function_exists('set_cookie')) {
/**
* Set cookie
*
* Accepts seven parameters, or you can submit an associative
* array in the first parameter containing all the values.
*
* @param array|Cookie|string $name Cookie name / array containing binds / Cookie object
* @param string $value The value of the cookie
* @param int $expire The number of seconds until expiration
* @param string $domain For site-wide cookie. Usually: .yourdomain.com
* @param string $path The cookie path
* @param string $prefix The cookie prefix ('': the default prefix)
* @param bool|null $secure True makes the cookie secure
* @param bool|null $httpOnly True makes the cookie accessible via http(s) only (no javascript)
* @param string|null $sameSite The cookie SameSite value
*
* @see \CodeIgniter\HTTP\Response::setCookie()
*/
function set_cookie(
$name,
string $value = '',
int $expire = 0,
string $domain = '',
string $path = '/',
string $prefix = '',
?bool $secure = null,
?bool $httpOnly = null,
?string $sameSite = null,
): void {
$response = service('response');
$response->setCookie($name, $value, $expire, $domain, $path, $prefix, $secure, $httpOnly, $sameSite);
}
}
if (! function_exists('get_cookie')) {
/**
* Fetch an item from the $_COOKIE array
*
* @param string $index
* @param string|null $prefix Cookie name prefix.
* '': the prefix in Config\Cookie
* null: no prefix
*
* @return array|string|null
*
* @see \CodeIgniter\HTTP\IncomingRequest::getCookie()
*/
function get_cookie($index, bool $xssClean = false, ?string $prefix = '')
{
if ($prefix === '') {
$cookie = config(CookieConfig::class);
$prefix = $cookie->prefix;
}
$request = service('request');
$filter = $xssClean ? FILTER_SANITIZE_FULL_SPECIAL_CHARS : FILTER_DEFAULT;
return $request->getCookie($prefix . $index, $filter);
}
}
if (! function_exists('delete_cookie')) {
/**
* Delete a cookie
*
* @param string $name
* @param string $domain the cookie domain. Usually: .yourdomain.com
* @param string $path the cookie path
* @param string $prefix the cookie prefix
*
* @see \CodeIgniter\HTTP\Response::deleteCookie()
*/
function delete_cookie($name, string $domain = '', string $path = '/', string $prefix = ''): void
{
service('response')->deleteCookie($name, $domain, $path, $prefix);
}
}
if (! function_exists('has_cookie')) {
/**
* Checks if a cookie exists by name.
*/
function has_cookie(string $name, ?string $value = null, string $prefix = ''): bool
{
return service('response')->hasCookie($name, $value, $prefix);
}
}
@@ -0,0 +1,79 @@
<?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\I18n\Time;
// CodeIgniter Date Helpers
if (! function_exists('now')) {
/**
* Get "now" time
*
* Returns Time::now()->getTimestamp() based on the timezone parameter or on the
* app_timezone() setting
*
* @param non-empty-string|null $timezone
*
* @throws Exception
*/
function now(?string $timezone = null): int
{
$timezone = ($timezone === null || $timezone === '') ? app_timezone() : $timezone;
if ($timezone === 'local' || $timezone === date_default_timezone_get()) {
return Time::now()->getTimestamp();
}
$time = Time::now($timezone);
sscanf(
$time->format('j-n-Y G:i:s'),
'%d-%d-%d %d:%d:%d',
$day,
$month,
$year,
$hour,
$minute,
$second,
);
return mktime($hour, $minute, $second, $month, $day, $year);
}
}
if (! function_exists('timezone_select')) {
/**
* Generates a select field of all available timezones
*
* Returns a string with the formatted HTML
*
* @param string $class Optional class to apply to the select field
* @param string $default Default value for initial selection
* @param int $what One of the DateTimeZone class constants (for listIdentifiers)
* @param string $country A two-letter ISO 3166-1 compatible country code (for listIdentifiers)
*
* @throws Exception
*/
function timezone_select(string $class = '', string $default = '', int $what = DateTimeZone::ALL, ?string $country = null): string
{
$timezones = DateTimeZone::listIdentifiers($what, $country);
$buffer = "<select name='timezone' class='{$class}'>\n";
foreach ($timezones as $timezone) {
$selected = ($timezone === $default) ? 'selected' : '';
$buffer .= "<option value='{$timezone}' {$selected}>{$timezone}</option>\n";
}
return $buffer . ("</select>\n");
}
}
@@ -0,0 +1,456 @@
<?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\Exceptions\InvalidArgumentException;
// CodeIgniter File System Helpers
if (! function_exists('directory_map')) {
/**
* Create a Directory Map
*
* Reads the specified directory and builds an array
* representation of it. Sub-folders contained with the
* directory will be mapped as well.
*
* @param string $sourceDir Path to source
* @param int $directoryDepth Depth of directories to traverse
* (0 = fully recursive, 1 = current dir, etc)
* @param bool $hidden Whether to show hidden files
*/
function directory_map(string $sourceDir, int $directoryDepth = 0, bool $hidden = false): array
{
try {
$fp = opendir($sourceDir);
$fileData = [];
$newDepth = $directoryDepth - 1;
$sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
while (false !== ($file = readdir($fp))) {
// Remove '.', '..', and hidden files [optional]
if ($file === '.' || $file === '..' || ($hidden === false && $file[0] === '.')) {
continue;
}
if (is_dir($sourceDir . $file)) {
$file .= DIRECTORY_SEPARATOR;
}
if (($directoryDepth < 1 || $newDepth > 0) && is_dir($sourceDir . $file)) {
$fileData[$file] = directory_map($sourceDir . $file, $newDepth, $hidden);
} else {
$fileData[] = $file;
}
}
closedir($fp);
return $fileData;
} catch (Throwable) {
return [];
}
}
}
if (! function_exists('directory_mirror')) {
/**
* Recursively copies the files and directories of the origin directory
* into the target directory, i.e. "mirror" its contents.
*
* @param bool $overwrite Whether individual files overwrite on collision
*
* @throws InvalidArgumentException
*/
function directory_mirror(string $originDir, string $targetDir, bool $overwrite = true): void
{
if (! is_dir($originDir = rtrim($originDir, '\\/'))) {
throw new InvalidArgumentException(sprintf('The origin directory "%s" was not found.', $originDir));
}
if (! is_dir($targetDir = rtrim($targetDir, '\\/'))) {
@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()) {
if (! is_dir($target)) {
mkdir($target, 0755);
}
} elseif ($overwrite || ! is_file($target)) {
copy($origin, $target);
}
}
}
}
if (! function_exists('write_file')) {
/**
* Write File
*
* Writes data to the file specified in the path.
* Creates a new file if non-existent.
*
* @param string $path File path
* @param string $data Data to write
* @param string $mode fopen() mode (default: 'wb')
*/
function write_file(string $path, string $data, string $mode = 'wb'): bool
{
try {
$fp = fopen($path, $mode);
flock($fp, LOCK_EX);
$result = 0;
for ($written = 0, $length = strlen($data); $written < $length; $written += $result) {
if (($result = fwrite($fp, substr($data, $written))) === false) {
break;
}
}
flock($fp, LOCK_UN);
fclose($fp);
return is_int($result);
} catch (Throwable) {
return false;
}
}
}
if (! function_exists('delete_files')) {
/**
* 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 bool $delDir Whether to delete any directories found in the path
* @param bool $htdocs Whether to skip deleting .htaccess and index page files
* @param bool $hidden Whether to include hidden files (files beginning with a period)
*/
function delete_files(string $path, bool $delDir = false, bool $htdocs = false, bool $hidden = false): bool
{
$path = realpath($path) ?: $path;
$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
try {
foreach (new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST,
) as $object) {
$filename = $object->getFilename();
if (! $hidden && $filename[0] === '.') {
continue;
}
if (! $htdocs || preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename) !== 1) {
$isDir = $object->isDir();
if ($isDir && $delDir) {
rmdir($object->getPathname());
continue;
}
if (! $isDir) {
unlink($object->getPathname());
}
}
}
return true;
} catch (Throwable) {
return false;
}
}
}
if (! function_exists('get_filenames')) {
/**
* Get Filenames
*
* Reads the specified directory and builds an array containing the filenames.
* Any sub-folders contained within the specified path are read as well.
*
* @param string $sourceDir Path to source
* @param bool|null $includePath Whether to include the path as part of the filename; false for no path, null for a relative path, true for full path
* @param bool $hidden Whether to include hidden files (files beginning with a period)
* @param bool $includeDir Whether to include directories
*/
function get_filenames(
string $sourceDir,
?bool $includePath = false,
bool $hidden = false,
bool $includeDir = true,
): array {
$files = [];
$sourceDir = realpath($sourceDir) ?: $sourceDir;
$sourceDir = rtrim($sourceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
try {
foreach (new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourceDir, RecursiveDirectoryIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS),
RecursiveIteratorIterator::SELF_FIRST,
) as $name => $object) {
$basename = pathinfo($name, PATHINFO_BASENAME);
if (! $hidden && $basename[0] === '.') {
continue;
}
if ($includeDir || ! $object->isDir()) {
if ($includePath === false) {
$files[] = $basename;
} elseif ($includePath === null) {
$files[] = str_replace($sourceDir, '', $name);
} else {
$files[] = $name;
}
}
}
} catch (Throwable) {
return [];
}
sort($files);
return $files;
}
}
if (! function_exists('get_dir_file_info')) {
/**
* 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 bool $topLevelOnly Look only at the top level directory specified?
* @param bool $recursion Internal variable to determine recursion status - do not use in calls
*/
function get_dir_file_info(string $sourceDir, bool $topLevelOnly = true, bool $recursion = false): array
{
static $fileData = [];
$relativePath = $sourceDir;
try {
$fp = opendir($sourceDir);
// reset the array and make sure $sourceDir has a trailing slash on the initial call
if ($recursion === false) {
$fileData = [];
$sourceDir = rtrim(realpath($sourceDir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
}
// Used to be foreach (scandir($sourceDir, 1) as $file), but scandir() is simply not as fast
while (false !== ($file = readdir($fp))) {
if (is_dir($sourceDir . $file) && $file[0] !== '.' && $topLevelOnly === false) {
get_dir_file_info($sourceDir . $file . DIRECTORY_SEPARATOR, $topLevelOnly, true);
} elseif ($file[0] !== '.') {
$fileData[$file] = get_file_info($sourceDir . $file);
$fileData[$file]['relative_path'] = $relativePath;
}
}
closedir($fp);
return $fileData;
} catch (Throwable) {
return [];
}
}
}
if (! function_exists('get_file_info')) {
/**
* 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 array|string $returnedValues Array or comma separated string of information returned
*
* @return array|null
*/
function get_file_info(string $file, $returnedValues = ['name', 'server_path', 'size', 'date'])
{
if (! is_file($file)) {
return null;
}
$fileInfo = [];
if (is_string($returnedValues)) {
$returnedValues = explode(',', $returnedValues);
}
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_really_writable($file);
break;
case 'executable':
$fileInfo['executable'] = is_executable($file);
break;
case 'fileperms':
$fileInfo['fileperms'] = fileperms($file);
break;
}
}
return $fileInfo;
}
}
if (! function_exists('symbolic_permissions')) {
/**
* Symbolic Permissions
*
* Takes a numeric value representing a file's permissions and returns
* standard symbolic notation representing that value
*
* @param int $perms Permissions
*/
function symbolic_permissions(int $perms): string
{
if (($perms & 0xC000) === 0xC000) {
$symbolic = 's'; // Socket
} elseif (($perms & 0xA000) === 0xA000) {
$symbolic = 'l'; // Symbolic Link
} elseif (($perms & 0x8000) === 0x8000) {
$symbolic = '-'; // Regular
} elseif (($perms & 0x6000) === 0x6000) {
$symbolic = 'b'; // Block special
} elseif (($perms & 0x4000) === 0x4000) {
$symbolic = 'd'; // Directory
} elseif (($perms & 0x2000) === 0x2000) {
$symbolic = 'c'; // Character special
} elseif (($perms & 0x1000) === 0x1000) {
$symbolic = 'p'; // FIFO pipe
} else {
$symbolic = 'u'; // Unknown
}
// Owner
$symbolic .= ((($perms & 0x0100) !== 0) ? 'r' : '-')
. ((($perms & 0x0080) !== 0) ? 'w' : '-')
. ((($perms & 0x0040) !== 0) ? ((($perms & 0x0800) !== 0) ? 's' : 'x') : ((($perms & 0x0800) !== 0) ? 'S' : '-'));
// Group
$symbolic .= ((($perms & 0x0020) !== 0) ? 'r' : '-')
. ((($perms & 0x0010) !== 0) ? 'w' : '-')
. ((($perms & 0x0008) !== 0) ? ((($perms & 0x0400) !== 0) ? 's' : 'x') : ((($perms & 0x0400) !== 0) ? 'S' : '-'));
// World
$symbolic .= ((($perms & 0x0004) !== 0) ? 'r' : '-')
. ((($perms & 0x0002) !== 0) ? 'w' : '-')
. ((($perms & 0x0001) !== 0) ? ((($perms & 0x0200) !== 0) ? 't' : 'x') : ((($perms & 0x0200) !== 0) ? 'T' : '-'));
return $symbolic;
}
}
if (! function_exists('octal_permissions')) {
/**
* Octal Permissions
*
* Takes a numeric value representing a file's permissions and returns
* a three character string representing the file's octal permissions
*
* @param int $perms Permissions
*/
function octal_permissions(int $perms): string
{
return substr(sprintf('%o', $perms), -3);
}
}
if (! function_exists('same_file')) {
/**
* Checks if two files both exist and have identical hashes
*
* @return bool Same or not
*/
function same_file(string $file1, string $file2): bool
{
return is_file($file1) && is_file($file2) && md5_file($file1) === md5_file($file2);
}
}
if (! function_exists('set_realpath')) {
/**
* Set Realpath
*
* @param bool $checkExistence Checks to see if the path exists
*/
function set_realpath(string $path, bool $checkExistence = false): string
{
// Security check to make sure the path is NOT a URL. No remote file inclusion!
if (preg_match('#^(http:\/\/|https:\/\/|www\.|ftp)#i', $path) || filter_var($path, FILTER_VALIDATE_IP) === $path) {
throw new InvalidArgumentException('The path you submitted must be a local server path, not a URL');
}
// Resolve the path
if (realpath($path) !== false) {
$path = realpath($path);
} elseif ($checkExistence && ! is_dir($path) && ! is_file($path)) {
throw new InvalidArgumentException('Not a valid path: ' . $path);
}
// Add a trailing slash, if this is a directory
return is_dir($path) ? rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $path;
}
}
@@ -0,0 +1,802 @@
<?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\Validation\Exceptions\ValidationException;
use Config\App;
use Config\Validation;
// CodeIgniter Form Helpers
if (! function_exists('form_open')) {
/**
* Form Declaration
*
* Creates the opening portion of the form.
*
* @param string $action the URI segments of the form destination
* @param array|string $attributes a key/value pair of attributes, or string representation
* @param array $hidden a key/value pair hidden data
*/
function form_open(string $action = '', $attributes = [], array $hidden = []): string
{
// If no action is provided then set to the current url
if ($action === '') {
$action = (string) current_url(true);
} // If an action is not a full URL then turn it into one
elseif (! str_contains($action, '://')) {
// If an action has {locale}
if (str_contains($action, '{locale}')) {
$action = str_replace('{locale}', service('request')->getLocale(), $action);
}
$action = site_url($action);
}
if (is_array($attributes) && array_key_exists('csrf_id', $attributes)) {
$csrfId = $attributes['csrf_id'];
unset($attributes['csrf_id']);
}
$attributes = stringify_attributes($attributes);
if (! str_contains(strtolower($attributes), 'method=')) {
$attributes .= ' method="post"';
}
if (! str_contains(strtolower($attributes), 'accept-charset=')) {
$config = config(App::class);
$attributes .= ' accept-charset="' . strtolower($config->charset) . '"';
}
$form = '<form action="' . $action . '"' . $attributes . ">\n";
// Add CSRF field if enabled, but leave it out for GET requests and requests to external websites
$before = service('filters')->getFilters()['before'];
if ((in_array('csrf', $before, true) || array_key_exists('csrf', $before)) && str_contains($action, base_url()) && ! str_contains(strtolower($form), strtolower('method="get"'))) {
$form .= csrf_field($csrfId ?? null);
}
foreach ($hidden as $name => $value) {
$form .= form_hidden($name, $value);
}
return $form;
}
}
if (! function_exists('form_open_multipart')) {
/**
* Form Declaration - Multipart type
*
* Creates the opening portion of the form, but with "multipart/form-data".
*
* @param string $action The URI segments of the form destination
* @param array|string $attributes A key/value pair of attributes, or the same as a string
* @param array $hidden A key/value pair hidden data
*/
function form_open_multipart(string $action = '', $attributes = [], array $hidden = []): string
{
if (is_string($attributes)) {
$attributes .= ' enctype="' . esc('multipart/form-data') . '"';
} else {
$attributes['enctype'] = 'multipart/form-data';
}
return form_open($action, $attributes, $hidden);
}
}
if (! function_exists('form_hidden')) {
/**
* Hidden Input Field
*
* Generates hidden fields. You can pass a simple key/value string or
* an associative array with multiple values.
*
* @param array|string $name Field name or associative array to create multiple fields
* @param array|string $value Field value
*/
function form_hidden($name, $value = '', bool $recursing = false): string
{
static $form;
if ($recursing === false) {
$form = "\n";
}
if (is_array($name)) {
foreach ($name as $key => $val) {
form_hidden($key, $val, true);
}
return $form;
}
if (! is_array($value)) {
$form .= form_input($name, $value, '', 'hidden');
} else {
foreach ($value as $k => $v) {
$k = is_int($k) ? '' : $k;
form_hidden($name . '[' . $k . ']', $v, true);
}
}
return $form;
}
}
if (! function_exists('form_input')) {
/**
* Text Input Field. If 'type' is passed in the $type field, it will be
* used as the input type, for making 'email', 'phone', etc input fields.
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_input($data = '', string $value = '', $extra = '', string $type = 'text'): string
{
$defaults = [
'type' => $type,
'name' => is_array($data) ? '' : $data,
'value' => $value,
];
return '<input ' . parse_form_attributes($data, $defaults) . stringify_attributes($extra) . _solidus() . ">\n";
}
}
if (! function_exists('form_password')) {
/**
* Password Field
*
* Identical to the input function but adds the "password" type
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_password($data = '', string $value = '', $extra = ''): string
{
if (! is_array($data)) {
$data = ['name' => $data];
}
$data['type'] = 'password';
return form_input($data, $value, $extra);
}
}
if (! function_exists('form_upload')) {
/**
* Upload Field
*
* Identical to the input function but adds the "file" type
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_upload($data = '', string $value = '', $extra = ''): string
{
$defaults = [
'type' => 'file',
'name' => '',
];
if (! is_array($data)) {
$data = ['name' => $data];
}
$data['type'] = 'file';
return '<input ' . parse_form_attributes($data, $defaults) . stringify_attributes($extra) . _solidus() . ">\n";
}
}
if (! function_exists('form_textarea')) {
/**
* Textarea field
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_textarea($data = '', string $value = '', $extra = ''): string
{
$defaults = [
'name' => is_array($data) ? '' : $data,
'cols' => '40',
'rows' => '10',
];
if (! is_array($data) || ! isset($data['value'])) {
$val = $value;
} else {
$val = $data['value'];
unset($data['value']); // textareas don't use the value attribute
}
// Unsets default rows and cols if defined in extra field as array or string.
if ((is_array($extra) && array_key_exists('rows', $extra)) || (is_string($extra) && str_contains(strtolower(preg_replace('/\s+/', '', $extra)), 'rows='))) {
unset($defaults['rows']);
}
if ((is_array($extra) && array_key_exists('cols', $extra)) || (is_string($extra) && str_contains(strtolower(preg_replace('/\s+/', '', $extra)), 'cols='))) {
unset($defaults['cols']);
}
return '<textarea ' . rtrim(parse_form_attributes($data, $defaults)) . stringify_attributes($extra) . '>'
. htmlspecialchars($val)
. "</textarea>\n";
}
}
if (! function_exists('form_multiselect')) {
/**
* Multi-select menu
*
* @param array|string $name
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_multiselect($name = '', array $options = [], array $selected = [], $extra = ''): string
{
$extra = stringify_attributes($extra);
if (! str_contains(strtolower($extra), strtolower('multiple'))) {
$extra .= ' multiple="multiple"';
}
return form_dropdown($name, $options, $selected, $extra);
}
}
if (! function_exists('form_dropdown')) {
/**
* Drop-down Menu
*
* @param array|string $data
* @param array|string $options
* @param array|string $selected
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_dropdown($data = '', $options = [], $selected = [], $extra = ''): string
{
$defaults = [];
if (is_array($data)) {
if (isset($data['selected'])) {
$selected = $data['selected'];
unset($data['selected']); // select tags don't have a selected attribute
}
if (isset($data['options'])) {
$options = $data['options'];
unset($data['options']); // select tags don't use an options attribute
}
} else {
$defaults = ['name' => $data];
}
if (! is_array($selected)) {
$selected = [$selected];
}
if (! is_array($options)) {
$options = [$options];
}
// If no selected state was submitted we will attempt to set it automatically
if ($selected === []) {
if (is_array($data)) {
if (isset($data['name'], $_POST[$data['name']])) {
$selected = [$_POST[$data['name']]];
}
} elseif (isset($_POST[$data])) {
$selected = [$_POST[$data]];
}
}
// Standardize selected as strings, like the option keys will be
foreach ($selected as $key => $item) {
$selected[$key] = (string) $item;
}
$extra = stringify_attributes($extra);
$multiple = (count($selected) > 1 && ! str_contains(strtolower($extra), 'multiple')) ? ' multiple="multiple"' : '';
$form = '<select ' . rtrim(parse_form_attributes($data, $defaults)) . $extra . $multiple . ">\n";
foreach ($options as $key => $val) {
// Keys should always be strings for strict comparison
$key = (string) $key;
if (is_array($val)) {
if ($val === []) {
continue;
}
$form .= '<optgroup label="' . $key . "\">\n";
foreach ($val as $optgroupKey => $optgroupVal) {
// Keys should always be strings for strict comparison
$optgroupKey = (string) $optgroupKey;
$sel = in_array($optgroupKey, $selected, true) ? ' selected="selected"' : '';
$form .= '<option value="' . htmlspecialchars($optgroupKey) . '"' . $sel . '>' . $optgroupVal . "</option>\n";
}
$form .= "</optgroup>\n";
} else {
$form .= '<option value="' . htmlspecialchars($key) . '"'
. (in_array($key, $selected, true) ? ' selected="selected"' : '') . '>'
. $val . "</option>\n";
}
}
return $form . "</select>\n";
}
}
if (! function_exists('form_checkbox')) {
/**
* Checkbox Field
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_checkbox($data = '', string $value = '', bool $checked = false, $extra = ''): string
{
$defaults = [
'type' => 'checkbox',
'name' => (! is_array($data) ? $data : ''),
'value' => $value,
];
if (is_array($data) && array_key_exists('checked', $data)) {
$checked = $data['checked'];
if ($checked === false) {
unset($data['checked']);
} else {
$data['checked'] = 'checked';
}
}
if ($checked === true) {
$defaults['checked'] = 'checked';
}
return '<input ' . parse_form_attributes($data, $defaults) . stringify_attributes($extra) . _solidus() . ">\n";
}
}
if (! function_exists('form_radio')) {
/**
* Radio Button
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_radio($data = '', string $value = '', bool $checked = false, $extra = ''): string
{
if (! is_array($data)) {
$data = ['name' => $data];
}
$data['type'] = 'radio';
return form_checkbox($data, $value, $checked, $extra);
}
}
if (! function_exists('form_submit')) {
/**
* Submit Button
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_submit($data = '', string $value = '', $extra = ''): string
{
return form_input($data, $value, $extra, 'submit');
}
}
if (! function_exists('form_reset')) {
/**
* Reset Button
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_reset($data = '', string $value = '', $extra = ''): string
{
return form_input($data, $value, $extra, 'reset');
}
}
if (! function_exists('form_button')) {
/**
* Form Button
*
* @param array|string $data
* @param array|object|string $extra string, array, object that can be cast to array
*/
function form_button($data = '', string $content = '', $extra = ''): string
{
$defaults = [
'name' => is_array($data) ? '' : $data,
'type' => 'button',
];
if (is_array($data) && isset($data['content'])) {
$content = $data['content'];
unset($data['content']); // content is not an attribute
}
return '<button ' . parse_form_attributes($data, $defaults) . stringify_attributes($extra) . '>'
. $content
. "</button>\n";
}
}
if (! function_exists('form_label')) {
/**
* Form Label Tag
*
* @param string $labelText The text to appear onscreen
* @param string $id The id the label applies to
* @param array $attributes Additional attributes
*/
function form_label(string $labelText = '', string $id = '', array $attributes = []): string
{
$label = '<label';
if ($id !== '') {
$label .= ' for="' . $id . '"';
}
foreach ($attributes as $key => $val) {
$label .= ' ' . $key . '="' . $val . '"';
}
return $label . '>' . $labelText . '</label>';
}
}
if (! function_exists('form_datalist')) {
/**
* Datalist
*
* The <datalist> element specifies a list of pre-defined options for an <input> element.
* Users will see a drop-down list of pre-defined options as they input data.
* The list attribute of the <input> element, must refer to the id attribute of the <datalist> element.
*/
function form_datalist(string $name, string $value, array $options): string
{
$data = [
'type' => 'text',
'name' => $name,
'list' => $name . '_list',
'value' => $value,
];
$out = form_input($data) . "\n";
$out .= "<datalist id='" . $name . "_list'>";
foreach ($options as $option) {
$out .= "<option value='{$option}'>\n";
}
return $out . ("</datalist>\n");
}
}
if (! function_exists('form_fieldset')) {
/**
* Fieldset Tag
*
* Used to produce <fieldset><legend>text</legend>. To close fieldset
* use form_fieldset_close()
*
* @param string $legendText The legend text
* @param array $attributes Additional attributes
*/
function form_fieldset(string $legendText = '', array $attributes = []): string
{
$fieldset = '<fieldset' . stringify_attributes($attributes) . ">\n";
if ($legendText !== '') {
return $fieldset . '<legend>' . $legendText . "</legend>\n";
}
return $fieldset;
}
}
if (! function_exists('form_fieldset_close')) {
/**
* Fieldset Close Tag
*/
function form_fieldset_close(string $extra = ''): string
{
return '</fieldset>' . $extra;
}
}
if (! function_exists('form_close')) {
/**
* Form Close Tag
*/
function form_close(string $extra = ''): string
{
return '</form>' . $extra;
}
}
if (! function_exists('set_value')) {
/**
* Form Value
*
* Grabs a value from the POST array for the specified field so you can
* re-populate an input field or textarea
*
* @param string $field Field name
* @param list<string>|string $default Default value
* @param bool $htmlEscape Whether to escape HTML special characters or not
*
* @return list<string>|string
*/
function set_value(string $field, $default = '', bool $htmlEscape = true)
{
$request = service('request');
// Try any old input data we may have first
$value = $request->getOldInput($field);
if ($value === null) {
$value = $request->getPost($field) ?? $default;
}
return ($htmlEscape) ? esc($value) : $value;
}
}
if (! function_exists('set_select')) {
/**
* Set Select
*
* Let's you set the selected value of a <select> menu via data in the POST array.
*/
function set_select(string $field, string $value = '', bool $default = false): string
{
$request = service('request');
// Try any old input data we may have first
$input = $request->getOldInput($field);
if ($input === null) {
$input = $request->getPost($field);
}
if ($input === null) {
return $default ? ' selected="selected"' : '';
}
if (is_array($input)) {
// Note: in_array('', array(0)) returns TRUE, do not use it
foreach ($input as &$v) {
if ($value === $v) {
return ' selected="selected"';
}
}
return '';
}
return ($input === $value) ? ' selected="selected"' : '';
}
}
if (! function_exists('set_checkbox')) {
/**
* Set Checkbox
*
* Let's you set the selected value of a checkbox via the value in the POST array.
*/
function set_checkbox(string $field, string $value = '', bool $default = false): string
{
$request = service('request');
// Try any old input data we may have first
$input = $request->getOldInput($field);
if ($input === null) {
$input = $request->getPost($field);
}
if (is_array($input)) {
// Note: in_array('', array(0)) returns TRUE, do not use it
foreach ($input as &$v) {
if ($value === $v) {
return ' checked="checked"';
}
}
return '';
}
$session = service('session');
$hasOldInput = $session->has('_ci_old_input');
// Unchecked checkbox and radio inputs are not even submitted by browsers ...
if ((string) $input === '0' || ! empty($request->getPost()) || $hasOldInput) {
return ($input === $value) ? ' checked="checked"' : '';
}
return $default ? ' checked="checked"' : '';
}
}
if (! function_exists('set_radio')) {
/**
* Set Radio
*
* Let's you set the selected value of a radio field via info in the POST array.
*/
function set_radio(string $field, string $value = '', bool $default = false): string
{
$request = service('request');
// Try any old input data we may have first
$oldInput = $request->getOldInput($field);
$postInput = $request->getPost($field);
$input = $oldInput ?? $postInput ?? $default;
if (is_array($input)) {
// Note: in_array('', array(0)) returns TRUE, do not use it
foreach ($input as $v) {
if ($value === $v) {
return ' checked="checked"';
}
}
return '';
}
// Unchecked checkbox and radio inputs are not even submitted by browsers ...
if ($oldInput !== null || $postInput !== null) {
return ((string) $input === $value) ? ' checked="checked"' : '';
}
return $default ? ' checked="checked"' : '';
}
}
if (! function_exists('validation_errors')) {
/**
* Returns the validation errors.
*
* First, checks the validation errors that are stored in the session.
* To store the errors in the session, you need to use `withInput()` with `redirect()`.
*
* The returned array should be in the following format:
* [
* 'field1' => 'error message',
* 'field2' => 'error message',
* ]
*
* @return array<string, string>
*/
function validation_errors()
{
$errors = session('_ci_validation_errors');
// Check the session to see if any were
// passed along from a redirect withErrors() request.
if ($errors !== null && (ENVIRONMENT === 'testing' || ! is_cli())) {
return $errors;
}
$validation = service('validation');
return $validation->getErrors();
}
}
if (! function_exists('validation_list_errors')) {
/**
* Returns the rendered HTML of the validation errors.
*
* See Validation::listErrors()
*/
function validation_list_errors(string $template = 'list'): string
{
$config = config(Validation::class);
$view = service('renderer');
if (! array_key_exists($template, $config->templates)) {
throw ValidationException::forInvalidTemplate($template);
}
return $view->setVar('errors', validation_errors())
->render($config->templates[$template]);
}
}
if (! function_exists('validation_show_error')) {
/**
* Returns a single error for the specified field in formatted HTML.
*
* See Validation::showError()
*/
function validation_show_error(string $field, string $template = 'single'): string
{
$config = config(Validation::class);
$view = service('renderer');
$errors = array_filter(validation_errors(), static fn ($key): bool => preg_match(
'/^' . str_replace(['\.\*', '\*\.'], ['\..+', '.+\.'], preg_quote($field, '/')) . '$/',
$key,
) === 1, ARRAY_FILTER_USE_KEY);
if ($errors === []) {
return '';
}
if (! array_key_exists($template, $config->templates)) {
throw ValidationException::forInvalidTemplate($template);
}
return $view->setVar('error', implode("\n", $errors))
->render($config->templates[$template]);
}
}
if (! function_exists('parse_form_attributes')) {
/**
* Parse the form attributes
*
* Helper function used by some of the form helpers
*
* @internal
*
* @param array|string $attributes List of attributes
* @param array $default Default values
*/
function parse_form_attributes($attributes, array $default): string
{
if (is_array($attributes)) {
foreach (array_keys($default) as $key) {
if (isset($attributes[$key])) {
$default[$key] = $attributes[$key];
unset($attributes[$key]);
}
}
if ($attributes !== []) {
$default = array_merge($default, $attributes);
}
}
$att = '';
foreach ($default as $key => $val) {
if (! is_bool($val)) {
if ($key === 'value') {
$val = esc($val);
} elseif ($key === 'name' && $default['name'] === '') {
continue;
}
$att .= $key . '="' . $val . '"' . ($key === array_key_last($default) ? '' : ' ');
} else {
$att .= $key . ' ';
}
}
return $att;
}
}
@@ -0,0 +1,559 @@
<?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\Files\Exceptions\FileNotFoundException;
use Config\DocTypes;
use Config\Mimes;
// CodeIgniter HTML Helpers
if (! function_exists('ul')) {
/**
* Unordered List
*
* Generates an HTML unordered list from a single or
* multidimensional array.
*
* @param array $list List entries
* @param array|object|string $attributes HTML attributes string, array, object
*/
function ul(array $list, $attributes = ''): string
{
return _list('ul', $list, $attributes);
}
}
if (! function_exists('ol')) {
/**
* Ordered List
*
* Generates an HTML ordered list from a single or multidimensional array.
*
* @param array $list List entries
* @param array|object|string $attributes HTML attributes string, array, object
*/
function ol(array $list, $attributes = ''): string
{
return _list('ol', $list, $attributes);
}
}
if (! function_exists('_list')) {
/**
* Generates the list
*
* Generates an HTML ordered list from a single or multidimensional array.
*
* @param array $list List entries
* @param array|object|string $attributes HTML attributes string, array, object
*/
function _list(string $type = 'ul', $list = [], $attributes = '', int $depth = 0): string
{
// Set the indentation based on the depth
$out = str_repeat(' ', $depth)
// Write the opening list tag
. '<' . $type . stringify_attributes($attributes) . ">\n";
// Cycle through the list elements. If an array is
// encountered we will recursively call _list()
foreach ($list as $key => $val) {
$out .= str_repeat(' ', $depth + 2) . '<li>';
if (! is_array($val)) {
$out .= $val;
} else {
$out .= $key
. "\n"
. _list($type, $val, '', $depth + 4)
. str_repeat(' ', $depth + 2);
}
$out .= "</li>\n";
}
// Set the indentation for the closing tag and apply it
return $out . str_repeat(' ', $depth) . '</' . $type . ">\n";
}
}
if (! function_exists('img')) {
/**
* Image
*
* Generates an image element
*
* @param array|string $src Image source URI, or array of attributes and values
* @param bool $indexPage Should `Config\App::$indexPage` be added to the source path
* @param array|object|string $attributes Additional HTML attributes
*/
function img($src = '', bool $indexPage = false, $attributes = ''): string
{
if (! is_array($src)) {
$src = ['src' => $src];
}
if (! isset($src['src'])) {
$src['src'] = $attributes['src'] ?? '';
}
if (! isset($src['alt'])) {
$src['alt'] = $attributes['alt'] ?? '';
}
$img = '<img';
// Check for a relative URI
if (preg_match('#^([a-z]+:)?//#i', $src['src']) !== 1 && ! str_starts_with($src['src'], 'data:')) {
if ($indexPage) {
$img .= ' src="' . site_url($src['src']) . '"';
} else {
$img .= ' src="' . slash_item('baseURL') . $src['src'] . '"';
}
unset($src['src']);
}
// Append any other values
foreach ($src as $key => $value) {
$img .= ' ' . $key . '="' . $value . '"';
}
// Prevent passing completed values to stringify_attributes
if (is_array($attributes)) {
unset($attributes['alt'], $attributes['src']);
}
return $img . stringify_attributes($attributes) . _solidus() . '>';
}
}
if (! function_exists('img_data')) {
/**
* Image (data)
*
* Generates a src-ready string from an image using the "data:" protocol
*
* @param string $path Image source path
* @param string|null $mime MIME type to use, or null to guess
*/
function img_data(string $path, ?string $mime = null): string
{
if (! is_file($path) || ! is_readable($path)) {
throw FileNotFoundException::forFileNotFound($path);
}
// Read in file binary data
$handle = fopen($path, 'rb');
$data = fread($handle, filesize($path));
fclose($handle);
// Encode as base64
$data = base64_encode($data);
// Figure out the type (Hail Mary to JPEG)
$mime ??= Mimes::guessTypeFromExtension(pathinfo($path, PATHINFO_EXTENSION)) ?? 'image/jpg';
return 'data:' . $mime . ';base64,' . $data;
}
}
if (! function_exists('doctype')) {
/**
* Doctype
*
* Generates a page document type declaration
*
* Examples of valid options: html5, xhtml-11, xhtml-strict, xhtml-trans,
* xhtml-frame, html4-strict, html4-trans, and html4-frame.
* All values are saved in the doctypes config file.
*
* @param string $type The doctype to be generated
*/
function doctype(string $type = 'html5'): string
{
$config = new DocTypes();
$doctypes = $config->list;
return $doctypes[$type] ?? '';
}
}
if (! function_exists('script_tag')) {
/**
* Script
*
* Generates link to a JS file
*
* @param array|string $src Script source or an array of attributes
* @param bool $indexPage Should `Config\App::$indexPage` be added to the JS path
*/
function script_tag($src = '', bool $indexPage = false): string
{
$cspNonce = csp_script_nonce();
$cspNonce = $cspNonce !== '' ? ' ' . $cspNonce : $cspNonce;
$script = '<script' . $cspNonce . ' ';
if (! is_array($src)) {
$src = ['src' => $src];
}
foreach ($src as $k => $v) {
if ($k === 'src' && preg_match('#^([a-z]+:)?//#i', $v) !== 1) {
if ($indexPage) {
$script .= 'src="' . site_url($v) . '" ';
} else {
$script .= 'src="' . slash_item('baseURL') . $v . '" ';
}
} else {
// for attributes without values, like async or defer, use NULL.
$script .= $k . (null === $v ? ' ' : '="' . $v . '" ');
}
}
return rtrim($script) . '></script>';
}
}
if (! function_exists('link_tag')) {
/**
* Link
*
* Generates link tag
*
* @param array<string, bool|string>|string $href Stylesheet href or an array
* @param bool $indexPage Should `Config\App::$indexPage` be added to the CSS path.
*/
function link_tag(
$href = '',
string $rel = 'stylesheet',
string $type = 'text/css',
string $title = '',
string $media = '',
bool $indexPage = false,
string $hreflang = '',
): string {
$attributes = [];
// extract fields if needed
if (is_array($href)) {
$rel = $href['rel'] ?? $rel;
$type = $href['type'] ?? $type;
$title = $href['title'] ?? $title;
$media = $href['media'] ?? $media;
$hreflang = $href['hreflang'] ?? '';
$indexPage = $href['indexPage'] ?? $indexPage;
$href = $href['href'] ?? '';
}
if (preg_match('#^([a-z]+:)?//#i', $href) !== 1) {
$attributes['href'] = $indexPage ? site_url($href) : slash_item('baseURL') . $href;
} else {
$attributes['href'] = $href;
}
if ($hreflang !== '') {
$attributes['hreflang'] = $hreflang;
}
$attributes['rel'] = $rel;
if ($type !== '' && $rel !== 'canonical' && $hreflang === '' && ! ($rel === 'alternate' && $media !== '')) {
$attributes['type'] = $type;
}
if ($media !== '') {
$attributes['media'] = $media;
}
if ($title !== '') {
$attributes['title'] = $title;
}
return '<link' . stringify_attributes($attributes) . _solidus() . '>';
}
}
if (! function_exists('video')) {
/**
* Video
*
* Generates a video element to embed videos. The video element can
* contain one or more video sources
*
* @param array|string $src Either a source string or an array of sources
* @param string $unsupportedMessage The message to display if the media tag is not supported by the browser
* @param string $attributes HTML attributes
* @param bool $indexPage Should `Config\App::$indexPage` be added to the source path
*/
function video($src, string $unsupportedMessage = '', string $attributes = '', array $tracks = [], bool $indexPage = false): string
{
if (is_array($src)) {
return _media('video', $src, $unsupportedMessage, $attributes, $tracks);
}
$video = '<video';
if (_has_protocol($src)) {
$video .= ' src="' . $src . '"';
} elseif ($indexPage) {
$video .= ' src="' . site_url($src) . '"';
} else {
$video .= ' src="' . slash_item('baseURL') . $src . '"';
}
if ($attributes !== '') {
$video .= ' ' . $attributes;
}
$video .= ">\n";
foreach ($tracks as $track) {
$video .= _space_indent() . $track . "\n";
}
if ($unsupportedMessage !== '') {
$video .= _space_indent()
. $unsupportedMessage
. "\n";
}
return $video . "</video>\n";
}
}
if (! function_exists('audio')) {
/**
* Audio
*
* Generates an audio element to embed sounds
*
* @param array|string $src Either a source string or an array of sources
* @param string $unsupportedMessage The message to display if the media tag is not supported by the browser.
* @param string $attributes HTML attributes
* @param bool $indexPage Should `Config\App::$indexPage` be added to the source path
*/
function audio($src, string $unsupportedMessage = '', string $attributes = '', array $tracks = [], bool $indexPage = false): string
{
if (is_array($src)) {
return _media('audio', $src, $unsupportedMessage, $attributes, $tracks);
}
$audio = '<audio';
if (_has_protocol($src)) {
$audio .= ' src="' . $src . '"';
} elseif ($indexPage) {
$audio .= ' src="' . site_url($src) . '"';
} else {
$audio .= ' src="' . slash_item('baseURL') . $src . '"';
}
if ($attributes !== '') {
$audio .= ' ' . $attributes;
}
$audio .= '>';
foreach ($tracks as $track) {
$audio .= "\n" . _space_indent() . $track;
}
if ($unsupportedMessage !== '') {
$audio .= "\n" . _space_indent() . $unsupportedMessage . "\n";
}
return $audio . "</audio>\n";
}
}
if (! function_exists('_media')) {
/**
* Generate media based tag
*
* @param string $unsupportedMessage The message to display if the media tag is not supported by the browser.
*/
function _media(string $name, array $types = [], string $unsupportedMessage = '', string $attributes = '', array $tracks = []): string
{
$media = '<' . $name;
if ($attributes === '') {
$media .= '>';
} else {
$media .= ' ' . $attributes . '>';
}
$media .= "\n";
foreach ($types as $option) {
$media .= _space_indent() . $option . "\n";
}
foreach ($tracks as $track) {
$media .= _space_indent() . $track . "\n";
}
if ($unsupportedMessage !== '') {
$media .= _space_indent() . $unsupportedMessage . "\n";
}
return $media . ('</' . $name . ">\n");
}
}
if (! function_exists('source')) {
/**
* Source
*
* Generates a source element that specifies multiple media resources
* for either audio or video element
*
* @param string $src The path of the media resource
* @param string $type The MIME-type of the resource with optional codecs parameters
* @param string $attributes HTML attributes
* @param bool $indexPage Should `Config\App::$indexPage` be added to the source path
*/
function source(string $src, string $type = 'unknown', string $attributes = '', bool $indexPage = false): string
{
if (! _has_protocol($src)) {
$src = $indexPage ? site_url($src) : slash_item('baseURL') . $src;
}
$source = '<source src="' . $src
. '" type="' . $type . '"';
if ($attributes !== '') {
$source .= ' ' . $attributes;
}
return $source . _solidus() . '>';
}
}
if (! function_exists('track')) {
/**
* Track
*
* Generates a track element to specify timed tracks. The tracks are
* formatted in WebVTT format.
*
* @param string $src The path of the .VTT file
* @param string $kind How the text track is meant to be used
* @param string $srcLanguage Language of the track text data
* @param string $label A user-readable title of the text track
*/
function track(string $src, string $kind, string $srcLanguage, string $label): string
{
return '<track src="' . $src
. '" kind="' . $kind
. '" srclang="' . $srcLanguage
. '" label="' . $label
. '"' . _solidus() . '>';
}
}
if (! function_exists('object')) {
/**
* Object
*
* Generates an object element that represents the media
* as either image or a resource plugin such as audio, video,
* Java applets, ActiveX, PDF and Flash
*
* @param string $data A resource URL
* @param string $type Content-type of the resource
* @param string $attributes HTML attributes
* @param bool $indexPage Should `Config\App::$indexPage` be added to the data path
*/
function object(string $data, string $type = 'unknown', string $attributes = '', array $params = [], bool $indexPage = false): string
{
if (! _has_protocol($data)) {
$data = $indexPage ? site_url($data) : slash_item('baseURL') . $data;
}
$object = '<object data="' . $data . '" '
. $attributes . '>';
if ($params !== []) {
$object .= "\n";
}
foreach ($params as $param) {
$object .= _space_indent() . $param . "\n";
}
return $object . "</object>\n";
}
}
if (! function_exists('param')) {
/**
* Param
*
* Generates a param element that defines parameters
* for the object element.
*
* @param string $name The name of the parameter
* @param string $value The value of the parameter
* @param string $type The MIME-type
* @param string $attributes HTML attributes
*/
function param(string $name, string $value, string $type = 'ref', string $attributes = ''): string
{
return '<param name="' . $name
. '" type="' . $type
. '" value="' . $value
. '" ' . $attributes . _solidus() . '>';
}
}
if (! function_exists('embed')) {
/**
* Embed
*
* Generates an embed element
*
* @param string $src The path of the resource to embed
* @param string $type MIME-type
* @param string $attributes HTML attributes
* @param bool $indexPage Should `Config\App::$indexPage` be added to the source path
*/
function embed(string $src, string $type = 'unknown', string $attributes = '', bool $indexPage = false): string
{
if (! _has_protocol($src)) {
$src = $indexPage ? site_url($src) : slash_item('baseURL') . $src;
}
return '<embed src="' . $src
. '" type="' . $type . '" '
. $attributes . _solidus() . ">\n";
}
}
if (! function_exists('_has_protocol')) {
/**
* Test the protocol of a URI.
*
* @return false|int
*/
function _has_protocol(string $url)
{
return preg_match('#^([a-z]+:)?//#i', $url);
}
}
if (! function_exists('_space_indent')) {
/**
* Provide space indenting.
*/
function _space_indent(int $depth = 2): string
{
return str_repeat(' ', $depth);
}
}
@@ -0,0 +1,339 @@
<?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.
*/
// CodeIgniter Inflector Helpers
if (! function_exists('singular')) {
/**
* Singular
*
* Takes a plural word and makes it singular
*
* @param string $string Input string
*/
function singular(string $string): string
{
$result = $string;
if (! is_pluralizable($result)) {
return $result;
}
// Arranged in order.
$singularRules = [
'/(matr)ices$/' => '\1ix',
'/(vert|ind)ices$/' => '\1ex',
'/^(ox)en/' => '\1',
'/(alias)es$/' => '\1',
'/([octop|vir])i$/' => '\1us',
'/(cris|ax|test)es$/' => '\1is',
'/(shoe)s$/' => '\1',
'/(o)es$/' => '\1',
'/(bus|campus)es$/' => '\1',
'/([m|l])ice$/' => '\1ouse',
'/(x|ch|ss|sh)es$/' => '\1',
'/(m)ovies$/' => '\1\2ovie',
'/(s)eries$/' => '\1\2eries',
'/([^aeiouy]|qu)ies$/' => '\1y',
'/([lr])ves$/' => '\1f',
'/(tive)s$/' => '\1',
'/(hive)s$/' => '\1',
'/([^f])ves$/' => '\1fe',
'/(^analy)ses$/' => '\1sis',
'/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/' => '\1\2sis',
'/([ti])a$/' => '\1um',
'/(p)eople$/' => '\1\2erson',
'/(m)en$/' => '\1an',
'/(s)tatuses$/' => '\1\2tatus',
'/(c)hildren$/' => '\1\2hild',
'/(n)ews$/' => '\1\2ews',
'/(quiz)zes$/' => '\1',
'/([^us])s$/' => '\1',
];
foreach ($singularRules as $rule => $replacement) {
if (preg_match($rule, $result)) {
$result = preg_replace($rule, $replacement, $result);
break;
}
}
return $result;
}
}
if (! function_exists('plural')) {
/**
* Plural
*
* Takes a singular word and makes it plural
*
* @param string $string Input string
*/
function plural(string $string): string
{
$result = $string;
if (! is_pluralizable($result)) {
return $result;
}
$pluralRules = [
'/(quiz)$/' => '\1zes', // quizzes
'/^(ox)$/' => '\1\2en', // ox
'/([m|l])ouse$/' => '\1ice', // mouse, louse
'/(matr|vert|ind)ix|ex$/' => '\1ices', // matrix, vertex, index
'/(x|ch|ss|sh)$/' => '\1es', // search, switch, fix, box, process, address
'/([^aeiouy]|qu)y$/' => '\1ies', // query, ability, agency
'/(hive)$/' => '\1s', // archive, hive
'/(?:([^f])fe|([lr])f)$/' => '\1\2ves', // half, safe, wife
'/sis$/' => 'ses', // basis, diagnosis
'/([ti])um$/' => '\1a', // datum, medium
'/(p)erson$/' => '\1eople', // person, salesperson
'/(m)an$/' => '\1en', // man, woman, spokesman
'/(c)hild$/' => '\1hildren', // child
'/(buffal|tomat)o$/' => '\1\2oes', // buffalo, tomato
'/(bu|campu)s$/' => '\1\2ses', // bus, campus
'/(alias|status|virus)$/' => '\1es', // alias
'/(octop)us$/' => '\1i', // octopus
'/(ax|cris|test)is$/' => '\1es', // axis, crisis
'/s$/' => 's', // no change (compatibility)
'/$/' => 's',
];
foreach ($pluralRules as $rule => $replacement) {
if (preg_match($rule, $result)) {
$result = preg_replace($rule, $replacement, $result);
break;
}
}
return $result;
}
}
if (! function_exists('counted')) {
/**
* Counted
*
* Takes a number and a word to return the plural or not
* E.g. 0 cats, 1 cat, 2 cats, ...
*
* @param int $count Number of items
* @param string $string Input string
*/
function counted(int $count, string $string): string
{
$result = "{$count} ";
return $result . ($count === 1 ? singular($string) : plural($string));
}
}
if (! function_exists('camelize')) {
/**
* Camelize
*
* Takes multiple words separated by spaces or
* underscores and converts them to camel case.
*
* @param string $string Input string
*/
function camelize(string $string): string
{
return lcfirst(str_replace(' ', '', ucwords(preg_replace('/[\s_]+/', ' ', $string))));
}
}
if (! function_exists('pascalize')) {
/**
* Pascalize
*
* Takes multiple words separated by spaces or
* underscores and converts them to Pascal case,
* which is camel case with an uppercase first letter.
*
* @param string $string Input string
*/
function pascalize(string $string): string
{
return ucfirst(camelize($string));
}
}
if (! function_exists('underscore')) {
/**
* Underscore
*
* Takes multiple words separated by spaces and underscores them
*
* @param string $string Input string
*/
function underscore(string $string): string
{
$replacement = trim($string);
return preg_replace('/[\s]+/', '_', $replacement);
}
}
if (! function_exists('decamelize')) {
/**
* Decamelize
*
* Takes multiple words separated by camel case and
* underscores them.
*
* @param string $string Input string
*/
function decamelize(string $string): string
{
return strtolower(preg_replace(['/([a-z\d])([A-Z])/', '/([^_])([A-Z][a-z])/'], '$1_$2', trim($string)));
}
}
if (! function_exists('humanize')) {
/**
* Humanize
*
* Takes multiple words separated by the separator,
* camelizes and changes them to spaces
*
* @param string $string Input string
* @param string $separator Input separator
*/
function humanize(string $string, string $separator = '_'): string
{
$replacement = trim($string);
return ucwords(preg_replace('/[' . $separator . ']+/', ' ', $replacement));
}
}
if (! function_exists('is_pluralizable')) {
/**
* Checks if the given word has a plural version.
*
* @param string $word Word to check
*/
function is_pluralizable(string $word): bool
{
$uncountables = in_array(
strtolower($word),
[
'advice',
'bravery',
'butter',
'chaos',
'clarity',
'coal',
'courage',
'cowardice',
'curiosity',
'education',
'equipment',
'evidence',
'fish',
'fun',
'furniture',
'greed',
'help',
'homework',
'honesty',
'information',
'insurance',
'jewelry',
'knowledge',
'livestock',
'love',
'luck',
'marketing',
'meta',
'money',
'mud',
'news',
'patriotism',
'racism',
'rice',
'satisfaction',
'scenery',
'series',
'sexism',
'silence',
'species',
'spelling',
'sugar',
'water',
'weather',
'wisdom',
'work',
],
true,
);
return ! $uncountables;
}
}
if (! function_exists('dasherize')) {
/**
* Replaces underscores with dashes in the string.
*
* @param string $string Input string
*/
function dasherize(string $string): string
{
return str_replace('_', '-', $string);
}
}
if (! function_exists('ordinal')) {
/**
* Returns the suffix that should be added to a
* number to denote the position in an ordered
* sequence such as 1st, 2nd, 3rd, 4th.
*
* @param int $integer The integer to determine the suffix
*/
function ordinal(int $integer): string
{
$suffixes = [
'th',
'st',
'nd',
'rd',
'th',
'th',
'th',
'th',
'th',
'th',
];
return $integer % 100 >= 11 && $integer % 100 <= 13 ? 'th' : $suffixes[$integer % 10];
}
}
if (! function_exists('ordinalize')) {
/**
* Turns a number into an ordinal string used
* to denote the position in an ordered sequence
* such as 1st, 2nd, 3rd, 4th.
*
* @param int $integer The integer to ordinalize
*/
function ordinalize(int $integer): string
{
return $integer . ordinal($integer);
}
}
@@ -0,0 +1,92 @@
<?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.
*/
// This helper is autoloaded by CodeIgniter.
if (! function_exists('dd')) {
if (class_exists(Kint::class)) {
/**
* Prints a Kint debug report and exits.
*
* @param array $vars
*
* @return never
*
* @codeCoverageIgnore Can't be tested ... exits
*/
function dd(...$vars): void
{
// @codeCoverageIgnoreStart
Kint::$aliases[] = 'dd';
Kint::dump(...$vars);
exit;
// @codeCoverageIgnoreEnd
}
} else {
// In case that Kint is not loaded.
/**
* dd function
*
* @param array $vars
*
* @return int
*/
function dd(...$vars)
{
return 0;
}
}
}
if (! function_exists('d') && ! class_exists(Kint::class)) {
// In case that Kint is not loaded.
/**
* d function
*
* @param array $vars
*
* @return int
*/
function d(...$vars)
{
return 0;
}
}
if (! function_exists('trace')) {
if (class_exists(Kint::class)) {
/**
* Provides a backtrace to the current execution point, from Kint.
*/
/**
* trace function
*/
function trace(): void
{
Kint::$aliases[] = 'trace';
Kint::trace();
}
} else {
// In case that Kint is not loaded.
/**
* trace function
*
* @return int
*/
function trace()
{
return 0;
}
}
}
@@ -0,0 +1,222 @@
<?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\Exceptions\BadFunctionCallException;
// CodeIgniter Number Helpers
if (! function_exists('number_to_size')) {
/**
* Formats a numbers as bytes, based on size, and adds the appropriate suffix
*
* @param int|string $num Will be cast as int
* @param non-empty-string|null $locale [optional]
*
* @return bool|string
*/
function number_to_size($num, int $precision = 1, ?string $locale = null)
{
// Strip any formatting & ensure numeric input
try {
// @phpstan-ignore-next-line
$num = 0 + str_replace(',', '', (string) $num);
} catch (ErrorException) {
// Catch "Warning: A non-numeric value encountered"
return false;
}
// ignore sub part
$generalLocale = $locale;
if ($locale !== null && $locale !== '' && ($underscorePos = strpos($locale, '_'))) {
$generalLocale = substr($locale, 0, $underscorePos);
}
if ($num >= 1_000_000_000_000) {
$num = round($num / 1_099_511_627_776, $precision);
$unit = lang('Number.terabyteAbbr', [], $generalLocale);
} elseif ($num >= 1_000_000_000) {
$num = round($num / 1_073_741_824, $precision);
$unit = lang('Number.gigabyteAbbr', [], $generalLocale);
} elseif ($num >= 1_000_000) {
$num = round($num / 1_048_576, $precision);
$unit = lang('Number.megabyteAbbr', [], $generalLocale);
} elseif ($num >= 1000) {
$num = round($num / 1024, $precision);
$unit = lang('Number.kilobyteAbbr', [], $generalLocale);
} else {
$unit = lang('Number.bytes', [], $generalLocale);
}
return format_number($num, $precision, $locale, ['after' => ' ' . $unit]);
}
}
if (! function_exists('number_to_amount')) {
/**
* Converts numbers to a more readable representation
* when dealing with very large numbers (in the thousands or above),
* up to the quadrillions, because you won't often deal with numbers
* larger than that.
*
* It uses the "short form" numbering system as this is most commonly
* used within most English-speaking countries today.
*
* @see https://simple.wikipedia.org/wiki/Names_for_large_numbers
*
* @param int|string $num Will be cast as int
* @param int $precision [optional] The optional number of decimal digits to round to.
* @param non-empty-string|null $locale [optional]
*
* @return bool|string
*/
function number_to_amount($num, int $precision = 0, ?string $locale = null)
{
// Strip any formatting & ensure numeric input
try {
// @phpstan-ignore-next-line
$num = 0 + str_replace(',', '', (string) $num);
} catch (ErrorException) {
// Catch "Warning: A non-numeric value encountered"
return false;
}
$suffix = '';
// ignore sub part
$generalLocale = $locale;
if ($locale !== null && $locale !== '' && ($underscorePos = strpos($locale, '_'))) {
$generalLocale = substr($locale, 0, $underscorePos);
}
if ($num >= 1_000_000_000_000_000) {
$suffix = lang('Number.quadrillion', [], $generalLocale);
$num = round(($num / 1_000_000_000_000_000), $precision);
} elseif ($num >= 1_000_000_000_000) {
$suffix = lang('Number.trillion', [], $generalLocale);
$num = round(($num / 1_000_000_000_000), $precision);
} elseif ($num >= 1_000_000_000) {
$suffix = lang('Number.billion', [], $generalLocale);
$num = round(($num / 1_000_000_000), $precision);
} elseif ($num >= 1_000_000) {
$suffix = lang('Number.million', [], $generalLocale);
$num = round(($num / 1_000_000), $precision);
} elseif ($num >= 1000) {
$suffix = lang('Number.thousand', [], $generalLocale);
$num = round(($num / 1000), $precision);
}
return format_number($num, $precision, $locale, ['after' => $suffix]);
}
}
if (! function_exists('number_to_currency')) {
function number_to_currency(float $num, string $currency, ?string $locale = null, int $fraction = 0): string
{
return format_number($num, 1, $locale, [
'type' => NumberFormatter::CURRENCY,
'currency' => $currency,
'fraction' => $fraction,
]);
}
}
if (! function_exists('format_number')) {
/**
* A general purpose, locale-aware, number_format method.
* Used by all of the functions of the number_helper.
*/
function format_number(float $num, int $precision = 1, ?string $locale = null, array $options = []): string
{
// If locale is not passed, get from the default locale that is set from our config file
// or set by HTTP content negotiation.
$locale ??= Locale::getDefault();
// Type can be any of the NumberFormatter options, but provide a default.
$type = (int) ($options['type'] ?? NumberFormatter::DECIMAL);
$formatter = new NumberFormatter($locale, $type);
// Try to format it per the locale
if ($type === NumberFormatter::CURRENCY) {
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, (float) $options['fraction']);
$output = $formatter->formatCurrency($num, $options['currency']);
} else {
// In order to specify a precision, we'll have to modify
// the pattern used by NumberFormatter.
$pattern = '#,##0.' . str_repeat('#', $precision);
$formatter->setPattern($pattern);
$output = $formatter->format($num);
}
// This might lead a trailing period if $precision == 0
$output = trim($output, '. ');
if (intl_is_failure($formatter->getErrorCode())) {
throw new BadFunctionCallException($formatter->getErrorMessage());
}
// Add on any before/after text.
if (isset($options['before']) && is_string($options['before'])) {
$output = $options['before'] . $output;
}
if (isset($options['after']) && is_string($options['after'])) {
$output .= $options['after'];
}
return $output;
}
}
if (! function_exists('number_to_roman')) {
/**
* Convert a number to a roman numeral.
*
* @param int|string $num it will convert to int
*/
function number_to_roman($num): ?string
{
static $map = [
'M' => 1000,
'CM' => 900,
'D' => 500,
'CD' => 400,
'C' => 100,
'XC' => 90,
'L' => 50,
'XL' => 40,
'X' => 10,
'IX' => 9,
'V' => 5,
'IV' => 4,
'I' => 1,
];
$num = (int) $num;
if ($num < 1 || $num > 3999) {
return null;
}
$result = '';
foreach ($map as $roman => $arabic) {
$repeat = (int) floor($num / $arabic);
$result .= str_repeat($roman, $repeat);
$num %= $arabic;
}
return $result;
}
}
@@ -0,0 +1,51 @@
<?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.
*/
// CodeIgniter Security Helpers
if (! function_exists('sanitize_filename')) {
/**
* Sanitize a filename to use in a URI.
*/
function sanitize_filename(string $filename): string
{
return service('security')->sanitizeFilename($filename);
}
}
if (! function_exists('strip_image_tags')) {
/**
* Strip Image Tags
*/
function strip_image_tags(string $str): string
{
return preg_replace(
[
'#<img[\s/]+.*?src\s*=\s*(["\'])([^\\1]+?)\\1.*?\>#i',
'#<img[\s/]+.*?src\s*=\s*?(([^\s"\'=<>`]+)).*?\>#i',
],
'\\2',
$str,
);
}
}
if (! function_exists('encode_php_tags')) {
/**
* Convert PHP tags to entities
*/
function encode_php_tags(string $str): string
{
return str_replace(['<?', '?>'], ['&lt;?', '?&gt;'], $str);
}
}
@@ -0,0 +1,72 @@
<?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\Exceptions\TestException;
use CodeIgniter\Model;
use CodeIgniter\Test\Fabricator;
use Config\Services;
// CodeIgniter Test Helpers
if (! function_exists('fake')) {
/**
* Creates a single item using Fabricator.
*
* @param Model|object|string $model Instance or name of the model
* @param array|null $overrides Overriding data to pass to Fabricator::setOverrides()
* @param bool $persist
*
* @return array|object
*/
function fake($model, ?array $overrides = null, $persist = true)
{
$fabricator = new Fabricator($model);
if ($overrides !== null) {
$fabricator->setOverrides($overrides);
}
if ($persist) {
return $fabricator->create();
}
return $fabricator->make();
}
}
if (! function_exists('mock')) {
/**
* Used within our test suite to mock certain system tools.
*
* @param string $className Fully qualified class name
*
* @return object
*/
function mock(string $className)
{
$mockClass = $className::$mockClass;
$mockService = $className::$mockServiceName ?? '';
if (empty($mockClass) || ! class_exists($mockClass)) {
throw TestException::forInvalidMockClass($mockClass);
}
$mock = new $mockClass();
if (! empty($mockService)) {
Services::injectMock($mockService, $mock);
}
return $mock;
}
}
@@ -0,0 +1,760 @@
<?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\Exceptions\InvalidArgumentException;
use Config\ForeignCharacters;
// CodeIgniter Text Helpers
if (! function_exists('word_limiter')) {
/**
* Word Limiter
*
* Limits a string to X number of words.
*
* @param string $endChar the end character. Usually an ellipsis
*/
function word_limiter(string $str, int $limit = 100, string $endChar = '&#8230;'): string
{
if (trim($str) === '') {
return $str;
}
preg_match('/^\s*+(?:\S++\s*+){1,' . $limit . '}/', $str, $matches);
if (strlen($str) === strlen($matches[0])) {
$endChar = '';
}
return rtrim($matches[0]) . $endChar;
}
}
if (! function_exists('character_limiter')) {
/**
* Character Limiter
*
* Limits the string based on the character count. Preserves complete words
* so the character count may not be exactly as specified.
*
* @param string $endChar the end character. Usually an ellipsis
*/
function character_limiter(string $string, int $limit = 500, string $endChar = '&#8230;'): string
{
if (mb_strlen($string) < $limit) {
return $string;
}
// a bit complicated, but faster than preg_replace with \s+
$string = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $string));
$stringLength = mb_strlen($string);
if ($stringLength <= $limit) {
return $string;
}
$output = '';
$outputLength = 0;
$words = explode(' ', trim($string));
foreach ($words as $word) {
$output .= $word . ' ';
$outputLength = mb_strlen($output);
if ($outputLength >= $limit) {
$output = trim($output);
break;
}
}
return ($outputLength === $stringLength) ? $output : $output . $endChar;
}
}
if (! function_exists('ascii_to_entities')) {
/**
* High ASCII to Entities
*
* Converts high ASCII text and MS Word special characters to character entities
*/
function ascii_to_entities(string $str): string
{
$out = '';
for ($i = 0, $s = strlen($str) - 1, $count = 1, $temp = []; $i <= $s; $i++) {
$ordinal = ord($str[$i]);
if ($ordinal < 128) {
/*
If the $temp array has a value but we have moved on, then it seems only
fair that we output that entity and restart $temp before continuing.
*/
if (count($temp) === 1) {
$out .= '&#' . array_shift($temp) . ';';
$count = 1;
}
$out .= $str[$i];
} else {
if ($temp === []) {
$count = ($ordinal < 224) ? 2 : 3;
}
$temp[] = $ordinal;
if (count($temp) === $count) {
$number = ($count === 3) ? (($temp[0] % 16) * 4096) + (($temp[1] % 64) * 64) + ($temp[2] % 64) : (($temp[0] % 32) * 64) + ($temp[1] % 64);
$out .= '&#' . $number . ';';
$count = 1;
$temp = [];
}
// If this is the last iteration, just output whatever we have
elseif ($i === $s) {
$out .= '&#' . implode(';', $temp) . ';';
}
}
}
return $out;
}
}
if (! function_exists('entities_to_ascii')) {
/**
* Entities to ASCII
*
* Converts character entities back to ASCII
*/
function entities_to_ascii(string $str, bool $all = true): string
{
if (preg_match_all('/\&#(\d+)\;/', $str, $matches) >= 1) {
for ($i = 0, $s = count($matches[0]); $i < $s; $i++) {
$digits = (int) $matches[1][$i];
$out = '';
if ($digits < 128) {
$out .= chr($digits);
} elseif ($digits < 2048) {
$out .= chr(192 + (($digits - ($digits % 64)) / 64)) . chr(128 + ($digits % 64));
} else {
$out .= chr(224 + (($digits - ($digits % 4096)) / 4096))
. chr(128 + ((($digits % 4096) - ($digits % 64)) / 64))
. chr(128 + ($digits % 64));
}
$str = str_replace($matches[0][$i], $out, $str);
}
}
if ($all) {
return str_replace(
['&amp;', '&lt;', '&gt;', '&quot;', '&apos;', '&#45;'],
['&', '<', '>', '"', "'", '-'],
$str,
);
}
return $str;
}
}
if (! function_exists('word_censor')) {
/**
* Word Censoring Function
*
* Supply a string and an array of disallowed words and any
* matched words will be converted to #### or to the replacement
* word you've submitted.
*
* @param string $str the text string
* @param array $censored the array of censored words
* @param string $replacement the optional replacement value
*/
function word_censor(string $str, array $censored, string $replacement = ''): string
{
if ($censored === []) {
return $str;
}
$str = ' ' . $str . ' ';
// \w, \b and a few others do not match on a unicode character
// set for performance reasons. As a result words like über
// will not match on a word boundary. Instead, we'll assume that
// a bad word will be bookended by any of these characters.
$delim = '[-_\'\"`(){}<>\[\]|!?@#%&,.:;^~*+=\/ 0-9\n\r\t]';
foreach ($censored as $badword) {
$badword = str_replace('\*', '\w*?', preg_quote($badword, '/'));
if ($replacement !== '') {
$str = preg_replace(
"/({$delim})(" . $badword . ")({$delim})/i",
"\\1{$replacement}\\3",
$str,
);
} elseif (preg_match_all("/{$delim}(" . $badword . "){$delim}/i", $str, $matches, PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE) >= 1) {
$matches = $matches[1];
for ($i = count($matches) - 1; $i >= 0; $i--) {
$length = strlen($matches[$i][0]);
$str = substr_replace(
$str,
str_repeat('#', $length),
$matches[$i][1],
$length,
);
}
}
}
return trim($str);
}
}
if (! function_exists('highlight_code')) {
/**
* Code Highlighter
*
* Colorizes code strings
*
* @param string $str the text string
*/
function highlight_code(string $str): string
{
/* The highlight string function encodes and highlights
* brackets so we need them to start raw.
*
* Also replace any existing PHP tags to temporary markers
* so they don't accidentally break the string out of PHP,
* and thus, thwart the highlighting.
*/
$str = str_replace(
['&lt;', '&gt;', '<?', '?>', '<%', '%>', '\\', '</script>'],
['<', '>', 'phptagopen', 'phptagclose', 'asptagopen', 'asptagclose', 'backslashtmp', 'scriptclose'],
$str,
);
// The highlight_string function requires that the text be surrounded
// by PHP tags, which we will remove later
$str = highlight_string('<?php ' . $str . ' ?>', true);
// Remove our artificially added PHP, and the syntax highlighting that came with it
$str = preg_replace(
[
'/<span style="color: #([A-Z0-9]+)">&lt;\?php(&nbsp;| )/i',
'/(<span style="color: #[A-Z0-9]+">.*?)\?&gt;<\/span>\n<\/span>\n<\/code>/is',
'/<span style="color: #[A-Z0-9]+"\><\/span>/i',
],
[
'<span style="color: #$1">',
"$1</span>\n</span>\n</code>",
'',
],
$str,
);
// Replace our markers back to PHP tags.
return str_replace(
[
'phptagopen',
'phptagclose',
'asptagopen',
'asptagclose',
'backslashtmp',
'scriptclose',
],
[
'&lt;?',
'?&gt;',
'&lt;%',
'%&gt;',
'\\',
'&lt;/script&gt;',
],
$str,
);
}
}
if (! function_exists('highlight_phrase')) {
/**
* Phrase Highlighter
*
* Highlights a phrase within a text string
*
* @param string $str the text string
* @param string $phrase the phrase you'd like to highlight
* @param string $tagOpen the opening tag to precede the phrase with
* @param string $tagClose the closing tag to end the phrase with
*/
function highlight_phrase(string $str, string $phrase, string $tagOpen = '<mark>', string $tagClose = '</mark>'): string
{
return ($str !== '' && $phrase !== '') ? preg_replace('/(' . preg_quote($phrase, '/') . ')/i', $tagOpen . '\\1' . $tagClose, $str) : $str;
}
}
if (! function_exists('convert_accented_characters')) {
/**
* Convert Accented Foreign Characters to ASCII
*
* @param string $str Input string
*/
function convert_accented_characters(string $str): string
{
static $arrayFrom, $arrayTo;
if (! is_array($arrayFrom)) {
$config = new ForeignCharacters();
if ($config->characterList === [] || ! is_array($config->characterList)) {
$arrayFrom = [];
$arrayTo = [];
return $str;
}
$arrayFrom = array_keys($config->characterList);
$arrayTo = array_values($config->characterList);
unset($config);
}
return preg_replace($arrayFrom, $arrayTo, $str);
}
}
if (! function_exists('word_wrap')) {
/**
* Word Wrap
*
* Wraps text at the specified character. Maintains the integrity of words.
* Anything placed between {unwrap}{/unwrap} will not be word wrapped, nor
* will URLs.
*
* @param string $str the text string
* @param int $charlim = 76 the number of characters to wrap at
*/
function word_wrap(string $str, int $charlim = 76): string
{
// Reduce multiple spaces
$str = preg_replace('| +|', ' ', $str);
// Standardize newlines
if (str_contains($str, "\r")) {
$str = str_replace(["\r\n", "\r"], "\n", $str);
}
// If the current word is surrounded by {unwrap} tags we'll
// strip the entire chunk and replace it with a marker.
$unwrap = [];
if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches) >= 1) {
for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
$unwrap[] = $matches[1][$i];
$str = str_replace($matches[0][$i], '{{unwrapped' . $i . '}}', $str);
}
}
// Use PHP's native function to do the initial wordwrap.
// We set the cut flag to FALSE so that any individual words that are
// too long get left alone. In the next step we'll deal with them.
$str = wordwrap($str, $charlim, "\n", false);
// Split the string into individual lines of text and cycle through them
$output = '';
foreach (explode("\n", $str) as $line) {
// Is the line within the allowed character count?
// If so we'll join it to the output and continue
if (mb_strlen($line) <= $charlim) {
$output .= $line . "\n";
continue;
}
$temp = '';
while (mb_strlen($line) > $charlim) {
// If the over-length word is a URL we won't wrap it
if (preg_match('!\[url.+\]|://|www\.!', $line)) {
break;
}
// Trim the word down
$temp .= mb_substr($line, 0, $charlim - 1);
$line = mb_substr($line, $charlim - 1);
}
// If $temp contains data it means we had to split up an over-length
// word into smaller chunks so we'll add it back to our current line
if ($temp !== '') {
$output .= $temp . "\n" . $line . "\n";
} else {
$output .= $line . "\n";
}
}
// Put our markers back
foreach ($unwrap as $key => $val) {
$output = str_replace('{{unwrapped' . $key . '}}', $val, $output);
}
// remove any trailing newline
return rtrim($output);
}
}
if (! function_exists('ellipsize')) {
/**
* Ellipsize String
*
* This function will strip tags from a string, split it at its max_length and ellipsize
*
* @param string $str String to ellipsize
* @param int $maxLength Max length of string
* @param float|int $position int (1|0) or float, .5, .2, etc for position to split
* @param string $ellipsis ellipsis ; Default '...'
*
* @return string Ellipsized string
*/
function ellipsize(string $str, int $maxLength, $position = 1, string $ellipsis = '&hellip;'): string
{
// Strip tags
$str = trim(strip_tags($str));
// Is the string long enough to ellipsize?
if (mb_strlen($str) <= $maxLength) {
return $str;
}
$beg = mb_substr($str, 0, (int) floor($maxLength * $position));
$position = ($position > 1) ? 1 : $position;
if ($position === 1) {
$end = mb_substr($str, 0, -($maxLength - mb_strlen($beg)));
} else {
$end = mb_substr($str, -($maxLength - mb_strlen($beg)));
}
return $beg . $ellipsis . $end;
}
}
if (! function_exists('strip_slashes')) {
/**
* Strip Slashes
*
* Removes slashes contained in a string or in an array
*
* @param array|string $str string or array
*
* @return array|string string or array
*/
function strip_slashes($str)
{
if (! is_array($str)) {
return stripslashes($str);
}
foreach ($str as $key => $val) {
$str[$key] = strip_slashes($val);
}
return $str;
}
}
if (! function_exists('strip_quotes')) {
/**
* Strip Quotes
*
* Removes single and double quotes from a string
*/
function strip_quotes(string $str): string
{
return str_replace(['"', "'"], '', $str);
}
}
if (! function_exists('quotes_to_entities')) {
/**
* Quotes to Entities
*
* Converts single and double quotes to entities
*/
function quotes_to_entities(string $str): string
{
return str_replace(["\\'", '"', "'", '"'], ['&#39;', '&quot;', '&#39;', '&quot;'], $str);
}
}
if (! function_exists('reduce_double_slashes')) {
/**
* Reduce Double Slashes
*
* Converts double slashes in a string to a single slash,
* except those found in http://
*
* http://www.some-site.com//index.php
*
* becomes:
*
* http://www.some-site.com/index.php
*/
function reduce_double_slashes(string $str): string
{
return preg_replace('#(^|[^:])//+#', '\\1/', $str);
}
}
if (! function_exists('reduce_multiples')) {
/**
* Reduce Multiples
*
* Reduces multiple instances of a particular character. Example:
*
* Fred, Bill,, Joe, Jimmy
*
* becomes:
*
* Fred, Bill, Joe, Jimmy
*
* @param string $character the character you wish to reduce
* @param bool $trim TRUE/FALSE - whether to trim the character from the beginning/end
*/
function reduce_multiples(string $str, string $character = ',', bool $trim = false): string
{
$pattern = '#' . preg_quote($character, '#') . '{2,}#';
$str = preg_replace($pattern, $character, $str);
return $trim ? trim($str, $character) : $str;
}
}
if (! function_exists('random_string')) {
/**
* Create a Random String
*
* Useful for generating passwords or hashes.
*
* @param string $type Type of random string. basic, alpha, alnum, numeric, nozero, md5, sha1, and crypto
* @param int $len Number of characters
*
* @deprecated The type 'basic', 'md5', and 'sha1' are deprecated. They are not cryptographically secure.
*/
function random_string(string $type = 'alnum', int $len = 8): string
{
switch ($type) {
case 'alnum':
case 'nozero':
case 'alpha':
switch ($type) {
case 'alpha':
$pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'alnum':
$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'nozero':
$pool = '123456789';
break;
}
return _from_random($len, $pool);
case 'numeric':
$max = 10 ** $len - 1;
$rand = random_int(0, $max);
return sprintf('%0' . $len . 'd', $rand);
case 'md5':
return md5(uniqid((string) mt_rand(), true));
case 'sha1':
return sha1(uniqid((string) mt_rand(), true));
case 'crypto':
if ($len % 2 !== 0) {
throw new InvalidArgumentException(
'You must set an even number to the second parameter when you use `crypto`.',
);
}
return bin2hex(random_bytes($len / 2));
}
// 'basic' type treated as default
return (string) mt_rand();
}
}
if (! function_exists('_from_random')) {
/**
* The following function was derived from code of Symfony (v6.2.7 - 2023-02-28)
* https://github.com/symfony/symfony/blob/80cac46a31d4561804c17d101591a4f59e6db3a2/src/Symfony/Component/String/ByteString.php#L45
* Code subject to the MIT license (https://github.com/symfony/symfony/blob/v6.2.7/LICENSE).
* Copyright (c) 2004-present Fabien Potencier
*
* The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03)
* https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16
* Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE).
* Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/)
*
* @internal Outside the framework this should not be used directly.
*/
function _from_random(int $length, string $pool): string
{
if ($length <= 0) {
throw new InvalidArgumentException(
sprintf('A strictly positive length is expected, "%d" given.', $length),
);
}
$poolSize = \strlen($pool);
$bits = (int) ceil(log($poolSize, 2.0));
if ($bits <= 0 || $bits > 56) {
throw new InvalidArgumentException(
'The length of the alphabet must in the [2^1, 2^56] range.',
);
}
$string = '';
while ($length > 0) {
$urandomLength = (int) ceil(2 * $length * $bits / 8.0);
$data = random_bytes($urandomLength);
$unpackedData = 0;
$unpackedBits = 0;
for ($i = 0; $i < $urandomLength && $length > 0; $i++) {
// Unpack 8 bits
$unpackedData = ($unpackedData << 8) | \ord($data[$i]);
$unpackedBits += 8;
// While we have enough bits to select a character from the alphabet, keep
// consuming the random data
for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) {
$index = ($unpackedData & ((1 << $bits) - 1));
$unpackedData >>= $bits;
// Unfortunately, the alphabet size is not necessarily a power of two.
// Worst case, it is 2^k + 1, which means we need (k+1) bits and we
// have around a 50% chance of missing as k gets larger
if ($index < $poolSize) {
$string .= $pool[$index];
$length--;
}
}
}
}
return $string;
}
}
if (! function_exists('increment_string')) {
/**
* Add's _1 to a string or increment the ending number to allow _2, _3, etc
*
* @param string $str Required
* @param string $separator What should the duplicate number be appended with
* @param int $first Which number should be used for the first dupe increment
*/
function increment_string(string $str, string $separator = '_', int $first = 1): string
{
preg_match('/(.+)' . preg_quote($separator, '/') . '([0-9]+)$/', $str, $match);
return isset($match[2]) ? $match[1] . $separator . ((int) $match[2] + 1) : $str . $separator . $first;
}
}
if (! function_exists('alternator')) {
/**
* Alternator
*
* Allows strings to be alternated. See docs...
*
* @param string ...$args (as many parameters as needed)
*/
function alternator(...$args): string
{
static $i;
if (func_num_args() === 0) {
$i = 0;
return '';
}
return $args[($i++ % count($args))];
}
}
if (! function_exists('excerpt')) {
/**
* Excerpt.
*
* Allows to extract a piece of text surrounding a word or phrase.
*
* @param string $text String to search the phrase
* @param string $phrase Phrase that will be searched for.
* @param int $radius The amount of characters returned around the phrase.
* @param string $ellipsis Ending that will be appended
*
* If no $phrase is passed, will generate an excerpt of $radius characters
* from the beginning of $text.
*/
function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $ellipsis = '...'): string
{
if (isset($phrase)) {
$phrasePosition = mb_stripos($text, $phrase);
$phraseLength = mb_strlen($phrase);
} else {
$phrasePosition = $radius / 2;
$phraseLength = 1;
}
$beforeWords = explode(' ', mb_substr($text, 0, $phrasePosition));
$afterWords = explode(' ', mb_substr($text, $phrasePosition + $phraseLength));
$firstPartOutput = ' ';
$endPartOutput = ' ';
$count = 0;
foreach (array_reverse($beforeWords) as $beforeWord) {
$beforeWordLength = mb_strlen($beforeWord);
if (($beforeWordLength + $count + 1) < $radius) {
$firstPartOutput = ' ' . $beforeWord . $firstPartOutput;
}
$count = ++$count + $beforeWordLength;
}
$count = 0;
foreach ($afterWords as $afterWord) {
$afterWordLength = mb_strlen($afterWord);
if (($afterWordLength + $count + 1) < $radius) {
$endPartOutput .= $afterWord . ' ';
}
$count = ++$count + $afterWordLength;
}
$ellPre = $phrase !== null ? $ellipsis : '';
return str_replace(' ', ' ', $ellPre . $firstPartOutput . $phrase . $endPartOutput . $ellipsis);
}
}
@@ -0,0 +1,536 @@
<?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\HTTP\CLIRequest;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\SiteURI;
use CodeIgniter\HTTP\URI;
use CodeIgniter\Router\Exceptions\RouterException;
use Config\App;
// CodeIgniter URL Helpers
if (! function_exists('site_url')) {
/**
* Returns a site URL as defined by the App config.
*
* @param array|string $relativePath URI string or array of URI segments.
* @param string|null $scheme URI scheme. E.g., http, ftp. If empty
* string '' is set, a protocol-relative
* link is returned.
* @param App|null $config Alternate configuration to use.
*/
function site_url($relativePath = '', ?string $scheme = null, ?App $config = null): string
{
$currentURI = service('request')->getUri();
assert($currentURI instanceof SiteURI);
return $currentURI->siteUrl($relativePath, $scheme, $config);
}
}
if (! function_exists('base_url')) {
/**
* Returns the base URL as defined by the App config.
* Base URLs are trimmed site URLs without the index page.
*
* @param array|string $relativePath URI string or array of URI segments.
* @param string|null $scheme URI scheme. E.g., http, ftp. If empty
* string '' is set, a protocol-relative
* link is returned.
*/
function base_url($relativePath = '', ?string $scheme = null): string
{
$currentURI = service('request')->getUri();
assert($currentURI instanceof SiteURI);
return $currentURI->baseUrl($relativePath, $scheme);
}
}
if (! function_exists('current_url')) {
/**
* Returns the current full URL based on the Config\App settings and IncomingRequest.
*
* @param bool $returnObject True to return an object instead of a string
* @param IncomingRequest|null $request A request to use when retrieving the path
*
* @return string|URI When returning string, the query and fragment parts are removed.
* When returning URI, the query and fragment parts are preserved.
*/
function current_url(bool $returnObject = false, ?IncomingRequest $request = null)
{
$request ??= service('request');
/** @var CLIRequest|IncomingRequest $request */
$uri = $request->getUri();
return $returnObject ? $uri : URI::createURIString($uri->getScheme(), $uri->getAuthority(), $uri->getPath());
}
}
if (! function_exists('previous_url')) {
/**
* Returns the previous URL the current visitor was on. For security reasons
* we first check in a saved session variable, if it exists, and use that.
* If that's not available, however, we'll use a sanitized url from $_SERVER['HTTP_REFERER']
* which can be set by the user so is untrusted and not set by certain browsers/servers.
*
* @return string|URI
*/
function previous_url(bool $returnObject = false)
{
// Grab from the session first, if we have it,
// since it's more reliable and safer.
if (isset($_SESSION)) {
$referer = session('_ci_previous_url');
}
// Otherwise, grab a sanitized version from $_SERVER.
$referer ??= request()->getServer('HTTP_REFERER', FILTER_SANITIZE_URL) ?? site_url('/');
return $returnObject ? new URI($referer) : $referer;
}
}
if (! function_exists('uri_string')) {
/**
* URL String
*
* Returns the path part (relative to baseURL) of the current URL
*/
function uri_string(): string
{
// The value of service('request')->getUri()->getPath() returns
// full URI path.
$uri = service('request')->getUri();
$path = $uri instanceof SiteURI ? $uri->getRoutePath() : $uri->getPath();
return ltrim($path, '/');
}
}
if (! function_exists('index_page')) {
/**
* Index page
*
* Returns the "index_page" from your config file
*
* @param App|null $altConfig Alternate configuration to use
*/
function index_page(?App $altConfig = null): string
{
// use alternate config if provided, else default one
$config = $altConfig ?? config(App::class);
return $config->indexPage;
}
}
if (! function_exists('anchor')) {
/**
* Anchor Link
*
* Creates an anchor based on the local URL.
*
* @param array|string $uri URI string or array of URI segments
* @param string $title The link title
* @param array|object|string $attributes Any attributes
* @param App|null $altConfig Alternate configuration to use
*/
function anchor($uri = '', string $title = '', $attributes = '', ?App $altConfig = null): string
{
// use alternate config if provided, else default one
$config = $altConfig ?? config(App::class);
$siteUrl = is_array($uri) ? site_url($uri, null, $config) : (preg_match('#^(\w+:)?//#i', $uri) ? $uri : site_url($uri, null, $config));
// eliminate trailing slash
$siteUrl = rtrim($siteUrl, '/');
if ($title === '') {
$title = $siteUrl;
}
if ($attributes !== '') {
$attributes = stringify_attributes($attributes);
}
return '<a href="' . $siteUrl . '"' . $attributes . '>' . $title . '</a>';
}
}
if (! function_exists('anchor_popup')) {
/**
* Anchor Link - Pop-up version
*
* Creates an anchor based on the local URL. The link
* opens a new window based on the attributes specified.
*
* @param string $uri the URL
* @param string $title the link title
* @param array|false|object|string $attributes any attributes
* @param App|null $altConfig Alternate configuration to use
*/
function anchor_popup($uri = '', string $title = '', $attributes = false, ?App $altConfig = null): string
{
// use alternate config if provided, else default one
$config = $altConfig ?? config(App::class);
$siteUrl = preg_match('#^(\w+:)?//#i', $uri) ? $uri : site_url($uri, null, $config);
$siteUrl = rtrim($siteUrl, '/');
if ($title === '') {
$title = $siteUrl;
}
if ($attributes === false) {
return '<a href="' . $siteUrl . '" onclick="window.open(\'' . $siteUrl . "', '_blank'); return false;\">" . $title . '</a>';
}
if (! is_array($attributes)) {
$attributes = [$attributes];
// Ref: http://www.w3schools.com/jsref/met_win_open.asp
$windowName = '_blank';
} elseif (! empty($attributes['window_name'])) {
$windowName = $attributes['window_name'];
unset($attributes['window_name']);
} else {
$windowName = '_blank';
}
$atts = [];
foreach (['width' => '800', 'height' => '600', 'scrollbars' => 'yes', 'menubar' => 'no', 'status' => 'yes', 'resizable' => 'yes', 'screenx' => '0', 'screeny' => '0'] as $key => $val) {
$atts[$key] = $attributes[$key] ?? $val;
unset($attributes[$key]);
}
$attributes = stringify_attributes($attributes);
return '<a href="' . $siteUrl
. '" onclick="window.open(\'' . $siteUrl . "', '" . $windowName . "', '" . stringify_attributes($atts, true) . "'); return false;\""
. $attributes . '>' . $title . '</a>';
}
}
if (! function_exists('mailto')) {
/**
* Mailto Link
*
* @param string $email the email address
* @param string $title the link title
* @param array|object|string $attributes any attributes
*/
function mailto(string $email, string $title = '', $attributes = ''): string
{
if (trim($title) === '') {
$title = $email;
}
return '<a href="mailto:' . $email . '"' . stringify_attributes($attributes) . '>' . $title . '</a>';
}
}
if (! function_exists('safe_mailto')) {
/**
* Encoded Mailto Link
*
* Create a spam-protected mailto link written in Javascript
*
* @param string $email the email address
* @param string $title the link title
* @param array|object|string $attributes any attributes
*/
function safe_mailto(string $email, string $title = '', $attributes = ''): string
{
$count = 0;
if (trim($title) === '') {
$title = $email;
}
$x = str_split('<a href="mailto:', 1);
for ($i = 0, $l = strlen($email); $i < $l; $i++) {
$x[] = '|' . ord($email[$i]);
}
$x[] = '"';
if ($attributes !== '') {
if (is_array($attributes)) {
foreach ($attributes as $key => $val) {
$x[] = ' ' . $key . '="';
for ($i = 0, $l = strlen($val); $i < $l; $i++) {
$x[] = '|' . ord($val[$i]);
}
$x[] = '"';
}
} else {
for ($i = 0, $l = mb_strlen($attributes); $i < $l; $i++) {
$x[] = mb_substr($attributes, $i, 1);
}
}
}
$x[] = '>';
$temp = [];
for ($i = 0, $l = strlen($title); $i < $l; $i++) {
$ordinal = ord($title[$i]);
if ($ordinal < 128) {
$x[] = '|' . $ordinal;
} else {
if ($temp === []) {
$count = ($ordinal < 224) ? 2 : 3;
}
$temp[] = $ordinal;
if (count($temp) === $count) {
$number = ($count === 3) ? (($temp[0] % 16) * 4096) + (($temp[1] % 64) * 64) + ($temp[2] % 64) : (($temp[0] % 32) * 64) + ($temp[1] % 64);
$x[] = '|' . $number;
$count = 1;
$temp = [];
}
}
}
$x[] = '<';
$x[] = '/';
$x[] = 'a';
$x[] = '>';
$x = array_reverse($x);
// improve obfuscation by eliminating newlines & whitespace
$cspNonce = csp_script_nonce();
$cspNonce = $cspNonce !== '' ? ' ' . $cspNonce : $cspNonce;
$output = '<script' . $cspNonce . '>'
. 'var l=new Array();';
foreach ($x as $i => $value) {
$output .= 'l[' . $i . "] = '" . $value . "';";
}
return $output . ('for (var i = l.length-1; i >= 0; i=i-1) {'
. "if (l[i].substring(0, 1) === '|') document.write(\"&#\"+unescape(l[i].substring(1))+\";\");"
. 'else document.write(unescape(l[i]));'
. '}'
. '</script>');
}
}
if (! function_exists('auto_link')) {
/**
* Auto-linker
*
* Automatically links URL and Email addresses.
* Note: There's a bit of extra code here to deal with
* URLs or emails that end in a period. We'll strip these
* off and add them after the link.
*
* @param string $str the string
* @param string $type the type: email, url, or both
* @param bool $popup whether to create pop-up links
*/
function auto_link(string $str, string $type = 'both', bool $popup = false): string
{
// Find and replace any URLs.
if (
$type !== 'email'
&& preg_match_all(
'#([a-z][a-z0-9+\-.]*://|www\.)[a-z0-9]+(-+[a-z0-9]+)*(\.[a-z0-9]+(-+[a-z0-9]+)*)+(/([^\s()<>;]+\w)?/?)?#i',
$str,
$matches,
PREG_OFFSET_CAPTURE | PREG_SET_ORDER,
) >= 1
) {
// Set our target HTML if using popup links.
$target = ($popup) ? ' target="_blank"' : '';
// We process the links in reverse order (last -> first) so that
// the returned string offsets from preg_match_all() are not
// moved as we add more HTML.
foreach (array_reverse($matches) as $match) {
// $match[0] is the matched string/link
// $match[1] is either a protocol prefix or 'www.'
//
// With PREG_OFFSET_CAPTURE, both of the above is an array,
// where the actual value is held in [0] and its offset at the [1] index.
$a = '<a href="' . (strpos($match[1][0], '/') ? '' : 'http://') . $match[0][0] . '"' . $target . '>' . $match[0][0] . '</a>';
$str = substr_replace($str, $a, $match[0][1], strlen($match[0][0]));
}
}
// Find and replace any emails.
if (
$type !== 'url'
&& preg_match_all(
'#([\w\.\-\+]+@[a-z0-9\-]+\.[a-z0-9\-\.]+[^[:punct:]\s])#i',
$str,
$matches,
PREG_OFFSET_CAPTURE,
) >= 1
) {
foreach (array_reverse($matches[0]) as $match) {
if (filter_var($match[0], FILTER_VALIDATE_EMAIL) !== false) {
$str = substr_replace($str, safe_mailto($match[0]), $match[1], strlen($match[0]));
}
}
}
return $str;
}
}
if (! function_exists('prep_url')) {
/**
* Prep URL - Simply adds the http:// or https:// part if no scheme is included.
*
* Formerly used URI, but that does not play nicely with URIs missing
* the scheme.
*
* @param string $str the URL
* @param bool $secure set true if you want to force https://
*/
function prep_url(string $str = '', bool $secure = false): string
{
if (in_array($str, ['http://', 'https://', '//', ''], true)) {
return '';
}
if (parse_url($str, PHP_URL_SCHEME) === null) {
$str = 'http://' . ltrim($str, '/');
}
// force replace http:// with https://
if ($secure) {
$str = preg_replace('/^(?:http):/i', 'https:', $str);
}
return $str;
}
}
if (! function_exists('url_title')) {
/**
* Create URL Title
*
* Takes a "title" string as input and creates a
* human-friendly URL string with a "separator" string
* as the word separator.
*
* @param string $str Input string
* @param string $separator Word separator (usually '-' or '_')
* @param bool $lowercase Whether to transform the output string to lowercase
*/
function url_title(string $str, string $separator = '-', bool $lowercase = false): string
{
$qSeparator = preg_quote($separator, '#');
$trans = [
'&.+?;' => '',
'[^\w\d\pL\pM _-]' => '',
'\s+' => $separator,
'(' . $qSeparator . ')+' => $separator,
];
$str = strip_tags($str);
foreach ($trans as $key => $val) {
$str = preg_replace('#' . $key . '#iu', $val, $str);
}
if ($lowercase) {
$str = mb_strtolower($str);
}
return trim(trim($str, $separator));
}
}
if (! function_exists('mb_url_title')) {
/**
* Create URL Title that takes into account accented characters
*
* Takes a "title" string as input and creates a
* human-friendly URL string with a "separator" string
* as the word separator.
*
* @param string $str Input string
* @param string $separator Word separator (usually '-' or '_')
* @param bool $lowercase Whether to transform the output string to lowercase
*/
function mb_url_title(string $str, string $separator = '-', bool $lowercase = false): string
{
helper('text');
return url_title(convert_accented_characters($str), $separator, $lowercase);
}
}
if (! function_exists('url_to')) {
/**
* Get the full, absolute URL to a route name or controller method
* (with additional arguments)
*
* NOTE: This requires the controller/method to
* have a route defined in the routes Config file.
*
* @param string $controller Route name or Controller::method
* @param int|string ...$args One or more parameters to be passed to the route.
* The last parameter allows you to set the locale.
*
* @throws RouterException
*/
function url_to(string $controller, ...$args): string
{
if (! $route = route_to($controller, ...$args)) {
$explode = explode('::', $controller);
if (isset($explode[1])) {
throw RouterException::forControllerNotFound($explode[0], $explode[1]);
}
throw RouterException::forInvalidRoute($controller);
}
return site_url($route);
}
}
if (! function_exists('url_is')) {
/**
* Determines if current url path contains
* the given path. It may contain a wildcard (*)
* which will allow any valid character.
*
* Example:
* if (url_is('admin*')) ...
*/
function url_is(string $path): bool
{
// Setup our regex to allow wildcards
$path = '/' . trim(str_replace('*', '(\S)*', $path), '/ ');
$currentPath = '/' . trim(uri_string(), '/ ');
return (bool) preg_match("|^{$path}$|", $currentPath, $matches);
}
}
@@ -0,0 +1,61 @@
<?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.
*/
// CodeIgniter XML Helpers
if (! function_exists('xml_convert')) {
/**
* Convert Reserved XML characters to Entities
*/
function xml_convert(string $str, bool $protectAll = false): string
{
$temp = '__TEMP_AMPERSANDS__';
// Replace entities to temporary markers so that
// ampersands won't get messed up
$str = preg_replace('/&#(\d+);/', $temp . '\\1;', $str);
if ($protectAll) {
$str = preg_replace('/&(\w+);/', $temp . '\\1;', $str);
}
$original = [
'&',
'<',
'>',
'"',
"'",
'-',
];
$replacement = [
'&amp;',
'&lt;',
'&gt;',
'&quot;',
'&apos;',
'&#45;',
];
$str = str_replace($original, $replacement, $str);
// Decode the temp markers back to entities
$str = preg_replace('/' . $temp . '(\d+);/', '&#\\1;', $str);
if ($protectAll) {
return preg_replace('/' . $temp . '(\w+);/', '&\\1;', $str);
}
return $str;
}
}