From cf6fe335a12049eb4ab342b10bd54f0fab4eb862 Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Fri, 2 Oct 2026 21:58:25 +0600 Subject: [PATCH 1/6] fix: prevent infinite loop in word_wrap() with a character limit below 2 --- system/Helpers/text_helper.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index cf9459100dfb..1efb346f3348 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -345,6 +345,9 @@ function convert_accented_characters(string $str): string */ function word_wrap(string $str, int $charlim = 76): string { + // A limit below 1 is meaningless and would make the wrapping loop endless + $charlim = max(1, $charlim); + // Reduce multiple spaces $str = preg_replace('| +|', ' ', $str); @@ -388,9 +391,11 @@ function word_wrap(string $str, int $charlim = 76): string if (preg_match('!\[url.+\]|://|www\.!', $line)) { break; } - // Trim the word down - $temp .= mb_substr($line, 0, $charlim - 1); - $line = mb_substr($line, $charlim - 1); + // Trim the word down. Always take at least one character, + // otherwise a limit of 1 or less would never make progress. + $chunkLength = max(1, $charlim - 1); + $temp .= mb_substr($line, 0, $chunkLength); + $line = mb_substr($line, $chunkLength); } // If $temp contains data it means we had to split up an over-length From 35428183bb105381a1e01ae356ab43c020369812 Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Fri, 2 Oct 2026 21:59:39 +0600 Subject: [PATCH 2/6] test: add regression tests for word_wrap() with small character limits --- tests/system/Helpers/TextHelperTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index a4702db4b604..1ca8ef82a7d6 100644 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -358,6 +358,27 @@ public function testEllipsize(): void } } + /** + * A character limit below 2 used to make word_wrap() loop forever. + */ + #[DataProvider('provideWordWrapSmallCharLimit')] + public function testWordWrapSmallCharLimit(int $charlim): void + { + $this->assertSame("aaa\na\nbbb\nb", word_wrap('aaaa bbbb', $charlim)); + } + + /** + * @return iterable + */ + public static function provideWordWrapSmallCharLimit(): iterable + { + yield 'one' => [1]; + + yield 'zero' => [0]; + + yield 'negative' => [-5]; + } + public function testWordWrap(): void { $string = 'Here is a simple string of text that will help us demonstrate this function.'; From 9cd9ad343dfccbd71045112b47db31d95c479a71 Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Mon, 5 Oct 2026 21:58:22 +0600 Subject: [PATCH 3/6] fix: wrap word_wrap() at one character when charlim is 1 --- system/Helpers/text_helper.php | 8 ++++++-- tests/system/Helpers/TextHelperTest.php | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 1efb346f3348..cf39c336d094 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -386,6 +386,10 @@ function word_wrap(string $str, int $charlim = 76): string $temp = ''; + // Chunks are joined directly for wider limits, but with a limit of 1 + // each chunk is a full line of its own. + $separator = $charlim === 1 ? "\n" : ''; + while (mb_strlen($line) > $charlim) { // If the over-length word is a URL we won't wrap it if (preg_match('!\[url.+\]|://|www\.!', $line)) { @@ -394,14 +398,14 @@ function word_wrap(string $str, int $charlim = 76): string // Trim the word down. Always take at least one character, // otherwise a limit of 1 or less would never make progress. $chunkLength = max(1, $charlim - 1); - $temp .= mb_substr($line, 0, $chunkLength); + $temp .= mb_substr($line, 0, $chunkLength) . $separator; $line = mb_substr($line, $chunkLength); } // 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"; + $output .= $temp . ($separator === '' ? "\n" : '') . $line . "\n"; } else { $output .= $line . "\n"; } diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index 1ca8ef82a7d6..d55e6e254534 100644 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -364,7 +364,7 @@ public function testEllipsize(): void #[DataProvider('provideWordWrapSmallCharLimit')] public function testWordWrapSmallCharLimit(int $charlim): void { - $this->assertSame("aaa\na\nbbb\nb", word_wrap('aaaa bbbb', $charlim)); + $this->assertSame("a\na\na\na\nb\nb\nb\nb", word_wrap('aaaa bbbb', $charlim)); } /** From 2f0b2237f761fb67a34970634cc59f0f6569cfda Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Mon, 5 Oct 2026 22:29:47 +0600 Subject: [PATCH 4/6] fix: keep long words whole in word_wrap() when charlim is 1 --- system/Helpers/text_helper.php | 18 +++++++----------- tests/system/Helpers/TextHelperTest.php | 2 +- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index cf39c336d094..67ae1794b89f 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -386,26 +386,22 @@ function word_wrap(string $str, int $charlim = 76): string $temp = ''; - // Chunks are joined directly for wider limits, but with a limit of 1 - // each chunk is a full line of its own. - $separator = $charlim === 1 ? "\n" : ''; - - while (mb_strlen($line) > $charlim) { + // A limit of 1 can't be used to split a word (each piece needs room for + // the continuation), so the word is kept whole, like a long URL. + while ($charlim > 1 && 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. Always take at least one character, - // otherwise a limit of 1 or less would never make progress. - $chunkLength = max(1, $charlim - 1); - $temp .= mb_substr($line, 0, $chunkLength) . $separator; - $line = mb_substr($line, $chunkLength); + // 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 . ($separator === '' ? "\n" : '') . $line . "\n"; + $output .= $temp . "\n" . $line . "\n"; } else { $output .= $line . "\n"; } diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php index d55e6e254534..88878bcf8294 100644 --- a/tests/system/Helpers/TextHelperTest.php +++ b/tests/system/Helpers/TextHelperTest.php @@ -364,7 +364,7 @@ public function testEllipsize(): void #[DataProvider('provideWordWrapSmallCharLimit')] public function testWordWrapSmallCharLimit(int $charlim): void { - $this->assertSame("a\na\na\na\nb\nb\nb\nb", word_wrap('aaaa bbbb', $charlim)); + $this->assertSame("aaaa\nbbbb", word_wrap('aaaa bbbb', $charlim)); } /** From 2094fef43efe6c7293cbe31a1b515f0d42dbf153 Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Tue, 6 Oct 2026 16:41:30 +0600 Subject: [PATCH 5/6] refactor: drop redundant charlim clamp in word_wrap() --- system/Helpers/text_helper.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php index 67ae1794b89f..09d14abbe2ee 100644 --- a/system/Helpers/text_helper.php +++ b/system/Helpers/text_helper.php @@ -345,9 +345,6 @@ function convert_accented_characters(string $str): string */ function word_wrap(string $str, int $charlim = 76): string { - // A limit below 1 is meaningless and would make the wrapping loop endless - $charlim = max(1, $charlim); - // Reduce multiple spaces $str = preg_replace('| +|', ' ', $str); From d19a0d53e23cb2c62f94a3362ff52c722f6a1b32 Mon Sep 17 00:00:00 2001 From: MD Ali Kadar Date: Tue, 6 Oct 2026 16:42:17 +0600 Subject: [PATCH 6/6] docs: add changelog entry for word_wrap() small charlim fix --- user_guide_src/source/changelogs/v4.7.5.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index deabaf4b7ab4..492db76c20c7 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -57,6 +57,7 @@ Bugs Fixed - **Debug:** Fixed a bug where ``Timer::start()`` treated ``0.0`` as an empty value and substituted the current time. - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. +- **Helpers:** Fixed a bug where ``word_wrap()`` entered an infinite loop when the character limit was below 2. Over-length words are now kept whole in that case. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). - **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors.