Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5598888716 | ||
|
|
235a388199 | ||
|
|
35aa00a457 | ||
|
|
31426d763e | ||
|
|
c4ce330d01 | ||
|
|
e92c14c9ab | ||
|
|
394d09d3e1 | ||
|
|
564b4eab40 |
15
CLAUDE.md
15
CLAUDE.md
@@ -45,16 +45,20 @@ shopPRO is a PHP e-commerce platform with an admin panel and customer-facing sto
|
||||
# Specific test method
|
||||
./test.ps1 --filter testGetQuantityReturnsCorrectValue
|
||||
|
||||
# Alternative
|
||||
composer test
|
||||
# Alternatives
|
||||
composer test # standard
|
||||
./test.bat # testdox (readable list)
|
||||
./test-simple.bat # dots
|
||||
./test-debug.bat # debug output
|
||||
./test.sh # Git Bash
|
||||
```
|
||||
|
||||
PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`.
|
||||
|
||||
Current suite: **810 tests, 2264 assertions**.
|
||||
Current suite: **817 tests, 2271 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.
|
||||
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. ZIP structure must start directly from project directories — no version subfolder inside the archive.
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -229,6 +233,9 @@ Before starting implementation, review current state of docs.
|
||||
- `docs/DATABASE_STRUCTURE.md` — full database schema
|
||||
- `docs/TESTING.md` — test suite guide and structure
|
||||
- `docs/FORM_EDIT_SYSTEM.md` — form system architecture
|
||||
- `docs/CLASS_CATALOG.md` — full catalog of all classes with descriptions
|
||||
- `docs/TODO.md` — outstanding tasks and planned features
|
||||
- `docs/CRON_QUEUE_PLAN.md` — planned cron/queue architecture
|
||||
- `docs/CHANGELOG.md` — version history
|
||||
- `api-docs/api-reference.json` — REST API documentation (ordersPRO)
|
||||
- `api-docs/index.html` — REST API documentation (ordersPRO)
|
||||
|
||||
@@ -78,7 +78,8 @@ $_SESSION['can_use_rfm'] = true;
|
||||
action="<?= htmlspecialchars($form->action) ?>" enctype="multipart/form-data">
|
||||
|
||||
<input type="hidden" name="_form_id" value="<?= htmlspecialchars($form->formId) ?>">
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
|
||||
|
||||
<?php foreach ($form->hiddenFields as $name => $value): ?>
|
||||
<input type="hidden" name="<?= htmlspecialchars($name) ?>" value="<?= htmlspecialchars($value ?? '') ?>">
|
||||
<?php endforeach; ?>
|
||||
|
||||
@@ -37,12 +37,13 @@
|
||||
?>
|
||||
<div class="alert alert-danger alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
<i class="icon fa fa-ban "></i><?= $alert;?>
|
||||
<i class="icon fa fa-ban "></i><?= htmlspecialchars($alert) ?>
|
||||
</div>
|
||||
<? endif;
|
||||
?>
|
||||
<form method="POST" action="/admin/" class="form-horizontal" rol="form">
|
||||
<input type="hidden" name="s-action" value="user-logon" />
|
||||
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
|
||||
<div class="form-group form-inline row">
|
||||
<div class="col-12">
|
||||
<div class="input-group input-login">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<form method="POST" action="/admin/" class="form-horizontal" rol="form">
|
||||
<input type="hidden" name="s-action" value="user-2fa-verify">
|
||||
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
|
||||
<div class="form-group row">
|
||||
<label class="col col-sm-4 control-label" for="login">Kod z e-maila:</label>
|
||||
<div class="col col-sm-8">
|
||||
@@ -14,5 +15,6 @@
|
||||
</form>
|
||||
<form method="POST" action="/admin/" style="margin-top:10px">
|
||||
<input type="hidden" name="s-action" value="user-2fa-resend">
|
||||
<input type="hidden" name="_csrf_token" value="<?= htmlspecialchars(\Shared\Security\CsrfToken::getToken()) ?>">
|
||||
<button class="btn btn-danger">Wyślij kod ponownie</button>
|
||||
</form>
|
||||
@@ -318,9 +318,7 @@ class ArticleRepository
|
||||
|
||||
if (is_array($results)) {
|
||||
foreach ($results as $row) {
|
||||
if (file_exists('../' . $row['src'])) {
|
||||
unlink('../' . $row['src']);
|
||||
}
|
||||
$this->safeUnlink($row['src']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,9 +335,7 @@ class ArticleRepository
|
||||
|
||||
if (is_array($results)) {
|
||||
foreach ($results as $row) {
|
||||
if (file_exists('../' . $row['src'])) {
|
||||
unlink('../' . $row['src']);
|
||||
}
|
||||
$this->safeUnlink($row['src']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,9 +815,7 @@ class ArticleRepository
|
||||
$results = $this->db->select('pp_articles_files', '*', ['article_id' => null]);
|
||||
if (is_array($results)) {
|
||||
foreach ($results as $row) {
|
||||
if (file_exists('../' . $row['src'])) {
|
||||
unlink('../' . $row['src']);
|
||||
}
|
||||
$this->safeUnlink($row['src']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -836,15 +830,31 @@ class ArticleRepository
|
||||
$results = $this->db->select('pp_articles_images', '*', ['article_id' => null]);
|
||||
if (is_array($results)) {
|
||||
foreach ($results as $row) {
|
||||
if (file_exists('../' . $row['src'])) {
|
||||
unlink('../' . $row['src']);
|
||||
}
|
||||
$this->safeUnlink($row['src']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->delete('pp_articles_images', ['article_id' => null]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Usuwa plik z dysku tylko jeśli ścieżka pozostaje wewnątrz katalogu upload/.
|
||||
* Zapobiega path traversal przy danych z bazy.
|
||||
*/
|
||||
private function safeUnlink(string $src): void
|
||||
{
|
||||
$base = realpath('../upload');
|
||||
if (!$base) {
|
||||
return;
|
||||
}
|
||||
$full = realpath('../' . ltrim($src, '/'));
|
||||
if ($full && strpos($full, $base . DIRECTORY_SEPARATOR) === 0 && is_file($full)) {
|
||||
unlink($full);
|
||||
} elseif ($full) {
|
||||
error_log( '[shopPRO] safeUnlink: ścieżka poza upload/: ' . $src );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pobiera artykuly opublikowane w podanym zakresie dat.
|
||||
*/
|
||||
|
||||
@@ -159,10 +159,15 @@ class IntegrationsRepository
|
||||
if ( empty( $response['accessToken'] ) )
|
||||
return false;
|
||||
|
||||
$this->saveSetting( 'apilo', 'access-token', $response['accessToken'] );
|
||||
$this->saveSetting( 'apilo', 'refresh-token', $response['refreshToken'] );
|
||||
$this->saveSetting( 'apilo', 'access-token-expire-at', $response['accessTokenExpireAt'] );
|
||||
$this->saveSetting( 'apilo', 'refresh-token-expire-at', $response['refreshTokenExpireAt'] );
|
||||
try {
|
||||
$this->saveSetting( 'apilo', 'access-token', $response['accessToken'] );
|
||||
$this->saveSetting( 'apilo', 'refresh-token', $response['refreshToken'] );
|
||||
$this->saveSetting( 'apilo', 'access-token-expire-at', $response['accessTokenExpireAt'] );
|
||||
$this->saveSetting( 'apilo', 'refresh-token-expire-at', $response['refreshTokenExpireAt'] );
|
||||
} catch ( \Exception $e ) {
|
||||
error_log( '[shopPRO] Apilo: błąd zapisu tokenów: ' . $e->getMessage() );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1601,9 +1601,7 @@ class ProductRepository
|
||||
$results = $this->db->select( 'pp_shop_products_files', '*', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||||
if ( is_array( $results ) ) {
|
||||
foreach ( $results as $row ) {
|
||||
if ( file_exists( '../' . $row['src'] ) ) {
|
||||
unlink( '../' . $row['src'] );
|
||||
}
|
||||
$this->safeUnlink( $row['src'] );
|
||||
}
|
||||
}
|
||||
$this->db->delete( 'pp_shop_products_files', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||||
@@ -1614,9 +1612,7 @@ class ProductRepository
|
||||
$results = $this->db->select( 'pp_shop_products_images', '*', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||||
if ( is_array( $results ) ) {
|
||||
foreach ( $results as $row ) {
|
||||
if ( file_exists( '../' . $row['src'] ) ) {
|
||||
unlink( '../' . $row['src'] );
|
||||
}
|
||||
$this->safeUnlink( $row['src'] );
|
||||
}
|
||||
}
|
||||
$this->db->delete( 'pp_shop_products_images', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||||
@@ -2125,14 +2121,30 @@ class ProductRepository
|
||||
$results = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => null ] );
|
||||
if ( is_array( $results ) ) {
|
||||
foreach ( $results as $row ) {
|
||||
if ( file_exists( '../' . $row['src'] ) ) {
|
||||
unlink( '../' . $row['src'] );
|
||||
}
|
||||
$this->safeUnlink( $row['src'] );
|
||||
}
|
||||
}
|
||||
$this->db->delete( 'pp_shop_products_images', [ 'product_id' => null ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Usuwa plik z dysku tylko jeśli ścieżka pozostaje wewnątrz katalogu upload/.
|
||||
* Zapobiega path traversal przy danych z bazy.
|
||||
*/
|
||||
private function safeUnlink(string $src): void
|
||||
{
|
||||
$base = realpath('../upload');
|
||||
if (!$base) {
|
||||
return;
|
||||
}
|
||||
$full = realpath('../' . ltrim($src, '/'));
|
||||
if ($full && strpos($full, $base . DIRECTORY_SEPARATOR) === 0 && is_file($full)) {
|
||||
unlink($full);
|
||||
} elseif ($full) {
|
||||
error_log( '[shopPRO] safeUnlink: ścieżka poza upload/: ' . $src );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Oznacza plik do usunięcia.
|
||||
*/
|
||||
|
||||
26
autoload/Shared/Security/CsrfToken.php
Normal file
26
autoload/Shared/Security/CsrfToken.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Shared\Security;
|
||||
|
||||
class CsrfToken
|
||||
{
|
||||
const SESSION_KEY = 'csrf_token';
|
||||
|
||||
public static function getToken(): string
|
||||
{
|
||||
if (empty($_SESSION[self::SESSION_KEY])) {
|
||||
$_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32));
|
||||
}
|
||||
return (string) $_SESSION[self::SESSION_KEY];
|
||||
}
|
||||
|
||||
public static function validate(string $token): bool
|
||||
{
|
||||
$sessionToken = isset($_SESSION[self::SESSION_KEY]) ? (string) $_SESSION[self::SESSION_KEY] : '';
|
||||
return $sessionToken !== '' && hash_equals($sessionToken, $token);
|
||||
}
|
||||
|
||||
public static function regenerate(): void
|
||||
{
|
||||
$_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32));
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,15 @@ class App
|
||||
$sa = \Shared\Helpers\Helpers::get( 's-action' );
|
||||
if ( !$sa ) return;
|
||||
|
||||
if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) {
|
||||
$csrfToken = isset( $_POST['_csrf_token'] ) ? (string) $_POST['_csrf_token'] : '';
|
||||
if ( !\Shared\Security\CsrfToken::validate( $csrfToken ) ) {
|
||||
\Shared\Helpers\Helpers::alert( 'Nieprawidłowy token bezpieczeństwa. Spróbuj ponownie.' );
|
||||
header( 'Location: /admin/' );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$domain = preg_replace( '/^www\./', '', $_SERVER['SERVER_NAME'] );
|
||||
$cookie_name = 'admin_remember_' . str_replace( '.', '-', $domain );
|
||||
$users = new \Domain\User\UserRepository( $mdb );
|
||||
@@ -84,6 +93,7 @@ class App
|
||||
exit;
|
||||
}
|
||||
|
||||
\Shared\Security\CsrfToken::regenerate();
|
||||
self::finalize_admin_login( $user, $domain, $cookie_name, (bool) \Shared\Helpers\Helpers::get( 'remember' ) );
|
||||
header( 'Location: /admin/articles/list/' );
|
||||
exit;
|
||||
@@ -127,6 +137,7 @@ class App
|
||||
header( 'Location: /admin/' );
|
||||
exit;
|
||||
}
|
||||
\Shared\Security\CsrfToken::regenerate();
|
||||
self::finalize_admin_login( $user, $domain, $cookie_name, !empty( $pending['remember'] ) );
|
||||
header( 'Location: /admin/articles/list/' );
|
||||
exit;
|
||||
|
||||
@@ -32,6 +32,13 @@ class FormRequestHandler
|
||||
'data' => []
|
||||
];
|
||||
|
||||
// Walidacja CSRF
|
||||
$csrfToken = isset($postData['_csrf_token']) ? (string) $postData['_csrf_token'] : '';
|
||||
if (!\Shared\Security\CsrfToken::validate($csrfToken)) {
|
||||
$result['errors'] = ['csrf' => 'Nieprawidłowy token bezpieczeństwa. Odśwież stronę i spróbuj ponownie.'];
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Walidacja
|
||||
$errors = $this->validator->validate($postData, $formViewModel->fields, $formViewModel->languages);
|
||||
|
||||
|
||||
2
cron.php
2
cron.php
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT ^ E_WARNING ^ E_DEPRECATED );
|
||||
error_reporting( E_ALL ^ E_NOTICE ^ E_STRICT );
|
||||
|
||||
function __autoload_my_classes( $classname )
|
||||
{
|
||||
|
||||
@@ -4,6 +4,36 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze.
|
||||
|
||||
---
|
||||
|
||||
## ver. 0.337 (2026-03-12) - Bezpieczeństwo: ochrona CSRF panelu administracyjnego
|
||||
|
||||
- **SECURITY**: `autoload/Shared/Security/CsrfToken.php` — nowa klasa z `getToken()`, `validate()`, `regenerate()` (token 64-znakowy hex, `hash_equals()` przeciw timing attacks)
|
||||
- **SECURITY**: `admin/templates/components/form-edit.php` — dodano ukryte pole `_csrf_token` we wszystkich formularzach edycji
|
||||
- **SECURITY**: `autoload/admin/Support/Forms/FormRequestHandler::handleSubmit()` — walidacja CSRF przed przetworzeniem danych formularza
|
||||
- **SECURITY**: `admin/templates/site/unlogged-layout.php` — token CSRF w formularzu logowania + fix XSS na komunikacie alertu (`htmlspecialchars`)
|
||||
- **SECURITY**: `admin/templates/users/user-2fa.php` — token CSRF w obu formularzach 2FA (weryfikacja i resend)
|
||||
- **SECURITY**: `autoload/admin/App::special_actions()` — walidacja CSRF dla żądań POST; regeneracja tokenu po udanym logowaniu (obie ścieżki: bezpośrednia i przez 2FA)
|
||||
- **TEST**: `tests/Unit/Shared/Security/CsrfTokenTest.php` — 7 nowych testów; suite: 817 testów, 2271 asercji
|
||||
|
||||
---
|
||||
|
||||
## ver. 0.336 (2026-03-12) - Poprawki bezpieczeństwa: error handling w krytycznych ścieżkach
|
||||
|
||||
- **FIX**: `cron.php` — przywrócono `E_WARNING` i `E_DEPRECATED` (wyciszano je od zawsze, ukrywając potencjalne błędy)
|
||||
- **FIX**: `IntegrationsRepository::apiloAuthorize()` — try-catch po zapisie tokenów Apilo; błąd DB logowany i zwraca `false` zamiast cicho kontynuować
|
||||
- **FIX**: `ProductRepository::safeUnlink()` — `error_log()` gdy ścieżka istnieje ale jest poza `upload/`
|
||||
- **FIX**: `ArticleRepository::safeUnlink()` — to samo
|
||||
|
||||
---
|
||||
|
||||
## ver. 0.335 (2026-03-12) - Poprawki bezpieczeństwa: path traversal i XSS w szablonach
|
||||
|
||||
- **SECURITY**: `ProductRepository` — dodano `safeUnlink()` z walidacją `realpath()` zapobiegającą path traversal; użyta w `cleanupDeletedFiles()`, `cleanupDeletedImages()`, `deleteNonassignedImages()`
|
||||
- **SECURITY**: `ArticleRepository` — to samo; użyta w `deleteMarkedImages()`, `deleteMarkedFiles()`, `deleteNonassignedFiles()`, `deleteNonassignedImages()`
|
||||
- **SECURITY**: `templates/articles/article-full.php` — `htmlspecialchars()` na tytule artykułu, `$_SERVER['SERVER_NAME']` i `$url` w linkach social media
|
||||
- **SECURITY**: `templates/articles/article-entry.php` — `htmlspecialchars()` na tytule i `$url` (3 miejsca: href, title, alt)
|
||||
|
||||
---
|
||||
|
||||
## ver. 0.334 (2026-03-12) - Poprawki bezpieczeństwa: debug log, SQL, RedBeanPHP
|
||||
|
||||
- **SECURITY**: `ShopOrderController::paymentStatusTpay()` — usunięto `file_put_contents('tpay.txt', ...)` który logował pełne dane POST/GET płatności do publicznego pliku
|
||||
|
||||
@@ -23,10 +23,10 @@ composer test # standard
|
||||
## Aktualny stan
|
||||
|
||||
```text
|
||||
OK (810 tests, 2264 assertions)
|
||||
OK (817 tests, 2271 assertions)
|
||||
```
|
||||
|
||||
Zweryfikowano: 2026-03-10 (ver. 0.333)
|
||||
Zweryfikowano: 2026-03-12 (ver. 0.337)
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
@@ -71,6 +71,9 @@ tests/
|
||||
| | |-- Transport/TransportRepositoryTest.php
|
||||
| | |-- Update/UpdateRepositoryTest.php
|
||||
| | `-- User/UserRepositoryTest.php
|
||||
| |-- Shared/
|
||||
| | `-- Security/
|
||||
| | `-- CsrfTokenTest.php
|
||||
| `-- admin/
|
||||
| `-- Controllers/
|
||||
| |-- ArticlesControllerTest.php
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
<div class="col-12 col-md-6 ">
|
||||
<div class="article-entry">
|
||||
<? $this -> article['language']['seo_link'] ? $url = $this -> article['language']['seo_link'] : $url = 'a-' . $this -> article['id'] . '-' . \Shared\Helpers\Helpers::seo( $this -> article['language']['title'] );?>
|
||||
<? $safeTitle = htmlspecialchars( $this -> article['language']['title'], ENT_QUOTES, 'UTF-8' ); $safeUrl = htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' );?>
|
||||
<div class="blog-image">
|
||||
<a href="/<?= $url;?>" title="<?= $this -> article['language']['title'];?>" <? if ( $this -> article['language']['noindex'] ):?>rel="nofollow"<? endif;?>> <img src="<?= \front\Views\Articles::getImage( $this -> article );?>" alt="<?= $this -> article['language']['title'];?>"></a>
|
||||
<a href="/<?= $safeUrl;?>" title="<?= $safeTitle;?>" <? if ( $this -> article['language']['noindex'] ):?>rel="nofollow"<? endif;?>> <img src="<?= \front\Views\Articles::getImage( $this -> article );?>" alt="<?= $safeTitle;?>"></a>
|
||||
</div>
|
||||
<h3 class="article-title">
|
||||
<a href="/<? if ( \Shared\Helpers\Helpers::get_session( 'current-lang' ) != ( new \Domain\Languages\LanguagesRepository( $GLOBALS['mdb'] ) )->defaultLanguage() ) echo \Shared\Helpers\Helpers::get_session( 'current-lang' ) . '/';?><?= $url;?>" title="<?= $this -> article['language']['title'];?>" <? if ( $this -> article['language']['noindex'] ):?>rel="nofollow"<? endif;?>><?= $this -> article['language']['title'];?></a>
|
||||
<a href="/<? if ( \Shared\Helpers\Helpers::get_session( 'current-lang' ) != ( new \Domain\Languages\LanguagesRepository( $GLOBALS['mdb'] ) )->defaultLanguage() ) echo \Shared\Helpers\Helpers::get_session( 'current-lang' ) . '/';?><?= $safeUrl;?>" title="<?= $safeTitle;?>" <? if ( $this -> article['language']['noindex'] ):?>rel="nofollow"<? endif;?>><?= $safeTitle;?></a>
|
||||
</h3>
|
||||
<div class="date-add"><?= date( 'd.m.Y', strtotime( $this -> article['date_add'] ) );?></div>
|
||||
<div class="entry">
|
||||
@@ -32,6 +33,6 @@
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<a href="/<?= $url;?>" class="btn btn-success" title="<?= $this -> article['language']['title'];?>" <? if ( $this -> article['language']['noindex'] ):?>rel="nofollow"<? endif;?>><span class="text"><?= $lang['wiecej'];?></span></a>
|
||||
<a href="/<?= $safeUrl;?>" class="btn btn-success" title="<?= $safeTitle;?>" <? if ( $this -> article['language']['noindex'] ):?>rel="nofollow"<? endif;?>><span class="text"><?= $lang['wiecej'];?></span></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -8,24 +8,26 @@ $text = \front\Views\Articles::generateHeadersIds( $text );
|
||||
$this -> article['language']['seo_link'] ? $url = $this -> article['language']['seo_link'] : $url = 'a-' . $this -> article['id'] . '-' . \Shared\Helpers\Helpers::seo( $this -> article['language']['title'] );
|
||||
|
||||
if ( $this -> article['show_title'] )
|
||||
echo '<h3 class="article-title">' . $this -> article['language']['title'] . '</h3>';
|
||||
echo '<h3 class="article-title">' . htmlspecialchars( $this -> article['language']['title'], ENT_QUOTES, 'UTF-8' ) . '</h3>';
|
||||
|
||||
if ( $this -> article['social_icons'] ):
|
||||
$safeHost = htmlspecialchars( $_SERVER['SERVER_NAME'], ENT_QUOTES, 'UTF-8' );
|
||||
$safeUrl = htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' );
|
||||
?>
|
||||
<div class="social-icons">
|
||||
<a class="fb" href="http://www.facebook.com/sharer.php?u=http://www.<?= $_SERVER['SERVER_NAME'];?>/<?= $url;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;" title="facebook" target="_blank" rel="nofollow">
|
||||
<a class="fb" href="http://www.facebook.com/sharer.php?u=http://www.<?= $safeHost;?>/<?= $safeUrl;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;" title="facebook" target="_blank" rel="nofollow">
|
||||
<img src="/images/system/logo-facebook.jpg" alt="facebook">
|
||||
</a>
|
||||
<a class="pinterest" href="http://pinterest.com/pin/create/button/?url=http://www.<?= $_SERVER['SERVER_NAME'];?>/<?= $url;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;" title="pinterest" target="_blank" rel="nofollow">
|
||||
<a class="pinterest" href="http://pinterest.com/pin/create/button/?url=http://www.<?= $safeHost;?>/<?= $safeUrl;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;" title="pinterest" target="_blank" rel="nofollow">
|
||||
<img src="/images/system/logo-pinterest.jpg" alt="pinterest">
|
||||
</a>
|
||||
<a class="twitter" href="http://twitter.com/share?url=http://www.<?= $_SERVER['SERVER_NAME'];?>/<?= $url;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=450,width=600');return false;" title="twitter" target="_blank" rel="nofollow">
|
||||
<a class="twitter" href="http://twitter.com/share?url=http://www.<?= $safeHost;?>/<?= $safeUrl;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=450,width=600');return false;" title="twitter" target="_blank" rel="nofollow">
|
||||
<img src="/images/system/logo-twitter.jpg" alt="twitter">
|
||||
</a>
|
||||
<a class="linkedin" href="http://www.linkedin.com/shareArticle?mini=true&url=http://www.<?= $_SERVER['SERVER_NAME'];?>/<?= $url;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=500,width=850');return false;" title="linked in" target="_blank" rel="nofollow">
|
||||
<a class="linkedin" href="http://www.linkedin.com/shareArticle?mini=true&url=http://www.<?= $safeHost;?>/<?= $safeUrl;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=500,width=850');return false;" title="linked in" target="_blank" rel="nofollow">
|
||||
<img src="/images/system/logo-linkedin.jpg" alt="linkedin">
|
||||
</a>
|
||||
<a class="gp" href="https://plus.google.com/share?url=http://www.<?= $_SERVER['SERVER_NAME'];?>/<?= $url;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;" title="google+" target="_blank" rel="nofollow">
|
||||
<a class="gp" href="https://plus.google.com/share?url=http://www.<?= $safeHost;?>/<?= $safeUrl;?>" onclick="javascript:window.open(this.href, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600');return false;" title="google+" target="_blank" rel="nofollow">
|
||||
<img src="/images/system/logo-google.jpg" alt="google+">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
60
tests/Unit/Shared/Security/CsrfTokenTest.php
Normal file
60
tests/Unit/Shared/Security/CsrfTokenTest.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
namespace Tests\Unit\Shared\Security;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shared\Security\CsrfToken;
|
||||
|
||||
class CsrfTokenTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
}
|
||||
|
||||
public function testGetTokenReturns64CharHexString(): void
|
||||
{
|
||||
$token = CsrfToken::getToken();
|
||||
$this->assertIsString($token);
|
||||
$this->assertSame(64, strlen($token));
|
||||
$this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $token);
|
||||
}
|
||||
|
||||
public function testGetTokenIsIdempotent(): void
|
||||
{
|
||||
$first = CsrfToken::getToken();
|
||||
$second = CsrfToken::getToken();
|
||||
$this->assertSame($first, $second);
|
||||
}
|
||||
|
||||
public function testValidateReturnsTrueForCorrectToken(): void
|
||||
{
|
||||
$token = CsrfToken::getToken();
|
||||
$this->assertTrue(CsrfToken::validate($token));
|
||||
}
|
||||
|
||||
public function testValidateReturnsFalseForEmptyString(): void
|
||||
{
|
||||
CsrfToken::getToken();
|
||||
$this->assertFalse(CsrfToken::validate(''));
|
||||
}
|
||||
|
||||
public function testValidateReturnsFalseForWrongToken(): void
|
||||
{
|
||||
CsrfToken::getToken();
|
||||
$this->assertFalse(CsrfToken::validate('aabbccddeeff00112233445566778899aabbccddeeff00112233445566778899'));
|
||||
}
|
||||
|
||||
public function testValidateReturnsFalseWhenNoSessionToken(): void
|
||||
{
|
||||
$this->assertFalse(CsrfToken::validate('sometoken'));
|
||||
}
|
||||
|
||||
public function testRegenerateChangesToken(): void
|
||||
{
|
||||
$before = CsrfToken::getToken();
|
||||
CsrfToken::regenerate();
|
||||
$after = CsrfToken::getToken();
|
||||
$this->assertNotSame($before, $after);
|
||||
$this->assertSame(64, strlen($after));
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
|
||||
'admin\\ViewModels\\Forms\\' => __DIR__ . '/../autoload/admin/ViewModels/Forms/',
|
||||
'admin\\Validation\\' => __DIR__ . '/../autoload/admin/Validation/',
|
||||
'api\\' => __DIR__ . '/../autoload/api/',
|
||||
'Shared\\Security\\' => __DIR__ . '/../autoload/Shared/Security/',
|
||||
];
|
||||
|
||||
foreach ($prefixes as $prefix => $baseDir) {
|
||||
|
||||
BIN
updates/0.30/ver_0.334.zip
Normal file
BIN
updates/0.30/ver_0.334.zip
Normal file
Binary file not shown.
1
updates/0.30/ver_0.334_files.txt
Normal file
1
updates/0.30/ver_0.334_files.txt
Normal file
@@ -0,0 +1 @@
|
||||
F: ../libraries/rb.php
|
||||
27
updates/0.30/ver_0.334_manifest.json
Normal file
27
updates/0.30/ver_0.334_manifest.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"changelog": "Poprawki bezpieczenstwa: usunieto debug log tpay, naprawa SQL w IntegrationsRepository, usunieto RedBeanPHP",
|
||||
"version": "0.334",
|
||||
"files": {
|
||||
"added": [
|
||||
|
||||
],
|
||||
"deleted": [
|
||||
"libraries/rb.php"
|
||||
],
|
||||
"modified": [
|
||||
"admin/index.php",
|
||||
"autoload/Domain/Integrations/IntegrationsRepository.php",
|
||||
"autoload/front/Controllers/ShopOrderController.php",
|
||||
"index.php",
|
||||
"templates/shop-basket/summary-view.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:c009407c5b941d98b2777b6f7335f6587c93900d02df4441801e314b3deb7fb0",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-03-12",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
BIN
updates/0.30/ver_0.335.zip
Normal file
BIN
updates/0.30/ver_0.335.zip
Normal file
Binary file not shown.
26
updates/0.30/ver_0.335_manifest.json
Normal file
26
updates/0.30/ver_0.335_manifest.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"changelog": "Poprawki bezpieczenstwa: safeUnlink() z walidacja realpath(), escaping XSS w szablonach artykulow",
|
||||
"version": "0.335",
|
||||
"files": {
|
||||
"added": [
|
||||
|
||||
],
|
||||
"deleted": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"autoload/Domain/Article/ArticleRepository.php",
|
||||
"autoload/Domain/Product/ProductRepository.php",
|
||||
"templates/articles/article-entry.php",
|
||||
"templates/articles/article-full.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:2347ff654312f34e22b19cd89b229beabb039a3c253b047df07362d5c8393527",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-03-12",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
BIN
updates/0.30/ver_0.336.zip
Normal file
BIN
updates/0.30/ver_0.336.zip
Normal file
Binary file not shown.
26
updates/0.30/ver_0.336_manifest.json
Normal file
26
updates/0.30/ver_0.336_manifest.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"changelog": "Poprawki error handling: try-catch tokeny Apilo, przywrocenie E_WARNING w cron, error_log w safeUnlink",
|
||||
"version": "0.336",
|
||||
"files": {
|
||||
"added": [
|
||||
|
||||
],
|
||||
"deleted": [
|
||||
|
||||
],
|
||||
"modified": [
|
||||
"autoload/Domain/Article/ArticleRepository.php",
|
||||
"autoload/Domain/Integrations/IntegrationsRepository.php",
|
||||
"autoload/Domain/Product/ProductRepository.php",
|
||||
"cron.php"
|
||||
]
|
||||
},
|
||||
"checksum_zip": "sha256:15d4be791a2ee766e8e66ef4b2da61fd6efd0d0331a15c3b4ceed1a3702f7173",
|
||||
"sql": [
|
||||
|
||||
],
|
||||
"date": "2026-03-12",
|
||||
"directories_deleted": [
|
||||
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
<?
|
||||
$current_ver = 333;
|
||||
$current_ver = 336;
|
||||
|
||||
for ($i = 1; $i <= $current_ver; $i++)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user