diff --git a/application/config/constants.php b/application/config/constants.php index 565b337b..ca8ec0ee 100644 --- a/application/config/constants.php +++ b/application/config/constants.php @@ -89,18 +89,3 @@ defined('EXIT_USER_INPUT') || define('EXIT_USER_INPUT', 7); // invalid u defined('EXIT_DATABASE') || define('EXIT_DATABASE', 8); // database error defined('EXIT__AUTO_MIN') || define('EXIT__AUTO_MIN', 9); // lowest automatically-assigned error code defined('EXIT__AUTO_MAX') || define('EXIT__AUTO_MAX', 125); // highest automatically-assigned error code - -/** - * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_LOW instead. - */ -define('EVENT_PRIORITY_LOW', 200); - -/** - * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_NORMAL instead. - */ -define('EVENT_PRIORITY_NORMAL', 100); - -/** - * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_HIGH instead. - */ -define('EVENT_PRIORITY_HIGH', 10); diff --git a/application/modules/install/controllers/Install.php b/application/modules/install/controllers/Install.php index 6538999c..95cbf559 100644 --- a/application/modules/install/controllers/Install.php +++ b/application/modules/install/controllers/Install.php @@ -179,6 +179,13 @@ class Database extends Config "strictOn" => false, "failover" => [], "port" => '.(int)$_POST['cms_port'].', + "numberNative" => false, + "foundRows" => false, + "dateFormat" => [ + "date" => "Y-m-d", + "datetime" => "Y-m-d H:i:s", + "time" => "H:i:s, + ], ]; public array $account = [ @@ -199,6 +206,13 @@ class Database extends Config "strictOn" => false, "failover" => [], "port" => '.(int)$_POST['auth_port'].', + "numberNative" => false, + "foundRows" => false, + "dateFormat" => [ + "date" => "Y-m-d", + "datetime" => "Y-m-d H:i:s", + "time" => "H:i:s, + ], ]; } '; @@ -450,6 +464,13 @@ class Database extends Config 'compress' => false, 'strictOn' => false, 'failover' => [], + 'numberNative' => false, + 'foundRows' => false, + 'dateFormat' => [ + 'date' => 'Y-m-d', + 'datetime' => 'Y-m-d H:i:s', + 'time' => 'H:i:s', + ], ]; $worldConfig = [ @@ -470,6 +491,13 @@ class Database extends Config 'compress' => false, 'strictOn' => false, 'failover' => [], + 'numberNative' => false, + 'foundRows' => false, + 'dateFormat' => [ + 'date' => 'Y-m-d', + 'datetime' => 'Y-m-d H:i:s', + 'time' => 'H:i:s', + ], ]; // Connect to characters diff --git a/index.php b/index.php index 0cae39d5..cde9a43e 100644 --- a/index.php +++ b/index.php @@ -21,7 +21,7 @@ if (version_compare(PHP_VERSION, $minPhpVersion, '<')) { $message = sprintf( 'Your PHP version must be %s or higher to run FusionCMS. Current version: %s', $minPhpVersion, - PHP_VERSION + PHP_VERSION, ); header('HTTP/1.1 503 Service Unavailable.', true, 503); diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index bf008bdc..fab55ae6 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -298,7 +298,7 @@ class BaseBuilder /** * Constructor * - * @param array|string $tableName tablename or tablenames with or without aliases + * @param array|string|TableName $tableName tablename or tablenames with or without aliases * * Examples of $tableName: `mytable`, `jobs j`, `jobs j, users u`, `['jobs j','users u']` * @@ -315,15 +315,20 @@ class BaseBuilder */ $this->db = $db; + if ($tableName instanceof TableName) { + $this->tableName = $tableName->getTableName(); + $this->QBFrom[] = $this->db->escapeIdentifier($tableName); + $this->db->addTableAlias($tableName->getAlias()); + } // If it contains `,`, it has multiple tables - if (is_string($tableName) && strpos($tableName, ',') === false) { + elseif (is_string($tableName) && strpos($tableName, ',') === false) { $this->tableName = $tableName; // @TODO remove alias if exists + $this->from($tableName); } else { $this->tableName = ''; + $this->from($tableName); } - $this->from($tableName); - if ($options !== null && $options !== []) { foreach ($options as $key => $value) { if (property_exists($this, $key)) { @@ -3008,7 +3013,7 @@ class BaseBuilder * * @param array|string $table The table to inspect * - * @return string|void + * @return string|null */ protected function trackAliases($table) { @@ -3017,26 +3022,28 @@ class BaseBuilder $this->trackAliases($t); } - return; + return null; } // Does the string contain a comma? If so, we need to separate // the string into discreet statements - if (strpos($table, ',') !== false) { + if (str_contains($table, ',')) { return $this->trackAliases(explode(',', $table)); } // if a table alias is used we can recognize it by a space - if (strpos($table, ' ') !== false) { + if (str_contains($table, ' ')) { // if the alias is written with the AS keyword, remove it $table = preg_replace('/\s+AS\s+/i', ' ', $table); // Grab the alias - $table = trim(strrchr($table, ' ')); + $alias = trim(strrchr($table, ' ')); // Store the alias, if it doesn't already exist - $this->db->addTableAlias($table); + $this->db->addTableAlias($alias); } + + return null; } /** diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 7110205e..3e9a9819 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -338,7 +338,7 @@ abstract class BaseConnection implements ConnectionInterface /** * Array of table aliases. * - * @var array + * @var list */ protected $aliasedTables = []; @@ -574,10 +574,14 @@ abstract class BaseConnection implements ConnectionInterface * * @return $this */ - public function addTableAlias(string $table) + public function addTableAlias(string $alias) { - if (! in_array($table, $this->aliasedTables, true)) { - $this->aliasedTables[] = $table; + if ($alias === '') { + return $this; + } + + if (! in_array($alias, $this->aliasedTables, true)) { + $this->aliasedTables[] = $alias; } return $this; @@ -823,6 +827,16 @@ abstract class BaseConnection implements ConnectionInterface return $this->transStatus; } + /** + * Reset transaction status - to restart transactions after strict mode failure + */ + public function resetTransStatus(): static + { + $this->transStatus = true; + + return $this; + } + /** * Begin Transaction */ @@ -913,7 +927,7 @@ abstract class BaseConnection implements ConnectionInterface /** * Returns a non-shared new instance of the query builder for this connection. * - * @param array|string $tableName + * @param array|string|TableName $tableName * * @return BaseBuilder * @@ -1045,9 +1059,10 @@ abstract class BaseConnection implements ConnectionInterface * the correct identifiers. * * @param array|int|string $item - * @param bool $prefixSingle Prefix a table name with no segments? - * @param bool $protectIdentifiers Protect table or column names? - * @param bool $fieldExists Supplied $item contains a column name? + * @param array|int|string|TableName $item + * @param bool $prefixSingle Prefix a table name with no segments? + * @param bool $protectIdentifiers Protect table or column names? + * @param bool $fieldExists Supplied $item contains a column name? * * @return array|string * @phpstan-return ($item is array ? array : string) @@ -1068,6 +1083,11 @@ abstract class BaseConnection implements ConnectionInterface return $escapedArray; } + if ($item instanceof TableName) { + /** @psalm-suppress NoValue I don't know why ERROR. */ + return $this->escapeTableName($item); + } + // If you pass `['column1', 'column2']`, `$item` will be int because the array keys are int. $item = (string) $item; @@ -1135,6 +1155,42 @@ abstract class BaseConnection implements ConnectionInterface return $item . $alias; } + /** + * Escape the SQL Identifier + * + * This function escapes single identifier. + * + * @param non-empty-string|TableName $item + */ + public function escapeIdentifier($item): string + { + if ($item === '') { + return ''; + } + + if ($item instanceof TableName) { + return $this->escapeTableName($item); + } + + return $this->escapeChar + . str_replace( + $this->escapeChar, + $this->escapeChar . $this->escapeChar, + $item, + ) + . $this->escapeChar; + } + + /** + * Returns escaped table name with alias. + */ + private function escapeTableName(TableName $tableName): string + { + $alias = $tableName->getAlias(); + + return $this->escapeIdentifier($tableName->getActualTableName()) + . (($alias !== '') ? ' ' . $this->escapeIdentifier($alias) : ''); + } private function protectDotItem(string $item, string $alias, bool $protectIdentifiers, bool $fieldExists): string { @@ -1518,12 +1574,16 @@ abstract class BaseConnection implements ConnectionInterface /** * Fetch Field Names * + * @param string|TableName $tableName + * * @return false|list * * @throws DatabaseException */ - public function getFieldNames(string $table) + public function getFieldNames($tableName) { + $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName; + // Is there a cached result? if (isset($this->dataCache['field_names'][$table])) { return $this->dataCache['field_names'][$table]; @@ -1533,7 +1593,7 @@ abstract class BaseConnection implements ConnectionInterface $this->initialize(); } - if (false === ($sql = $this->_listColumns($table))) { + if (false === ($sql = $this->_listColumns($tableName))) { if ($this->DBDebug) { throw new DatabaseException('This feature is not available for the database you are using.'); } @@ -1747,9 +1807,11 @@ abstract class BaseConnection implements ConnectionInterface /** * Generates a platform-specific query string so that the column names can be fetched. * + * @param string|TableName $table + * * @return false|string */ - abstract protected function _listColumns(string $table = ''); + abstract protected function _listColumns($table = ''); /** * Platform-specific field data information. diff --git a/system/Database/MySQLi/Connection.php b/system/Database/MySQLi/Connection.php index ea148a27..d75fc83e 100644 --- a/system/Database/MySQLi/Connection.php +++ b/system/Database/MySQLi/Connection.php @@ -15,6 +15,7 @@ namespace CodeIgniter\Database\MySQLi; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\TableName; use LogicException; use mysqli; use mysqli_result; @@ -81,6 +82,16 @@ class Connection extends BaseConnection */ public $numberNative = false; + /** + * Use MYSQLI_CLIENT_FOUND_ROWS + * + * Whether affectedRows() should return number of rows found, + * or number of rows changed, after an UPDATE query. + * + * @var bool + */ + public $foundRows = false; + /** * Connect to the database. * @@ -182,6 +193,10 @@ class Connection extends BaseConnection $clientFlags += MYSQLI_CLIENT_SSL; } + if ($this->foundRows) { + $clientFlags += MYSQLI_CLIENT_FOUND_ROWS; + } + try { if ($this->mysqli->real_connect( $hostname, @@ -408,10 +423,19 @@ class Connection extends BaseConnection /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { - return 'SHOW COLUMNS FROM ' . $this->protectIdentifiers($table, true, null, false); + $tableName = $this->protectIdentifiers( + $table, + true, + null, + false, + ); + + return 'SHOW COLUMNS FROM ' . $tableName; } /** diff --git a/system/Database/OCI8/Connection.php b/system/Database/OCI8/Connection.php index 071703de..f95d0af7 100644 --- a/system/Database/OCI8/Connection.php +++ b/system/Database/OCI8/Connection.php @@ -16,6 +16,7 @@ namespace CodeIgniter\Database\OCI8; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Query; +use CodeIgniter\Database\TableName; use ErrorException; use stdClass; @@ -285,17 +286,22 @@ class Connection extends BaseConnection /** * Generates a platform-specific query string so that the column names can be fetched. */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { - if (strpos($table, '.') !== false) { - sscanf($table, '%[^.].%s', $owner, $table); + if ($table instanceof TableName) { + $tableName = $this->escape(strtoupper($table->getActualTableName())); + $owner = $this->username; + } elseif (str_contains($table, '.')) { + sscanf($table, '%[^.].%s', $owner, $tableName); + $tableName = $this->escape(strtoupper($this->DBPrefix . $tableName)); } else { - $owner = $this->username; + $owner = $this->username; + $tableName = $this->escape(strtoupper($this->DBPrefix . $table)); } return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . ' - AND UPPER(TABLE_NAME) = ' . $this->escape(strtoupper($this->DBPrefix . $table)); + AND UPPER(TABLE_NAME) = ' . $tableName; } /** diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index 00abbee4..a80cc35d 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -16,6 +16,7 @@ namespace CodeIgniter\Database\Postgre; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\RawSql; +use CodeIgniter\Database\TableName; use ErrorException; use PgSql\Connection as PgSqlConnection; use PgSql\Result as PgSqlResult; @@ -302,13 +303,20 @@ class Connection extends BaseConnection /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { + if ($table instanceof TableName) { + $tableName = $this->escape($table->getActualTableName()); + } else { + $tableName = $this->escape($this->DBPrefix . strtolower($table)); + } + return 'SELECT "column_name" FROM "information_schema"."columns" - WHERE LOWER("table_name") = ' - . $this->escape($this->DBPrefix . strtolower($table)) + WHERE LOWER("table_name") = ' . $tableName . ' ORDER BY "ordinal_position"'; } diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index f1f219fb..eac75055 100644 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -15,6 +15,7 @@ namespace CodeIgniter\Database\SQLSRV; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\TableName; use stdClass; /** @@ -225,12 +226,20 @@ class Connection extends BaseConnection /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { + if ($table instanceof TableName) { + $tableName = $this->escape(strtolower($table->getActualTableName())); + } else { + $tableName = $this->escape($this->DBPrefix . strtolower($table)); + } + return 'SELECT [COLUMN_NAME] ' . ' FROM [INFORMATION_SCHEMA].[COLUMNS]' - . ' WHERE [TABLE_NAME] = ' . $this->escape($this->DBPrefix . $table) + . ' WHERE [TABLE_NAME] = ' . $tableName . ' AND [TABLE_SCHEMA] = ' . $this->escape($this->schema); } diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index 3c22a42d..fd1991c6 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -15,6 +15,7 @@ namespace CodeIgniter\Database\SQLite3; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; +use CodeIgniter\Database\TableName; use Exception; use SQLite3; use SQLite3Result; @@ -55,6 +56,15 @@ class Connection extends BaseConnection */ protected $busyTimeout; + /** + * The setting of the "synchronous" flag + * + * @var int<0, 3>|null flag + * + * @see https://www.sqlite.org/pragma.html#pragma_synchronous + */ + protected ?int $synchronous = null; + /** * @return void */ @@ -69,6 +79,13 @@ class Connection extends BaseConnection if (is_int($this->busyTimeout)) { $this->connID->busyTimeout($this->busyTimeout); } + + if (is_int($this->synchronous)) { + if (! in_array($this->synchronous, [0, 1, 2, 3], true)) { + throw new InvalidArgumentException('Invalid synchronous value.'); + } + $this->connID->exec('PRAGMA synchronous = ' . $this->synchronous); + } } /** @@ -209,19 +226,31 @@ class Connection extends BaseConnection /** * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string|TableName $table */ - protected function _listColumns(string $table = ''): string + protected function _listColumns($table = ''): string { - return 'PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')'; + if ($table instanceof TableName) { + $tableName = $this->escapeIdentifier($table); + } else { + $tableName = $this->protectIdentifiers($table, true, null, false); + } + + return 'PRAGMA TABLE_INFO(' . $tableName . ')'; } /** + * @param string|TableName $tableName + * * @return false|list * * @throws DatabaseException */ - public function getFieldNames(string $table) + public function getFieldNames($tableName) { + $table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName; + // Is there a cached result? if (isset($this->dataCache['field_names'][$table])) { return $this->dataCache['field_names'][$table]; @@ -231,7 +260,7 @@ class Connection extends BaseConnection $this->initialize(); } - $sql = $this->_listColumns($table); + $sql = $this->_listColumns($tableName); $query = $this->query($sql); $this->dataCache['field_names'][$table] = []; diff --git a/system/Database/TableName.php b/system/Database/TableName.php new file mode 100644 index 00000000..86527226 --- /dev/null +++ b/system/Database/TableName.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace CodeIgniter\Database; + +/** + * Represents a table name in SQL. + * + * @interal + * + * @see \CodeIgniter\Database\TableNameTest + */ +class TableName +{ + /** + * @param string $actualTable Actual table name + * @param string $logicalTable Logical table name (w/o DB prefix) + * @param string $schema Schema name + * @param string $database Database name + * @param string $alias Alias name + */ + protected function __construct( + private readonly string $actualTable, + private readonly string $logicalTable = '', + private readonly string $schema = '', + private readonly string $database = '', + private readonly string $alias = '', + ) { + } + + /** + * Creates a new instance. + * + * @param string $table Table name (w/o DB prefix) + * @param string $alias Alias name + */ + public static function create(string $dbPrefix, string $table, string $alias = ''): self + { + return new self( + $dbPrefix . $table, + $table, + '', + '', + $alias, + ); + } + + /** + * Creates a new instance from an actual table name. + * + * @param string $actualTable Actual table name with DB prefix + * @param string $alias Alias name + */ + public static function fromActualName(string $dbPrefix, string $actualTable, string $alias = ''): self + { + $prefix = $dbPrefix; + $logicalTable = ''; + + if (str_starts_with($actualTable, $prefix)) { + $logicalTable = substr($actualTable, strlen($prefix)); + } + + return new self( + $actualTable, + $logicalTable, + '', + $alias, + ); + } + + /** + * Creates a new instance from full name. + * + * @param string $table Table name (w/o DB prefix) + * @param string $schema Schema name + * @param string $database Database name + * @param string $alias Alias name + */ + public static function fromFullName( + string $dbPrefix, + string $table, + string $schema = '', + string $database = '', + string $alias = '', + ): self { + return new self( + $dbPrefix . $table, + $table, + $schema, + $database, + $alias, + ); + } + + /** + * Returns the single segment table name w/o DB prefix. + */ + public function getTableName(): string + { + return $this->logicalTable; + } + + /** + * Returns the actual single segment table name w/z DB prefix. + */ + public function getActualTableName(): string + { + return $this->actualTable; + } + + public function getAlias(): string + { + return $this->alias; + } + + public function getSchema(): string + { + return $this->schema; + } + + public function getDatabase(): string + { + return $this->database; + } +} \ No newline at end of file diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php index 53b23cea..a85e4010 100644 --- a/system/HTTP/CURLRequest.php +++ b/system/HTTP/CURLRequest.php @@ -651,6 +651,15 @@ class CURLRequest extends OutgoingRequest $this->setHeader('Content-Length', (string) strlen($json)); } + // Resolve IP + if (array_key_exists('force_ip_resolve', $config)) { + $curlOptions[CURLOPT_IPRESOLVE] = match ($config['force_ip_resolve']) { + 'v4' => CURL_IPRESOLVE_V4, + 'v6' => CURL_IPRESOLVE_V6, + default => CURL_IPRESOLVE_WHATEVER, + }; + } + // version if (! empty($config['version'])) { $version = sprintf('%.1F', $config['version']); @@ -660,6 +669,12 @@ class CURLRequest extends OutgoingRequest $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; } elseif ($version === '2.0') { $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_2_0; + } elseif ($version === '3.0') { + if (! defined('CURL_HTTP_VERSION_3')) { + define('CURL_HTTP_VERSION_3', 30); + } + + $curlOptions[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_3; } } diff --git a/system/I18n/TimeLegacy.php b/system/I18n/TimeLegacy.php index 35523e32..51d1d8a7 100644 --- a/system/I18n/TimeLegacy.php +++ b/system/I18n/TimeLegacy.php @@ -12,6 +12,8 @@ namespace CodeIgniter\I18n; use DateTime; +use Exception; +use ReturnTypeWillChange; /** * Legacy Time class. @@ -37,10 +39,29 @@ use DateTime; * @property string $weekOfYear read-only * @property string $year read-only * + * @phpstan-consistent-constructor + * * @deprecated Use Time instead. * @see \CodeIgniter\I18n\TimeLegacyTest */ class TimeLegacy extends DateTime { use TimeTrait; + + /** + * Returns a new instance with the date set to the new timestamp. + * + * @param int $timestamp + * + * @return static + * + * @throws Exception + */ + #[ReturnTypeWillChange] + public function setTimestamp($timestamp) + { + $time = date('Y-m-d H:i:s', $timestamp); + + return static::parse($time, $this->timezone, $this->locale); + } } diff --git a/system/helpers/text_helper.php b/system/helpers/text_helper.php index c9753525..b43e1654 100644 --- a/system/helpers/text_helper.php +++ b/system/helpers/text_helper.php @@ -53,35 +53,40 @@ if (!function_exists('character_limiter')) { /** * Character Limiter * - * Limits the string based on the character count. Preserves complete words + * 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 $str, int $n = 500, string $endChar = '…'): string + function character_limiter(string $string, int $limit = 500, string $endChar = '…'): string { - if (mb_strlen($str) < $n) { - return $str; + if (mb_strlen($string) < $limit) { + return $string; } // a bit complicated, but faster than preg_replace with \s+ - $str = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $str)); + $string = preg_replace('/ {2,}/', ' ', str_replace(["\r", "\n", "\t", "\x0B", "\x0C"], ' ', $string)); + $stringLength = mb_strlen($string); - if (mb_strlen($str) <= $n) { - return $str; + if ($stringLength <= $limit) { + return $string; } - $out = ''; + $output = ''; + $outputLength = 0; + $words = explode(' ', trim($string)); - foreach (explode(' ', trim($str)) as $val) { - $out .= $val . ' '; - if (mb_strlen($out) >= $n) { - $out = trim($out); + foreach ($words as $word) { + $output .= $word . ' '; + $outputLength = mb_strlen($output); + + if ($outputLength >= $limit) { + $output = trim($output); break; } } - return (mb_strlen($out) === mb_strlen($str)) ? $out : $out . $endChar; + return ($outputLength === $stringLength) ? $output : $output . $endChar; } } @@ -729,38 +734,44 @@ if (!function_exists('excerpt')) { function excerpt(string $text, ?string $phrase = null, int $radius = 100, string $ellipsis = '...'): string { if (isset($phrase)) { - $phrasePos = stripos($text, $phrase); - $phraseLen = strlen($phrase); + $phrasePosition = mb_stripos($text, $phrase); + $phraseLength = mb_strlen($phrase); } else { - $phrasePos = $radius / 2; - $phraseLen = 1; + $phrasePosition = $radius / 2; + $phraseLength = 1; } - $pre = explode(' ', substr($text, 0, $phrasePos)); - $pos = explode(' ', substr($text, $phrasePos + $phraseLen)); + $beforeWords = explode(' ', mb_substr($text, 0, $phrasePosition)); + $afterWords = explode(' ', mb_substr($text, $phrasePosition + $phraseLength)); - $prev = ' '; - $post = ' '; - $count = 0; + $firstPartOutput = ' '; + $endPartOutput = ' '; + $count = 0; - foreach (array_reverse($pre) as $e) { - if ((strlen($e) + $count + 1) < $radius) { - $prev = ' ' . $e . $prev; + foreach (array_reverse($beforeWords) as $beforeWord) { + $beforeWordLength = mb_strlen($beforeWord); + + if (($beforeWordLength + $count + 1) < $radius) { + $firstPartOutput = ' ' . $beforeWord . $firstPartOutput; } - $count = ++$count + strlen($e); + + $count = ++$count + $beforeWordLength; } $count = 0; - foreach ($pos as $s) { - if ((strlen($s) + $count + 1) < $radius) { - $post .= $s . ' '; + foreach ($afterWords as $afterWord) { + $afterWordLength = mb_strlen($afterWord); + + if (($afterWordLength + $count + 1) < $radius) { + $endPartOutput .= $afterWord . ' '; } - $count = ++$count + strlen($s); + + $count = ++$count + $afterWordLength; } $ellPre = $phrase !== null ? $ellipsis : ''; - return str_replace(' ', ' ', $ellPre . $prev . $phrase . $post . $ellipsis); + return str_replace(' ', ' ', $ellPre . $firstPartOutput . $phrase . $endPartOutput . $ellipsis); } }