diff --git a/src/Util/Strings.php b/src/Util/Strings.php index c520b54b9b..3dd91193d2 100644 --- a/src/Util/Strings.php +++ b/src/Util/Strings.php @@ -420,4 +420,42 @@ class Strings return $pathItem; } + + /** + * Multi-byte safe implementation of substr_replace where $start and $length are character offset and count rather + * than byte offset and counts. + * + * Depends on mbstring, use default encoding. + * + * @param string $string + * @param string $replacement + * @param int $start + * @param int|null $length + * @return string + * @see substr_replace() + */ + public static function substringReplace(string $string, string $replacement, int $start, int $length = null) + { + $string_length = mb_strlen($string); + + $length = $length ?? $string_length; + + if ($start < 0) { + $start = max(0, $string_length + $start); + } else if ($start > $string_length) { + $start = $string_length; + } + + if ($length < 0) { + $length = max(0, $string_length - $start + $length); + } else if ($length > $string_length) { + $length = $string_length; + } + + if (($start + $length) > $string_length) { + $length = $string_length - $start; + } + + return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length, $string_length - $start - $length); + } } diff --git a/tests/src/Util/StringsTest.php b/tests/src/Util/StringsTest.php index 86280cc5f1..66fb2f0f53 100644 --- a/tests/src/Util/StringsTest.php +++ b/tests/src/Util/StringsTest.php @@ -134,4 +134,64 @@ class StringsTest extends TestCase { $this->assertEquals($valid, Strings::isHex($input)); } + + /** + * Tests that Strings::substringReplace behaves the same as substr_replace with ASCII strings in all the possible + * numerical parameter configurations (positive, negative, zero, out of bounds either side, null) + */ + public function testSubstringReplaceASCII() + { + for ($start = -10; $start <= 10; $start += 5) { + $this->assertEquals( + substr_replace('string', 'replacement', $start), + Strings::substringReplace('string', 'replacement', $start) + ); + + for ($length = -10; $length <= 10; $length += 5) { + $this->assertEquals( + substr_replace('string', 'replacement', $start, $length), + Strings::substringReplace('string', 'replacement', $start, $length) + ); + } + } + } + + + public function dataSubstringReplaceMultiByte() + { + return [ + 'issue-8470' => [ + 'expected' => 'Je n’y pense que maintenant (pask ma sonnette ne fonctionne pas) : mettre un gentil mot avec mes coordonnées sur ma porte est le moyen le plus simple de rester en contact si besoin avec mon voisinage direct ! [url=https://www.instagram.com/p/B-UdH2loee1/?igshid=x4aglyju9kva]instagram.com/p/B-UdH2loee1/…[/url] [rest of the post]', + 'string' => 'Je n’y pense que maintenant (pask ma sonnette ne fonctionne pas) : mettre un gentil mot avec mes coordonnées sur ma porte est le moyen le plus simple de rester en contact si besoin avec mon voisinage direct ! https://t.co/YoBWTHsAAk [rest of the post]', + 'replacement' => '[url=https://www.instagram.com/p/B-UdH2loee1/?igshid=x4aglyju9kva]instagram.com/p/B-UdH2loee1/…[/url]', + 'start' => 209, + 'length' => 23, + ], + ]; + } + + /** + * Tests cases where Strings::substringReplace is needed over substr_replace with multi-byte strings and character + * offsets + * + * @param string $expected + * @param string $string + * @param string $replacement + * @param int $start + * @param int|null $length + * + * @dataProvider dataSubstringReplaceMultiByte + */ + public function testSubstringReplaceMultiByte(string $expected, string $string, string $replacement, int $start, int $length = null) + { + $this->assertEquals( + $expected, + Strings::substringReplace( + $string, + $replacement, + $start, + $length + ) + ); + } }