diff --git a/autoload/Domain/Article/ArticleRepository.php b/autoload/Domain/Article/ArticleRepository.php index d432f95..fb71159 100644 --- a/autoload/Domain/Article/ArticleRepository.php +++ b/autoload/Domain/Article/ArticleRepository.php @@ -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. */ diff --git a/autoload/Domain/Integrations/IntegrationsRepository.php b/autoload/Domain/Integrations/IntegrationsRepository.php index 8631ce3..8b9f478 100644 --- a/autoload/Domain/Integrations/IntegrationsRepository.php +++ b/autoload/Domain/Integrations/IntegrationsRepository.php @@ -28,10 +28,9 @@ class IntegrationsRepository public function getSettings( string $provider ): array { $table = $this->settingsTable( $provider ); - $stmt = $this->db->query( "SELECT * FROM $table" ); - $results = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : []; + $rows = $this->db->select( $table, [ 'name', 'value' ] ); $settings = []; - foreach ( $results as $row ) + foreach ( $rows ?: [] as $row ) $settings[$row['name']] = $row['value']; return $settings; @@ -160,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; } diff --git a/autoload/Domain/Order/OrderRepository.php b/autoload/Domain/Order/OrderRepository.php index adfdfa5..b2465a4 100644 --- a/autoload/Domain/Order/OrderRepository.php +++ b/autoload/Domain/Order/OrderRepository.php @@ -814,7 +814,7 @@ class OrderRepository \Shared\Helpers\Helpers::send_email($settings['contact_email'], 'Nowe zamówienie / ' . $settings['firm_name'] . ' / ' . $order['number'] . ' - ' . $order['client_surname'] . ' ' . $order['client_name'], $mail_order); // zmiana statusu w realizacji jeżeli płatność przy odbiorze - if ($payment_id == 3) { + if (!empty($payment_method['is_cod'])) { $this->updateOrderStatus($order_id, 4); $this->insertStatusHistory($order_id, 4, 1); } diff --git a/autoload/Domain/PaymentMethod/PaymentMethodRepository.php b/autoload/Domain/PaymentMethod/PaymentMethodRepository.php index d0fee49..4ddad69 100644 --- a/autoload/Domain/PaymentMethod/PaymentMethodRepository.php +++ b/autoload/Domain/PaymentMethod/PaymentMethodRepository.php @@ -122,6 +122,7 @@ class PaymentMethodRepository 'apilo_payment_type_id' => $this->normalizeApiloPaymentTypeId($data['apilo_payment_type_id'] ?? null), 'min_order_amount' => $this->normalizeDecimalOrNull($data['min_order_amount'] ?? null), 'max_order_amount' => $this->normalizeDecimalOrNull($data['max_order_amount'] ?? null), + 'is_cod' => (int)(!empty($data['is_cod']) ? 1 : 0), ]; $this->db->update('pp_shop_payment_methods', $row, ['id' => $paymentMethodId]); @@ -240,7 +241,8 @@ class PaymentMethodRepository spm.status, spm.apilo_payment_type_id, spm.min_order_amount, - spm.max_order_amount + spm.max_order_amount, + spm.is_cod FROM pp_shop_payment_methods AS spm INNER JOIN pp_shop_transport_payment_methods AS stpm ON stpm.id_payment_method = spm.id @@ -335,6 +337,7 @@ class PaymentMethodRepository $row['apilo_payment_type_id'] = $this->normalizeApiloPaymentTypeId($row['apilo_payment_type_id'] ?? null); $row['min_order_amount'] = $this->normalizeDecimalOrNull($row['min_order_amount'] ?? null); $row['max_order_amount'] = $this->normalizeDecimalOrNull($row['max_order_amount'] ?? null); + $row['is_cod'] = (int)($row['is_cod'] ?? 0); return $row; } diff --git a/autoload/Domain/Product/ProductRepository.php b/autoload/Domain/Product/ProductRepository.php index a538629..ddef888 100644 --- a/autoload/Domain/Product/ProductRepository.php +++ b/autoload/Domain/Product/ProductRepository.php @@ -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. */ diff --git a/autoload/Shared/Security/CsrfToken.php b/autoload/Shared/Security/CsrfToken.php new file mode 100644 index 0000000..2bb70cf --- /dev/null +++ b/autoload/Shared/Security/CsrfToken.php @@ -0,0 +1,26 @@ + $paymentMethod['apilo_payment_type_id'] ?? '', 'min_order_amount' => $paymentMethod['min_order_amount'] ?? '', 'max_order_amount' => $paymentMethod['max_order_amount'] ?? '', + 'is_cod' => (int)($paymentMethod['is_cod'] ?? 0), ]; $fields = [ @@ -220,6 +221,10 @@ class ShopPaymentMethodController 'tab' => 'settings', 'options' => $apiloOptions, ]), + FormField::switch('is_cod', [ + 'label' => 'Platnosc przy odbiorze', + 'tab' => 'settings', + ]), FormField::switch('status', [ 'label' => 'Aktywny', 'tab' => 'settings', diff --git a/autoload/admin/Support/Forms/FormRequestHandler.php b/autoload/admin/Support/Forms/FormRequestHandler.php index 2bf5105..74bd765 100644 --- a/autoload/admin/Support/Forms/FormRequestHandler.php +++ b/autoload/admin/Support/Forms/FormRequestHandler.php @@ -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); diff --git a/autoload/front/Controllers/ShopBasketController.php b/autoload/front/Controllers/ShopBasketController.php index e267d48..eb382e4 100644 --- a/autoload/front/Controllers/ShopBasketController.php +++ b/autoload/front/Controllers/ShopBasketController.php @@ -276,6 +276,19 @@ class ShopBasketController exit; } + $existingOrderId = isset( $_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] ) + ? (int)$_SESSION[ self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY ] + : 0; + if ( $existingOrderId > 0 ) + { + $existingOrderHash = $this->orderRepository->findHashById( $existingOrderId ); + if ( $existingOrderHash ) + { + header( 'Location: /zamowienie/' . $existingOrderHash ); + exit; + } + } + $client = \Shared\Helpers\Helpers::get_session( 'client' ); $orderSubmitToken = $this->createOrderSubmitToken(); @@ -325,7 +338,10 @@ class ShopBasketController exit; } - if ( $order_id = $this->orderRepository->createFromBasket( + $order_id = null; + try + { + $order_id = $this->orderRepository->createFromBasket( $client[ 'id' ], \Shared\Helpers\Helpers::get_session( 'basket' ), \Shared\Helpers\Helpers::get_session( 'basket-transport-method-id' ), @@ -347,7 +363,17 @@ class ShopBasketController \Shared\Helpers\Helpers::get_session( 'basket_orlen_point_info' ), \Shared\Helpers\Helpers::get_session( 'coupon' ), \Shared\Helpers\Helpers::get_session( 'basket_message' ) - ) ) + ); + } + catch ( \Exception $e ) + { + error_log( '[basketSave] createFromBasket exception: ' . $e->getMessage() ); + \Shared\Helpers\Helpers::error( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat-blad' ) ); + header( 'Location: /koszyk' ); + exit; + } + + if ( $order_id ) { \Shared\Helpers\Helpers::set_session( self::ORDER_SUBMIT_LAST_ORDER_ID_SESSION_KEY, (int)$order_id ); \Shared\Helpers\Helpers::alert( \Shared\Helpers\Helpers::lang( 'zamowienie-zostalo-zlozone-komunikat' ) ); diff --git a/autoload/front/Controllers/ShopOrderController.php b/autoload/front/Controllers/ShopOrderController.php index 24f4977..1071809 100644 --- a/autoload/front/Controllers/ShopOrderController.php +++ b/autoload/front/Controllers/ShopOrderController.php @@ -6,6 +6,8 @@ use Domain\Order\OrderAdminService; class ShopOrderController { + private const HOTPAY_HASH_SEED = 'ProjectPro1916;'; + private $repository; private $adminService; @@ -29,8 +31,6 @@ class ShopOrderController public function paymentStatusTpay() { - file_put_contents( 'tpay.txt', print_r( $_POST, true ) . print_r( $_GET, true ), FILE_APPEND ); - if ( \Shared\Helpers\Helpers::get( 'tr_status' ) == 'TRUE' && \Shared\Helpers\Helpers::get( 'tr_crc' ) ) { $order = $this->repository->findRawByHash( \Shared\Helpers\Helpers::get( 'tr_crc' ) ); @@ -102,7 +102,7 @@ class ShopOrderController $summary_tmp += $order['transport_cost']; endif; - if ( hash( "sha256", "ProjectPro1916;" . round( $summary_tmp, 2 ) . ";" . $_POST["ID_PLATNOSCI"] . ";" . $_POST["ID_ZAMOWIENIA"] . ";" . $_POST["STATUS"] . ";" . $_POST["SEKRET"] ) == $_POST["HASH"] ) + if ( hash( "sha256", self::HOTPAY_HASH_SEED . round( $summary_tmp, 2 ) . ";" . $_POST["ID_PLATNOSCI"] . ";" . $_POST["ID_ZAMOWIENIA"] . ";" . $_POST["STATUS"] . ";" . $_POST["SEKRET"] ) == $_POST["HASH"] ) { if ( $_POST["STATUS"] == "SUCCESS" ) { diff --git a/templates/articles/article-entry.php b/templates/articles/article-entry.php index a10b878..8e3c760 100644 --- a/templates/articles/article-entry.php +++ b/templates/articles/article-entry.php @@ -2,11 +2,12 @@
article['language']['seo_link'] ? $url = $this -> article['language']['seo_link'] : $url = 'a-' . $this -> article['id'] . '-' . \Shared\Helpers\Helpers::seo( $this -> article['language']['title'] );?> + article['language']['title'], ENT_QUOTES, 'UTF-8' ); $safeUrl = htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' );?>

- article['language']['noindex'] ):?>rel="nofollow"> article['language']['title'];?> + article['language']['noindex'] ):?>rel="nofollow">

article['date_add'] ) );?>
@@ -32,6 +33,6 @@ } ?>
- article['language']['noindex'] ):?>rel="nofollow"> + article['language']['noindex'] ):?>rel="nofollow">
\ No newline at end of file diff --git a/templates/articles/article-full.php b/templates/articles/article-full.php index 84d1b17..72e1036 100644 --- a/templates/articles/article-full.php +++ b/templates/articles/article-full.php @@ -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 '

' . $this -> article['language']['title'] . '

'; + echo '

' . htmlspecialchars( $this -> article['language']['title'], ENT_QUOTES, 'UTF-8' ) . '

'; if ( $this -> article['social_icons'] ): + $safeHost = htmlspecialchars( $_SERVER['SERVER_NAME'], ENT_QUOTES, 'UTF-8' ); + $safeUrl = htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' ); ?>
- + facebook - + pinterest - + twitter - + linkedin - + google+
diff --git a/templates/shop-basket/summary-view.php b/templates/shop-basket/summary-view.php index 1a88680..78acbe5 100644 --- a/templates/shop-basket/summary-view.php +++ b/templates/shop-basket/summary-view.php @@ -82,7 +82,7 @@
- Wartość koszyka: 1 + Wartość koszyka: diff --git a/templates_user/shop-basket/summary-view.php b/templates_user/shop-basket/summary-view.php index 8386881..3a7e9c3 100644 --- a/templates_user/shop-basket/summary-view.php +++ b/templates_user/shop-basket/summary-view.php @@ -95,17 +95,23 @@
transport[ 'name_visible' ];?>: - - = $this -> settings[ 'free_delivery' ] ? '0,00' : \Shared\Helpers\Helpers::decimal( $this -> transport[ 'cost' ] );?> zł - + + transport[ 'delivery_free' ] == 1 ):?> + 0,00 zł + + + transport[ 'cost' ] );?> zł + +
- Razem: - = $this -> settings[ 'free_delivery' ] ? \Shared\Helpers\Helpers::decimal( $summary ) : \Shared\Helpers\Helpers::decimal( $summary + $this -> transport[ 'cost' ] );?> zł + + transport[ 'delivery_free' ] == 1 ? \Shared\Helpers\Helpers::decimal( $summary ) : \Shared\Helpers\Helpers::decimal( $summary + $this -> transport[ 'cost' ] );?> zł +
payment_method[ 'name' ];?>