From f894d33c464e28a15cbdcd5c72dfd90bd5379e92 Mon Sep 17 00:00:00 2001 From: Nikita Kiselev Date: Sat, 14 Mar 2026 15:36:09 +0300 Subject: [PATCH] fix: normalize order response to valid UTF-8 before JSON encoding (#83) * fix: normalize cart json utf8 and add tests Ensure cart and checkout responses are cleaned to valid UTF-8 before JSON encoding and cover Utf8Cleaner with unit tests. Made-with: Cursor * fix: normalize order response to valid UTF-8 before JSON encoding - Add Utf8Cleaner::clean() for successful order creation response (data) - Add Utf8Cleaner::clean() for validation error response (ErrorBag) - Prevents Malformed UTF-8 characters error when creating order with non-UTF-8 data from OpenCart/DB (same approach as cart endpoints) Made-with: Cursor --- .../src/Handlers/CartHandler.php | 5 +- .../src/Handlers/OrderHandler.php | 5 +- .../src/Support/Utf8Cleaner.php | 49 ++++++++++ .../tests/Unit/Support/Utf8CleanerTest.php | 90 +++++++++++++++++++ 4 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 module/oc_telegram_shop/upload/oc_telegram_shop/src/Support/Utf8Cleaner.php create mode 100644 module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/Support/Utf8CleanerTest.php diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/CartHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/CartHandler.php index 8882509d..4b6f111a 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/CartHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/CartHandler.php @@ -3,6 +3,7 @@ namespace App\Handlers; use App\Services\CartService; +use App\Support\Utf8Cleaner; use Cart\Cart; use Symfony\Component\HttpFoundation\JsonResponse; use Openguru\OpenCartFramework\Http\Request; @@ -23,7 +24,7 @@ class CartHandler $items = $this->cartService->getCart(); return new JsonResponse([ - 'data' => $items, + 'data' => Utf8Cleaner::clean($items), ]); } @@ -48,7 +49,7 @@ class CartHandler } return new JsonResponse([ - 'data' => $items, + 'data' => Utf8Cleaner::clean($items), ]); } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/OrderHandler.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/OrderHandler.php index f09214dc..6bf2a0d1 100755 --- a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/OrderHandler.php +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Handlers/OrderHandler.php @@ -4,6 +4,7 @@ namespace App\Handlers; use App\Exceptions\OrderValidationFailedException; use App\Services\OrderCreateService; +use App\Support\Utf8Cleaner; use Symfony\Component\HttpFoundation\JsonResponse; use Openguru\OpenCartFramework\Http\Request; use Symfony\Component\HttpFoundation\Response; @@ -26,11 +27,11 @@ class OrderHandler ]); return new JsonResponse([ - 'data' => $order, + 'data' => Utf8Cleaner::clean($order), ], Response::HTTP_CREATED); } catch (OrderValidationFailedException $exception) { return new JsonResponse([ - 'data' => $exception->getErrorBag()->firstOfAll(), + 'data' => Utf8Cleaner::clean($exception->getErrorBag()->firstOfAll()), ], Response::HTTP_UNPROCESSABLE_ENTITY); } } diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/src/Support/Utf8Cleaner.php b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Support/Utf8Cleaner.php new file mode 100644 index 00000000..70551354 --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/src/Support/Utf8Cleaner.php @@ -0,0 +1,49 @@ + $item) { + $value[$key] = self::clean($item); + } + + return $value; + } + + if (is_object($value)) { + foreach (get_object_vars($value) as $property => $item) { + $value->{$property} = self::clean($item); + } + + return $value; + } + + if (is_string($value)) { + // Попробовать нормализовать строку в UTF-8, подменяя невалидные последовательности. + $converted = @mb_convert_encoding($value, 'UTF-8', 'UTF-8'); + + if ($converted === false) { + // Фолбэк: заменить невалидные байты стандартным алгоритмом json_encode. + $encoded = json_encode($value, JSON_INVALID_UTF8_SUBSTITUTE); + + if ($encoded === false) { + return ''; + } + + // Убираем кавычки, которые добавляет json_encode. + return json_decode($encoded, true) ?? ''; + } + + return $converted; + } + + return $value; + } +} diff --git a/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/Support/Utf8CleanerTest.php b/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/Support/Utf8CleanerTest.php new file mode 100644 index 00000000..e1a3357f --- /dev/null +++ b/module/oc_telegram_shop/upload/oc_telegram_shop/tests/Unit/Support/Utf8CleanerTest.php @@ -0,0 +1,90 @@ +assertSame(123, Utf8Cleaner::clean(123)); + $this->assertSame(12.3, Utf8Cleaner::clean(12.3)); + $this->assertSame(true, Utf8Cleaner::clean(true)); + $this->assertSame(false, Utf8Cleaner::clean(false)); + $this->assertNull(Utf8Cleaner::clean(null)); + } + + /** @test */ + public function leaves_valid_utf8_strings_unchanged(): void + { + $value = 'Привет, мир! 😀'; + + $this->assertSame($value, Utf8Cleaner::clean($value)); + } + + /** @test */ + public function cleans_strings_with_invalid_utf8_bytes(): void + { + // Строка с заведомо некорректной UTF-8 последовательностью. + $invalid = "\xF0\x28\x8C\x28"; + + $cleaned = Utf8Cleaner::clean($invalid); + + $this->assertIsString($cleaned); + $this->assertNotSame($invalid, $cleaned); + + $json = json_encode($cleaned); + $this->assertNotFalse($json, 'Resulting string must be valid UTF-8 for json_encode'); + } + + /** @test */ + public function cleans_nested_arrays_recursively(): void + { + $invalid = "\xF0\x28\x8C\x28"; + + $data = [ + 'foo' => 'bar', + 'nested' => [ + 'value' => $invalid, + ], + ]; + + $cleaned = Utf8Cleaner::clean($data); + + $this->assertIsArray($cleaned); + $this->assertSame('bar', $cleaned['foo']); + $this->assertIsString($cleaned['nested']['value']); + $this->assertNotSame($invalid, $cleaned['nested']['value']); + + $json = json_encode($cleaned); + $this->assertNotFalse($json); + } + + /** @test */ + public function cleans_objects_recursively(): void + { + $invalid = "\xF0\x28\x8C\x28"; + + $object = new class { + public string $name = 'test'; + public array $meta = []; + }; + + $object->meta['invalid'] = $invalid; + + $cleaned = Utf8Cleaner::clean($object); + + $this->assertSame('test', $cleaned->name); + $this->assertIsString($cleaned->meta['invalid']); + $this->assertNotSame($invalid, $cleaned->meta['invalid']); + + $json = json_encode($cleaned); + $this->assertNotFalse($json); + } +} +