diff --git a/CLAUDE.md b/CLAUDE.md index 8712a0c..517074b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,7 +36,7 @@ composer test PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`. -Current suite: **739 tests, 2089 assertions**. +Current suite: **750 tests, 2114 assertions**. ### Creating Updates See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs. diff --git a/admin/templates/components/table-list.php b/admin/templates/components/table-list.php index 31c49ae..4acf9a7 100644 --- a/admin/templates/components/table-list.php +++ b/admin/templates/components/table-list.php @@ -9,7 +9,7 @@ $buildUrl = function(array $params = []) use ($list): string { } } $qs = http_build_query($query); - return $list->basePath . ($qs ? ('?' . $qs) : ''); + return $list->basePath . $qs; }; $currentSort = $list->sort['column'] ?? ''; @@ -92,7 +92,7 @@ $isCompactColumn = function(array $column): bool {
-
+ filters as $filter): ?>
- + query as $key => $value): ?> @@ -300,7 +300,7 @@ $isCompactColumn = function(array $column): bool { Wyświetlaj - perPageOptions as $opt): ?> @@ -529,5 +529,26 @@ $isCompactColumn = function(array $column): bool { saveFilterState(true); } }); + + // --- Path-based form submission (admin URL routing) --- + $(document).off('submit.tablePathSubmit', 'form[data-path-submit]'); + $(document).on('submit.tablePathSubmit', 'form[data-path-submit]', function(e) { + e.preventDefault(); + var basePath = $(this).attr('data-path-submit'); + var data = $(this).serializeArray(); + var parts = []; + for (var i = 0; i < data.length; i++) { + if (String(data[i].value) !== '') { + parts.push(encodeURIComponent(data[i].name) + '=' + encodeURIComponent(data[i].value)); + } + } + window.location.href = basePath + (parts.length ? parts.join('&') : ''); + }); + + // Per-page select auto-submit + $(document).off('change.tablePerPage', '.js-per-page-select'); + $(document).on('change.tablePerPage', '.js-per-page-select', function() { + $(this).closest('form').trigger('submit'); + }); })(window.jQuery); diff --git a/autoload/Domain/Order/OrderAdminService.php b/autoload/Domain/Order/OrderAdminService.php index c7b7ab2..53de821 100644 --- a/autoload/Domain/Order/OrderAdminService.php +++ b/autoload/Domain/Order/OrderAdminService.php @@ -30,6 +30,14 @@ class OrderAdminService return $this->orders->orderStatuses(); } + /** + * @return array{names: array, colors: array} + */ + public function statusData(): array + { + return $this->orders->orderStatusData(); + } + /** * @return array{items: array>, total: int} */ diff --git a/autoload/Domain/Order/OrderRepository.php b/autoload/Domain/Order/OrderRepository.php index 1c8f23d..adfdfa5 100644 --- a/autoload/Domain/Order/OrderRepository.php +++ b/autoload/Domain/Order/OrderRepository.php @@ -245,25 +245,43 @@ class OrderRepository public function orderStatuses(): array { - $rows = $this->db->select('pp_shop_statuses', ['id', 'status'], [ + $data = $this->orderStatusData(); + return $data['names']; + } + + /** + * Zwraca nazwy i kolory statusów w jednym zapytaniu. + * + * @return array{names: array, colors: array} + */ + public function orderStatusData(): array + { + $rows = $this->db->select('pp_shop_statuses', ['id', 'status', 'color'], [ 'ORDER' => ['o' => 'ASC'], ]); + $names = []; + $colors = []; + if (!is_array($rows)) { - return []; + return ['names' => $names, 'colors' => $colors]; } - $result = []; foreach ($rows as $row) { $id = (int)($row['id'] ?? 0); if ($id < 0) { continue; } - $result[$id] = (string)($row['status'] ?? ''); + $names[$id] = (string)($row['status'] ?? ''); + + $color = trim((string)($row['color'] ?? '')); + if ($color !== '' && preg_match('/^#[0-9a-fA-F]{3,6}$/', $color)) { + $colors[$id] = $color; + } } - return $result; + return ['names' => $names, 'colors' => $colors]; } public function nextOrderId(int $orderId): ?int diff --git a/autoload/admin/Controllers/ShopOrderController.php b/autoload/admin/Controllers/ShopOrderController.php index 0f0bdc0..3233418 100644 --- a/autoload/admin/Controllers/ShopOrderController.php +++ b/autoload/admin/Controllers/ShopOrderController.php @@ -69,7 +69,9 @@ class ShopOrderController $listRequest['perPage'] ); - $statusesMap = $this->service->statuses(); + $statusData = $this->service->statusData(); + $statusesMap = $statusData['names']; + $statusColorsMap = $statusData['colors']; $rows = []; $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; @@ -77,7 +79,15 @@ class ShopOrderController $orderId = (int)($item['id'] ?? 0); $orderNumber = (string)($item['number'] ?? ''); $statusId = (int)($item['status'] ?? 0); - $statusLabel = (string)($statusesMap[$statusId] ?? ('Status #' . $statusId)); + $statusLabel = htmlspecialchars((string)($statusesMap[$statusId] ?? ('Status #' . $statusId)), ENT_QUOTES, 'UTF-8'); + $statusColor = isset($statusColorsMap[$statusId]) ? $statusColorsMap[$statusId] : ''; + + if ($statusColor !== '') { + $textColor = $this->contrastTextColor($statusColor); + $statusHtml = '' . $statusLabel . ''; + } else { + $statusHtml = $statusLabel; + } $rows[] = [ 'lp' => $lp++ . '.', @@ -86,13 +96,13 @@ class ShopOrderController 'paid' => ((int)($item['paid'] ?? 0) === 1) ? '' : '', - 'status' => htmlspecialchars($statusLabel, ENT_QUOTES, 'UTF-8'), + 'status' => $statusHtml, 'summary' => number_format((float)($item['summary'] ?? 0), 2, '.', ' ') . ' zł', 'client' => htmlspecialchars((string)($item['client'] ?? ''), ENT_QUOTES, 'UTF-8') . ' | zamówienia: ' . (int)($item['total_orders'] ?? 0) . '', 'address' => (string)($item['address'] ?? ''), 'order_email' => (string)($item['order_email'] ?? ''), 'client_phone' => (string)($item['client_phone'] ?? ''), - 'transport' => (string)($item['transport'] ?? ''), + 'transport' => $this->sanitizeInlineHtml((string)($item['transport'] ?? '')), 'payment_method' => (string)($item['payment_method'] ?? ''), '_actions' => [ [ @@ -127,7 +137,7 @@ class ShopOrderController ['key' => 'address', 'label' => 'Adres', 'sortable' => false], ['key' => 'order_email', 'sort_key' => 'order_email', 'label' => 'Email', 'sortable' => true], ['key' => 'client_phone', 'sort_key' => 'client_phone', 'label' => 'Telefon', 'sortable' => true], - ['key' => 'transport', 'sort_key' => 'transport', 'label' => 'Dostawa', 'sortable' => true], + ['key' => 'transport', 'sort_key' => 'transport', 'label' => 'Dostawa', 'sortable' => true, 'raw' => true], ['key' => 'payment_method', 'sort_key' => 'payment_method', 'label' => 'Płatność', 'sortable' => true], ], $rows, @@ -361,4 +371,26 @@ class ShopOrderController return date('Y-m-d H:i', $ts); } -} \ No newline at end of file + + private function contrastTextColor(string $hex): string + { + $hex = ltrim($hex, '#'); + if (strlen($hex) === 3) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + if (strlen($hex) !== 6) { + return '#fff'; + } + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + $luminance = (0.299 * $r + 0.587 * $g + 0.114 * $b) / 255; + return $luminance > 0.5 ? '#000' : '#fff'; + } + + private function sanitizeInlineHtml(string $html): string + { + $html = strip_tags($html, ''); + return preg_replace('/<(b|strong|i|em)\s[^>]*>/i', '<$1>', $html); + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6d9a01b..56732d1 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,17 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.308 (2026-02-22) - Kolory statusow zamowien + poprawki bezpieczenstwa + +- **NEW**: Kolorowe badge statusow na liscie zamowien w admin panelu — kolory pobierane z `pp_shop_statuses.color`, kontrast tekstu obliczany automatycznie +- **FIX**: Walidacja formatu hex koloru z bazy (`/^#[0-9a-fA-F]{3,6}$/`) — odrzucanie nieprawidlowych wartosci +- **FIX**: Sanityzacja HTML w kolumnie "Dostawa" — `strip_tags()` + regex usuwajacy atrybuty z dozwolonych tagow (zapobieganie XSS via `onclick` itp.) +- **OPTYMALIZACJA**: Polaczenie dwoch zapytan SQL do `pp_shop_statuses` (nazwy + kolory) w jedno `orderStatusData()` +- **ZMIANA**: Path-based form submit w `table-list.php` — formularze filtrow i per-page uzywaja JS interceptora z `data-path-submit` zamiast natywnego GET, kompatybilne z admin URL routing +- **NEW**: 11 nowych testow jednostkowych (750 total, 2114 assertions) + +--- + ## ver. 0.307 (2026-02-22) - Przycisk sprawdzania aktualizacji + auto-changelog - **NEW**: Przycisk "Sprawdz aktualizacje" w panelu admina — ikona odswiezenia obok numeru wersji, klik odpytuje serwer AJAX-em i pokazuje/ukrywa badge "aktualizacja" bez przeladowania strony diff --git a/docs/TESTING.md b/docs/TESTING.md index 8e1f110..e470a86 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -23,7 +23,7 @@ composer test # standard ## Aktualny stan ```text -OK (739 tests, 2089 assertions) +OK (750 tests, 2114 assertions) ``` Zweryfikowano: 2026-02-22 (ver. 0.304) diff --git a/tests/Unit/Domain/Order/OrderRepositoryTest.php b/tests/Unit/Domain/Order/OrderRepositoryTest.php index bc93a8b..4e50f0b 100644 --- a/tests/Unit/Domain/Order/OrderRepositoryTest.php +++ b/tests/Unit/Domain/Order/OrderRepositoryTest.php @@ -29,6 +29,66 @@ class OrderRepositoryTest extends TestCase $this->assertSame('W realizacji', $statuses[4]); } + public function testOrderStatusDataReturnsBothNamesAndColors(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('select') + ->willReturnCallback(function ($table, $columns, $where) { + if ($table === 'pp_shop_statuses') { + return [ + ['id' => 0, 'status' => 'Nowe', 'color' => '#ff0000'], + ['id' => 4, 'status' => 'W realizacji', 'color' => '#00ff00'], + ['id' => 5, 'status' => 'Wysłane', 'color' => ''], + ]; + } + return []; + }); + + $repository = new OrderRepository($mockDb); + $data = $repository->orderStatusData(); + + $this->assertArrayHasKey('names', $data); + $this->assertArrayHasKey('colors', $data); + $this->assertSame('Nowe', $data['names'][0]); + $this->assertSame('W realizacji', $data['names'][4]); + $this->assertSame('Wysłane', $data['names'][5]); + $this->assertSame('#ff0000', $data['colors'][0]); + $this->assertSame('#00ff00', $data['colors'][4]); + $this->assertArrayNotHasKey(5, $data['colors']); + } + + public function testOrderStatusDataFiltersInvalidHexColors(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('select') + ->willReturn([ + ['id' => 1, 'status' => 'OK', 'color' => '#abc'], + ['id' => 2, 'status' => 'Bad', 'color' => 'red'], + ['id' => 3, 'status' => 'XSS', 'color' => '#000" onclick="alert(1)'], + ['id' => 4, 'status' => 'Valid', 'color' => '#AABBCC'], + ]); + + $repository = new OrderRepository($mockDb); + $data = $repository->orderStatusData(); + + $this->assertSame('#abc', $data['colors'][1]); + $this->assertArrayNotHasKey(2, $data['colors']); + $this->assertArrayNotHasKey(3, $data['colors']); + $this->assertSame('#AABBCC', $data['colors'][4]); + } + + public function testOrderStatusDataReturnsEmptyOnDbFailure(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('select')->willReturn(false); + + $repository = new OrderRepository($mockDb); + $data = $repository->orderStatusData(); + + $this->assertSame([], $data['names']); + $this->assertSame([], $data['colors']); + } + public function testNextAndPrevOrderIdReturnNullForInvalidInput(): void { $mockDb = $this->createMock(\medoo::class); diff --git a/tests/Unit/admin/Controllers/ShopOrderControllerTest.php b/tests/Unit/admin/Controllers/ShopOrderControllerTest.php index 2b72a32..b5dae9a 100644 --- a/tests/Unit/admin/Controllers/ShopOrderControllerTest.php +++ b/tests/Unit/admin/Controllers/ShopOrderControllerTest.php @@ -85,4 +85,72 @@ class ShopOrderControllerTest extends TestCase $this->assertEquals('Domain\\Product\\ProductRepository', $params[1]->getType()->getName()); $this->assertTrue($params[1]->isOptional()); } + + // --- contrastTextColor tests (via reflection) --- + + public function testContrastTextColorReturnsBlackForLightColor(): void + { + $result = $this->invokePrivate('contrastTextColor', ['#ffffff']); + $this->assertSame('#000', $result); + } + + public function testContrastTextColorReturnsWhiteForDarkColor(): void + { + $result = $this->invokePrivate('contrastTextColor', ['#000000']); + $this->assertSame('#fff', $result); + } + + public function testContrastTextColorHandlesShortHex(): void + { + $result = $this->invokePrivate('contrastTextColor', ['#fff']); + $this->assertSame('#000', $result); + + $result = $this->invokePrivate('contrastTextColor', ['#000']); + $this->assertSame('#fff', $result); + } + + public function testContrastTextColorDefaultsToWhiteForInvalidHex(): void + { + $result = $this->invokePrivate('contrastTextColor', ['invalid']); + $this->assertSame('#fff', $result); + + $result = $this->invokePrivate('contrastTextColor', ['#zz']); + $this->assertSame('#fff', $result); + } + + // --- sanitizeInlineHtml tests (via reflection) --- + + public function testSanitizeInlineHtmlStripsDisallowedTags(): void + { + $result = $this->invokePrivate('sanitizeInlineHtml', ['Bold Italic']); + $this->assertSame('Bold alert(1) Italic', $result); + } + + public function testSanitizeInlineHtmlStripsAttributesFromAllowedTags(): void + { + $result = $this->invokePrivate('sanitizeInlineHtml', ['Bold']); + $this->assertSame('Bold', $result); + + $result = $this->invokePrivate('sanitizeInlineHtml', ['text']); + $this->assertSame('text', $result); + } + + public function testSanitizeInlineHtmlPreservesCleanTags(): void + { + $result = $this->invokePrivate('sanitizeInlineHtml', ['Bold Italic Strong Em']); + $this->assertSame('Bold Italic Strong Em', $result); + } + + public function testSanitizeInlineHtmlHandlesPlainText(): void + { + $result = $this->invokePrivate('sanitizeInlineHtml', ['Kurier DPD']); + $this->assertSame('Kurier DPD', $result); + } + + private function invokePrivate(string $method, array $args) + { + $reflection = new \ReflectionMethod($this->controller, $method); + $reflection->setAccessible(true); + return $reflection->invokeArgs($this->controller, $args); + } } diff --git a/updates/versions.php b/updates/versions.php index fe7917b..d11173d 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@