ver. 0.308: kolory statusow zamowien + poprawki bezpieczenstwa
- Kolorowe badge statusow na liscie zamowien (pp_shop_statuses.color) - Walidacja hex koloru z DB (regex), sanityzacja HTML transport - Polaczenie 2 zapytan SQL w jedno orderStatusData() - Path-based form submit w table-list.php (admin URL routing) - 11 nowych testow (750 total, 2114 assertions) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="js-table-filters-wrapper table-filters-wrapper<?= $hasActiveFilters ? ' open' : ''; ?>">
|
||||
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="row mb15 js-table-filters-form">
|
||||
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" data-path-submit="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="row mb15 js-table-filters-form">
|
||||
<?php foreach ($list->filters as $filter): ?>
|
||||
<?php
|
||||
$filterKey = (string)($filter['key'] ?? '');
|
||||
@@ -292,7 +292,7 @@ $isCompactColumn = function(array $column): bool {
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-sm-6 text-right">
|
||||
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="form-inline table-list-per-page-form">
|
||||
<form method="get" action="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" data-path-submit="<?= htmlspecialchars($list->basePath, ENT_QUOTES, 'UTF-8'); ?>" class="form-inline table-list-per-page-form">
|
||||
<?php foreach ($list->query as $key => $value): ?>
|
||||
<?php if ($key !== 'per_page' && $key !== 'page'): ?>
|
||||
<input type="hidden" name="<?= htmlspecialchars((string)$key, ENT_QUOTES, 'UTF-8'); ?>" value="<?= htmlspecialchars((string)$value, ENT_QUOTES, 'UTF-8'); ?>" />
|
||||
@@ -300,7 +300,7 @@ $isCompactColumn = function(array $column): bool {
|
||||
<?php endforeach; ?>
|
||||
<input type="hidden" name="page" value="1" />
|
||||
Wyświetlaj
|
||||
<select name="per_page" class="form-control input-sm" onchange="this.form.submit()">
|
||||
<select name="per_page" class="form-control input-sm js-per-page-select">
|
||||
<?php foreach ($list->perPageOptions as $opt): ?>
|
||||
<option value="<?= (int)$opt; ?>"<?= ((int)$opt === $perPage) ? ' selected="selected"' : ''; ?>><?= (int)$opt; ?></option>
|
||||
<?php endforeach; ?>
|
||||
@@ -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);
|
||||
</script>
|
||||
|
||||
@@ -30,6 +30,14 @@ class OrderAdminService
|
||||
return $this->orders->orderStatuses();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{names: array<int, string>, colors: array<int, string>}
|
||||
*/
|
||||
public function statusData(): array
|
||||
{
|
||||
return $this->orders->orderStatusData();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{items: array<int, array<string, mixed>>, total: int}
|
||||
*/
|
||||
|
||||
@@ -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<int, string>, colors: array<int, string>}
|
||||
*/
|
||||
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
|
||||
|
||||
@@ -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 = '<span class="label" style="background-color:' . htmlspecialchars($statusColor, ENT_QUOTES, 'UTF-8') . ';color:' . $textColor . '">' . $statusLabel . '</span>';
|
||||
} else {
|
||||
$statusHtml = $statusLabel;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'lp' => $lp++ . '.',
|
||||
@@ -86,13 +96,13 @@ class ShopOrderController
|
||||
'paid' => ((int)($item['paid'] ?? 0) === 1)
|
||||
? '<i class="fa fa-check text-success"></i>'
|
||||
: '<i class="fa fa-times text-dark"></i>',
|
||||
'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: <strong>' . (int)($item['total_orders'] ?? 0) . '</strong>',
|
||||
'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);
|
||||
}
|
||||
}
|
||||
|
||||
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, '<b><strong><i><em>');
|
||||
return preg_replace('/<(b|strong|i|em)\s[^>]*>/i', '<$1>', $html);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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', ['<b>Bold</b> <script>alert(1)</script> <em>Italic</em>']);
|
||||
$this->assertSame('<b>Bold</b> alert(1) <em>Italic</em>', $result);
|
||||
}
|
||||
|
||||
public function testSanitizeInlineHtmlStripsAttributesFromAllowedTags(): void
|
||||
{
|
||||
$result = $this->invokePrivate('sanitizeInlineHtml', ['<b onclick="alert(1)">Bold</b>']);
|
||||
$this->assertSame('<b>Bold</b>', $result);
|
||||
|
||||
$result = $this->invokePrivate('sanitizeInlineHtml', ['<strong style="color:red" class="x">text</strong>']);
|
||||
$this->assertSame('<strong>text</strong>', $result);
|
||||
}
|
||||
|
||||
public function testSanitizeInlineHtmlPreservesCleanTags(): void
|
||||
{
|
||||
$result = $this->invokePrivate('sanitizeInlineHtml', ['<b>Bold</b> <i>Italic</i> <strong>Strong</strong> <em>Em</em>']);
|
||||
$this->assertSame('<b>Bold</b> <i>Italic</i> <strong>Strong</strong> <em>Em</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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?
|
||||
$current_ver = 307;
|
||||
$current_ver = 308;
|
||||
|
||||
for ($i = 1; $i <= $current_ver; $i++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user