diff --git a/.editorconfig b/.editorconfig index f13cab145..d4f4ad5f9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,28 +1,28 @@ # https://editorconfig.org root = true -# --- Базові налаштування для всіх файлів --- +# --- Базовые настройки для всех файлов --- [*] charset = utf-8 indent_style = space indent_size = 2 -end_of_line = lf # Важливо для Docker/DDEV! Примусово ставимо Linux-переклади рядків. -insert_final_newline = true # Git любить порожній рядок наприкінці файлу -trim_trailing_whitespace = true # Видаляє зайві пробіли в кінці рядків під час збереження +end_of_line = lf # Важно для Docker/DDEV! Принудительно ставим Linux-переводы строк. +insert_final_newline = true # Git любит пустую строку в конце файла +trim_trailing_whitespace = true # Удаляет лишние пробелы в конце строк при сохранении -# --- Налаштування для WEB (Frontend) --- -# Для JSON, YAML, JS, HTML часто зручніше 2 пробіли (економить місце на екрані ноутбука) +# --- Настройки для WEB (Frontend) --- +# Для JSON, YAML, JS, HTML часто удобнее 2 пробела (экономит место на экране ноутбука) [*.{json,yaml,yml,js,css,scss,html}] indent_size = 2 -# --- Налаштування для PHP (Backend) --- -# Стандарт PSR-12 вимагає 4 пробіли +# --- Настройки для PHP (Backend) --- +# Стандарт PSR-12 требует 4 пробела [*.php] indent_size = 4 -# --- Налаштування для Smarty --- -# Зазвичай у CMS використовують 4 пробіли (як у PHP) або таби. -# Ставимо 4 пробіли для сумісності. +# --- Настройки для Smarty --- +# Обычно в CMS используют 4 пробела (как в PHP) или табы. +# Ставим 4 пробела для совместимости. [*.{tpl,tpl.php}] indent_size = 4 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 45d823a17..01ef2760a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -5,6 +5,8 @@ about: Створіть звіт, щоб допомогти нам покращ ## Повідомлення про проблему +> **Проблема безпеки?** Не публікуйте вразливості, секрети або дані користувачів у відкритому issue. Надішліть чутливий звіт через [приватний канал GitHub Security Advisories](https://github.com/MAX-IT-Tech/OkayCMS/security/advisories/new). + ### Опис Короткий опис проблеми. @@ -27,4 +29,4 @@ about: Створіть звіт, щоб допомогти нам покращ ### Оточення -Версія OkayCMS, версія php, версія mysql, версія Apache/Nginx, браузер тощо. Будь-яка корисна інформація. \ No newline at end of file +Версія OkayCMS, версія php, версія mysql, версія Apache/Nginx, браузер тощо. Будь-яка корисна інформація. diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 6a9fd56d6..a69998548 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -3,17 +3,10 @@ name: Security Scan on: push: branches: - - develop - - main - - "fix/**" - - "feat/**" - - "test/**" - - "chore/**" + - "**" pull_request: branches: - - develop - - main - - master + - "**" workflow_dispatch: permissions: @@ -25,6 +18,8 @@ jobs: runs-on: ubuntu-latest env: GITLEAKS_VERSION: "8.30.1" + PUBLIC_BASE_TAG: "4.6.0" + PUBLIC_BASE_SHA: "bd959404593aaa80d488c03dc128b9364bca1b57" steps: - name: Checkout code @@ -45,7 +40,18 @@ jobs: sudo install -m 0755 /tmp/gitleaks /usr/local/bin/gitleaks gitleaks version - - name: Scan for secrets + - name: Verify public baseline + if: github.repository == 'MAX-IT-Tech/OkayCMS' + run: | + git fetch --no-tags origin "refs/tags/${PUBLIC_BASE_TAG}:refs/tags/${PUBLIC_BASE_TAG}" + test "$(git rev-parse "${PUBLIC_BASE_TAG}^{commit}")" = "${PUBLIC_BASE_SHA}" + + - name: Scan commits since public baseline + if: github.repository == 'MAX-IT-Tech/OkayCMS' + run: gitleaks git --redact --verbose --log-opts="--full-history ${PUBLIC_BASE_TAG}..HEAD" . + + - name: Scan full private history + if: github.repository != 'MAX-IT-Tech/OkayCMS' run: gitleaks detect --source . --redact --verbose semgrep: diff --git a/.gitignore b/.gitignore index 621eb2a89..a3922676a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,12 +3,10 @@ /vendor/ /cache/ !/cache/**/.keep_folder -# DDEV: track .ddev/config.yaml and commands/; ignore generated files via .ddev/.gitignore /.devcontainer/ .phpunit.result.cache /var/phpstan /var/release-packages/ -# PHPStan probe config (dev/scripts/phpstan-without-ign-mig-01.sh) /phpstan.neon.probe.* # Local PHPStan / probe scratch under var/ (not part of app data) /var/phpstan-probe-*.json @@ -24,10 +22,10 @@ # Ignoring local configuration files /config/config.local.php /design/*/lang/local.*.php -.cursor/mcp.json +#design/*/css/theme-settings.css +# /robots.txt - Usually kept in Git, uncomment if it's unique to the dev # Ignoring all modules /Okay/Modules/* -!/Okay/Modules/AGENTS.md # Excluding native core modules !/Okay/Modules/OkayCMS/ # Ignoring all log contents @@ -37,8 +35,10 @@ !/Okay/log/**/ # Ignoring userfiles inside backend/files subfolders /backend/files/*/* +###backend/files/*/*.* # Keeping placeholder folders for structure preservation !/backend/files/*/.keep_folder +###!backend/files/*/.keep_folder # Ignoring user files (frontend) # Closing all frontend files /files/* @@ -68,6 +68,7 @@ !*.gitignore backend/files/export/export.csv var/phpstan/resultCache.php + ### macOS specific ignores ### # General .DS_Store diff --git a/.prodignore b/.prodignore index b956884e5..c6ee1b07f 100644 --- a/.prodignore +++ b/.prodignore @@ -4,7 +4,6 @@ .codex/ .cursor/ -.ddev/ .devcontainer/ .github/ .history/ diff --git a/.semgrepignore b/.semgrepignore index d1d879da1..23b147474 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -28,11 +28,5 @@ design/okay_shop/js/jquery-*.js design/okay_shop/js/select2*.js design/okay_shop/js/swiper*.js -# Local environment state -.claude/ -.codex/ -.compound-engineering/ -.cursor/ -.ddev/ -.devcontainer/ +# Local test state .phpunit.cache/ diff --git a/1DB_changes/okay_clean.sql b/1DB_changes/okay_clean.sql index 896581531..112ac9c06 100644 --- a/1DB_changes/okay_clean.sql +++ b/1DB_changes/okay_clean.sql @@ -140764,6 +140764,7 @@ INSERT INTO `ok_settings` (`setting_id`, `param`, `value`) VALUES (115, 'watermark_offset_y', '50'), (120, 'comparison_count', '5'), (121, 'is_preorder', '1'), +(122, 'use_backorder_status', '0'), (132, 'posts_num', '8'), (133, 'image_sizes', '200x200|60x60|50x50|219x172|162x77|183x183|35x35|400x300|100x100|1000x1000|800x600|300x300|87x72|330x300|77x77|165x90|150x150|300x120|55x55|250x250|75x75|70x70|360x360|1170x390|465x265|250x100|420x250|160x65|1170x420|120x60|50x40|420x260|420x245|420x225|420x220|26x26|32x32|30x30|80x80|180x180|150x130|120x100|40x30|40x25|20x20|24x24|160x60|90x90|23x23|120x75|480x220|120x80|120x70|120x65|100x60|100x50|1170x380|1170x360|1170x400|900x320|1170x700|1200x700|900x700|500x320|22x22|1067x400|400x350|65x65|80x25|80x30|1100x600|800x800|450x240|320x500|340x240|380x240|330x330'), (134, 'products_image_sizes', '200x200|50x50|1800x1200w|600x340|75x75|330x300|800x600|55x55|300x120|35x35|300x200|150x150|110x150|110x130|100x100|70x70|65x65|800x600w|80x80|200x150|40x40|800x550|300x180|800x500|125x125|120x120|1200x1000w|60x60|180x150|300x150|1800x1800w|600x800|700x800'), @@ -140788,8 +140789,8 @@ INSERT INTO `ok_settings` (`setting_id`, `param`, `value`) VALUES (166, 'site_favicon_version', '001'), (167, 'site_logo_version', '014'), (168, 'multilang_logo', '0'), -(169, 'social_share_theme', 'flat'), -(170, 'sj_shares', 'a:4:{i:0;s:7:\"twitter\";i:1;s:8:\"facebook\";i:2;s:10:\"googleplus\";i:3;s:8:\"linkedin\";}'), +(169, 'social_share_theme', 'default'), +(170, 'sj_shares', 'a:4:{i:0;s:8:\"copy_url\";i:1;s:8:\"facebook\";i:2;s:9:\"x-twitter\";i:3;s:8:\"linkedin\";}'), (197, 'site_email', 'support@okay-cms.com'), (198, 'site_phones', 'a:1:{i:0;s:16:\"+380 44 290 3833\";}'), (199, 'site_social_links', 'a:2:{i:0;s:28:\"https://facebook.com/okaycms\";i:1;s:27:\"https://twitter.com/okaycms\";}'), diff --git a/CHANGELOG.md b/CHANGELOG.md index 45b28542b..b3281634a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,6 @@ ## [Невипущено] -- Заплановано першочерговий план виправлення безпекових меж (Remediation Map): ліквідація рефлектованого XSS у формі коментарів та впровадження обов'язкової CSRF-валідації для публічних мутацій (коментарі, замовлення, швидке замовлення). -- Заплановано забезпечення цілісності платіжних інтеграцій: посилення перевірки автентичності та обов'язкової перевірки підписів для інбоунд-колбеків WayForPay та RozetkaPay перед фіксацією оплати, а також відмова від небезпечного unserialize на користь JSON у налаштуваннях модулів. -- Заплановано додання комплексу заходів із посилення безпеки оточення: впровадження CSP (Content Security Policy) у Report-Only режимі з подальшим переведенням в Enforce, ізоляція сесійних кук (`okay_sid` / `okay_admin_sid`) та аутентифікаційних токенів, а також ліквідація авторизованого traversal-шляху в інтеграції з 1С. -- Заплановано оптимізації SQL-запитів та продуктивності для MySQL 8.4: - - Прискорення фільтрації й сортування за ціною/залишками. - - Усунення N+1 проблеми завантаження категорій у `ProductsHelper::attachDescriptionByTemplate()` - - Консервативне скасування надлишкового `DISTINCT` для важких `mediumtext` колонок у базовому `CRUD.php` за наявності активного `GROUP BY`. - - Оптимізація випадкового вибору товарів (`ORDER BY RAND()`) - ### Додано - Додано потоковий нормалізатор CSV-імпорту: завантажений файл визначається за BOM/UTF-8-перевіркою та набором поширених кириличних кодувань Windows/macOS/DOS/Unix, після чого внутрішній `backend/files/import/import.csv` завжди зберігається як UTF-8 без BOM. @@ -24,10 +15,7 @@ - Додано вибір формату експорту товарів у backend UI: `CSV UTF-8` за замовчуванням і `CSV UTF-8 BOM (Excel)` для сумісності з Excel на Windows. - Додано спільний CSV writer для backend-експортів із підтримкою UTF-8 без BOM, UTF-8 BOM, потокового append-запису та захисту Excel/BOM-експорту від формульно-подібних текстових значень. - Додано root-вимогу Composer `ext-iconv`, потрібну для потокової конвертації кодувань CSV-імпорту. -- Додано `scheduler:list --json` для машинного читання зареєстрованих scheduled tasks у CLI/agent/automation сценаріях. -- Додано документацію для agent workflow: issue tracker, triage labels і domain-boundary для Core та symlinked/vendor-модулів. -- Додано плани подальшого security remediation для storefront і backend, а також окремий review-план оптимізації бази даних. -- Додано план другої ітерації імпорту/експорту XLSX через OpenSpout v5; legacy `.xls` залишено поза scope. +- Додано `scheduler:list --json` для машинного читання зареєстрованих scheduled tasks у CLI та automation-сценаріях. - Додано release-команди `release:changelog:draft` і `release:changelog:check`, які формують reviewable draft за Conventional Commits і перевіряють, чи `[Невипущено]` покриває зміни з вибраного git-діапазону. ### Змінено @@ -45,8 +33,11 @@ - На storefront jQuery 3.7.0 і Fancybox CSS тепер завантажуються з локальних theme assets замість CDN, щоб frontend не залежав від зовнішніх ресурсів під час рендерингу магазину. - Уточнено тестовий контракт реєстру багатомовних сутностей: core product language fields перевіряються як стабільний префікс, дозволяючи модулям додавати власні поля у тому самому PHPUnit-процесі. - Оновлено PHPStan до гілки 2.2 і синхронізовано release/tooling-залежності Composer, щоб статичний аналіз працював на актуальному стеку PHP 8.5. +- Документацію для розробників повернено до кореневого каталогу `docs/`, а production-інструкцію встановлення синхронізовано зі збирачем інсталяційного пакета. +- Прибрано невикористовувані runtime-залежності бібліотеки QR-кодів, які не використовуються публічними функціями OkayCMS. - Файл `backend/files/import/import.csv` оновлено як нормалізований UTF-8 fixture із розділювачем `;`, щоб smoke-перевірки імпорту відповідали новому CSV-контракту. - У `.gitignore` додано nested runtime logs, щоб локальні/generated лог-файли не потрапляли у робоче дерево. +- Описано порядок безпечного впровадження змін у захисті входу до адмінпанелі, обміні з 1С та прийманні повідомлень від платіжних систем, а також перелік перевірок перед випуском. ### Виправлено @@ -56,10 +47,20 @@ - Виправлено checkout Нової Пошти для доставки у відділення: серверна валідація тепер вимагає саме warehouse city ref і не приймає ref адресної доставки як місто відділення. - Виправлено видалення товарів із кошика при натисканні Enter у checkout-полях: кнопки видалення більше не є submit-кнопками форми та не можуть стати implicit default submitter замість оформлення замовлення. - Виправлено CLI entrypoint `ok`: режим відладки тепер береться з основного `debug_mode` і однаково керує показом startup errors та debug-деталей у console-командах. +- Виправлено запуск локального середовища розробки: режим налагодження тепер вмикається лише в локальних налаштуваннях і не змінює основний файл конфігурації проєкту. ### Безпека -- Підготовлено окремі remediation-плани для frontend і backend security audit, щоб після 4.6.0 виправлення безпекових меж можна було виконувати контрольованими, перевірюваними ітераціями. +- Додано правила відповідального повідомлення про вразливості: звичайні помилки залишаються у GitHub Issues, а чутливі звіти надсилаються через приватний канал GitHub Security Advisories. +- Оновлено Guzzle та його PSR-7-компонент до актуальних безпечних версій, що усувають відомі проблеми з cookies, proxy-заголовками, URI та HTTP-серіалізацією. +- Усунено вразливості, знайдені під час перевірки безпеки: захищено форми й платежі від підроблених запитів, посилено вхід до адмінпанелі, обмін із 1С, роботу платіжних систем, завантаження файлів і перенаправлення користувачів. +- Дані про адресу відвідувача та захищене з’єднання тепер приймаються від проміжних серверів лише тоді, коли вони явно позначені як довірені. Це запобігає підміні адреси та небезпечним перенаправленням після входу. +- Захист входу до адмінпанелі тепер коректно працює навіть за кількох одночасних спроб і не дозволяє обійти обмеження через паралельні запити або помилки сховища. +- Дії в адмінпанелі, зокрема завершення навчальних підказок і збереження файлів теми, тепер перевіряють права доступу та справжність запиту, показують зрозумілі повідомлення про помилки й дозволяють безпечно повторити невдалу дію. +- Імпорт із 1С тепер відхиляє пошкоджені, небезпечні або надмірно великі файли, не дозволяє звертатися до файлів поза каталогом обміну та зберігає можливість продовжити перерваний імпорт. +- Повідомлення про оплату від WayForPay і RozetkaPay тепер перевіряються до зміни замовлення: система звіряє платіжний спосіб, валюту, суму, номер операції та справжність повідомлення, а повторне повідомлення не спричиняє повторної оплати. +- WayForPay тепер відхиляє повідомлення про оплату, якщо в ньому відсутнє хоча б одне обов’язкове поле. Перевірку підтверджено на офіційних тестових даних платіжної системи. + - У Responsive Filemanager дозволено завантаження SVG тільки після sanitization boundary: активний вміст і небезпечні атрибути відсікаються, а hardened upload whitelist зберігається. ## [4.6.0] - 2026-05-21 @@ -67,7 +68,7 @@ ### Несумісні зміни - Мінімальна підтримувана версія PHP підвищена до PHP 8.5; встановлення та оновлення тепер потребують сумісного runtime і залежностей Composer для PHP 8.5. -- Оновлено основні runtime-залежності до нових major-версій, зокрема Smarty 5, Symfony 8, PSR-3/PSR-11, Aura SQL 6, PHPMailer 7, Mobile Detect 4, libphonenumber 9, chillerlan QR Code 6, DebugBar 3 та PHPUnit 13 для тестового середовища. +- Оновлено основні runtime-залежності до нових major-версій, зокрема Smarty 5, Symfony 8, PSR-3/PSR-11, Aura SQL 6, PHPMailer 7, Mobile Detect 4, libphonenumber 9, DebugBar 3 та PHPUnit 13 для тестового середовища. ### Додано @@ -75,20 +76,13 @@ - Додано `.prodignore` як єдине джерело виключень для production install/upgrade package builders. - Додано локальні кнопки поширення для сторінок товарів і публікацій без залежності від зовнішньої бібліотеки `jssocials`, включно з SVG-іконками для популярних каналів і копіюванням посилання. - Додано керування порогом показу кнопки Back to TOP через атрибут `data-show-offset` у шаблоні кнопки: `data-show-offset="auto"` або порожнє/некоректне значення зберігає стару поведінку з показом після прокрутки на висоту екрана; `data-show-offset="500"` показує кнопку після 500px прокрутки; `data-show-offset="0"` показує кнопку одразу після початку прокрутки. -- Додано генератор QR-коду НБУ з прикладом використання та тестами для перевірки формату результату. - Додано власний AI-клієнт для OpenAI-сумісного текстового API, каталог моделей і потокову відповідь без залежності від `orhanerday/open-ai`. - Додано підтримку resize-адаптера на базі Intervention Image та окремий WebP-конвертер для сучасного стеку обробки зображень. - Додано PDO-collector і форматування SQL-запитів для DebugBar 3, щоб діагностика запитів залишалася доступною після оновлення debug-панелі. - Додано явний реєстр багатомовних сутностей ядра замість runtime-пошуку класів через `haydenpierce/class-finder`. - Додано набір міграційних перевірок, smoke-тестів і Composer-команд для аналізу, тестування, аудиту безпеки та контролю PHP 8.5-сумісності. - Додано безпековий workflow `Security Scan` у GitHub Actions: Gitleaks виконує blocking-перевірку секретів, а Semgrep CE запускається як non-blocking SAST-перевірка для поступового triage. -- Додано локальні wrapper-скрипти `dev/scripts/security-gitleaks.sh` і `dev/scripts/security-semgrep.sh`, щоб ті самі перевірки можна було запускати перед PR або релізом без додаткового платного сервісу. -- Додано browser-smoke інструкцію для Responsive Filemanager, щоб перевіряти upload, preview, insert, rename, download і доступи після змін у security boundary. -- Додано відстежувану DDEV-конфігурацію (`.ddev/config.yaml`, post-start hooks) і шаблони в `dev/ddev/` для передбачуваного cold-start без ручного bootstrap. -- Додано `database:deploy --yes` / `-y` для неінтерактивного розгортання БД (агенти, CI, `composer create-project`). -- Додано `Makefile`, `dev/scripts/composer.sh` (DDEV-first) та `agent-compatibility.config.json` для agent-compatibility scan. -- Додано проєктну конфігурацію Phpactor зі схемою для автодоповнення, PHPStan diagnostics і PHPCS diagnostics у редакторі. -- Додано `phpcbf-on-save`, спільний helper для IDE wrapper-скриптів і перезапис шляхів PHPCS у JSON-виводі, щоб локальні редактори коректно працювали з DDEV-шляхами. +- Додано `database:deploy --yes` / `-y` для неінтерактивного розгортання БД (CI та `composer create-project`). ### Змінено @@ -114,7 +108,7 @@ ### Видалено - Видалено залежності `gregwar/image`, `rosell-dk/webp-convert`, `orhanerday/open-ai`, `haydenpierce/class-finder`, `snowplow/referer-parser`, `matthiasmullie/minify` і `jssocials`. -- Видалено legacy frontend-шаблони, npm/semgrep scaffolding, експериментальні build-файли, які не входять у production runtime. +- Видалено legacy frontend-шаблони, npm/semgrep scaffolding, застарілі security-документи та експериментальні build-файли, які не входять у production runtime. - Видалено застарілі export CSV-зразки з backend files, щоб не тримати generated/example data у релізному дереві. - Видалено застарілий `backend/design/js/codemirror/package.json`, який не використовується production runtime і створював зайвий шум для dependency/security tooling. diff --git a/Okay/Controllers/CartController.php b/Okay/Controllers/CartController.php index 2537247cb..31971d9d7 100644 --- a/Okay/Controllers/CartController.php +++ b/Okay/Controllers/CartController.php @@ -16,7 +16,6 @@ use Okay\Core\Response; use Okay\Core\Cart; use Okay\Core\Languages; -use Okay\Core\Security\CheckoutToken; use Okay\Helpers\DeliveriesHelper; use Okay\Helpers\PaymentsHelper; use Okay\Helpers\ValidateHelper; @@ -84,8 +83,6 @@ public function render( if ($error = $validateHelper->getCartValidateError($order)) { $this->design->assign('error', $error); - } elseif (!$this->acceptCheckoutSubmission($order, $cart)) { - $this->design->assign('error', 'csrf'); } else { // Add the order to the database. $order->lang_id = $languages->getLangId(); @@ -309,47 +306,6 @@ private function getCustomerCsrfError(ValidateHelper $validateHelper): ?string return $validateHelper->getCustomerCsrfError($this->request->post('customer_csrf_token')); } - private function acceptCheckoutSubmission(object $order, Cart $cart): bool - { - $checkoutToken = $this->request->post('checkout_token'); - if (is_string($checkoutToken) && $checkoutToken !== '') { - return CheckoutToken::consume($checkoutToken); - } - - return CheckoutToken::consumeFingerprint($this->getCheckoutSubmissionFingerprint($order, $cart)); - } - - private function getCheckoutSubmissionFingerprint(object $order, Cart $cart): string - { - $purchases = []; - foreach ($cart->purchases as $purchase) { - $purchases[] = [ - 'variant_id' => (string) $purchase->variant_id, - 'amount' => (int) $purchase->amount, - ]; - } - - usort( - $purchases, - static fn (array $first, array $second): int => $first['variant_id'] <=> $second['variant_id'] - ); - - $payload = json_encode([ - 'purchases' => $purchases, - 'delivery_id' => (string) ($order->delivery_id ?? ''), - 'payment_method_id' => (string) ($order->payment_method_id ?? ''), - 'name' => (string) ($order->name ?? ''), - 'last_name' => (string) ($order->last_name ?? ''), - 'email' => (string) ($order->email ?? ''), - 'phone' => (string) ($order->phone ?? ''), - 'comment' => (string) ($order->comment ?? ''), - 'total_price' => (string) $cart->total_price, - 'coupon_code' => (string) ($_SESSION['coupon_code'] ?? ''), - ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE); - - return hash('sha256', is_string($payload) ? $payload : ''); - } - /** * @param list $names */ diff --git a/Okay/Controllers/ErrorController.php b/Okay/Controllers/ErrorController.php index 930e885ef..89031e699 100644 --- a/Okay/Controllers/ErrorController.php +++ b/Okay/Controllers/ErrorController.php @@ -1,19 +1,16 @@ response->setStatusCode(404); - + $page = $pagesEntity->get('404'); $this->design->assign('page', $page); $this->design->assign('noindex_nofollow', true); @@ -21,11 +18,10 @@ public function pageNotFound(PagesEntity $pagesEntity) $this->design->assign('canonical', Router::generateUrl('page', ['url' => $page->url], true)); $this->response->setContent('page.tpl'); } - + public function siteDisabled() { $this->response->setStatusCode(503); $this->response->setContent('tech.tpl'); } - } diff --git a/Okay/Controllers/MainController.php b/Okay/Controllers/MainController.php index ab45c5541..b2da191c1 100644 --- a/Okay/Controllers/MainController.php +++ b/Okay/Controllers/MainController.php @@ -1,19 +1,15 @@ design->assign('canonical', Router::generateUrl('main', [], true)); $this->response->setContent('main.tpl'); } - } diff --git a/Okay/Controllers/OrderController.php b/Okay/Controllers/OrderController.php index 2a794a9f0..62fc6ca93 100644 --- a/Okay/Controllers/OrderController.php +++ b/Okay/Controllers/OrderController.php @@ -8,6 +8,7 @@ use Okay\Entities\OrderStatusEntity; use Okay\Helpers\MetadataHelpers\OrderMetadataHelper; use Okay\Helpers\OrdersHelper; +use Okay\Helpers\ValidateHelper; class OrderController extends AbstractController { @@ -21,6 +22,7 @@ public function render( CurrenciesEntity $currenciesEntity, OrdersHelper $ordersHelper, OrderMetadataHelper $orderMetadataHelper, + ValidateHelper $validateHelper, $url ) { $order = $ordersEntity->get((string)$url); @@ -54,12 +56,23 @@ public function render( /*Выбор другого способа оплаты*/ if ($this->request->method('post')) { - if ($paymentMethodId = $this->request->post('payment_method_id', 'integer')) { - $ordersEntity->update($order->id, ['payment_method_id' => $paymentMethodId]); - $order = $ordersEntity->get((int)$order->id); - } elseif ($this->request->post('reset_payment_method')) { - $ordersEntity->update($order->id, ['payment_method_id' => null]); - $order = $ordersEntity->get((int)$order->id); + if ($validateHelper->getCustomerCsrfError($this->request->post('customer_csrf_token')) !== null) { + $this->design->assign('error', 'csrf'); + } else { + $allowedPaymentIds = []; + foreach ($ordersHelper->getOrderPaymentMethodsList($order) as $allowedMethod) { + $allowedPaymentIds[(int) $allowedMethod->id] = true; + } + + if ($paymentMethodId = $this->request->post('payment_method_id', 'integer')) { + if (isset($allowedPaymentIds[(int) $paymentMethodId])) { + $ordersEntity->update($order->id, ['payment_method_id' => $paymentMethodId]); + $order = $ordersEntity->get((int)$order->id); + } + } elseif ($this->request->post('reset_payment_method')) { + $ordersEntity->update($order->id, ['payment_method_id' => null]); + $order = $ordersEntity->get((int)$order->id); + } } } diff --git a/Okay/Controllers/SiteMapController.php b/Okay/Controllers/SiteMapController.php index 745990360..ec9774ae6 100644 --- a/Okay/Controllers/SiteMapController.php +++ b/Okay/Controllers/SiteMapController.php @@ -1,14 +1,11 @@ createAdapter($adapterName); } - + protected function createAdapter($adapterName) { $reflector = new \ReflectionClass(static::class); - $adapterClass = $reflector->getNamespaceName().'\\'.$adapterName; + $adapterClass = $reflector->getNamespaceName() . '\\' . $adapterName; $this->adapter = new $adapterClass(); } - -} \ No newline at end of file +} diff --git a/Okay/Core/Adapters/Response/AdapterManager.php b/Okay/Core/Adapters/Response/AdapterManager.php index 33ec3c959..be745df16 100644 --- a/Okay/Core/Adapters/Response/AdapterManager.php +++ b/Okay/Core/Adapters/Response/AdapterManager.php @@ -1,18 +1,14 @@ adapter = new $adapterClass(); } - } diff --git a/Okay/Core/Adapters/Response/GptStream.php b/Okay/Core/Adapters/Response/GptStream.php index 4a31215b9..319bfd724 100644 --- a/Okay/Core/Adapters/Response/GptStream.php +++ b/Okay/Core/Adapters/Response/GptStream.php @@ -1,12 +1,9 @@ getService(\Okay\Core\Config::class); + + $cspReportOnly = $config->get('content_security_policy_report_only'); + if (is_string($cspReportOnly) && $cspReportOnly !== '') { + $headers[] = 'Content-Security-Policy-Report-Only: ' . $cspReportOnly; + } + + $permissionsPolicy = $config->get('permissions_policy'); + if (is_string($permissionsPolicy) && $permissionsPolicy !== '') { + $headers[] = 'Permissions-Policy: ' . $permissionsPolicy; + } + + $hsts = $config->get('strict_transport_security'); + if (Request::getProtocol() === 'https' && is_string($hsts) && $hsts !== '') { + $headers[] = 'Strict-Transport-Security: ' . $hsts; + } + + return $headers; } public function send($contents) diff --git a/Okay/Core/Adapters/Response/Image.php b/Okay/Core/Adapters/Response/Image.php index 99e4fb661..321b727f2 100644 --- a/Okay/Core/Adapters/Response/Image.php +++ b/Okay/Core/Adapters/Response/Image.php @@ -1,19 +1,16 @@ request->getRootUrl() . urldecode($this->request->get( - 'return', - null, - '/backend/index.php?controller=' . $controllerNameList - ))); + 'return', + null, + '/backend/index.php?controller=' . $controllerNameList + ))); } -} \ No newline at end of file +} diff --git a/Okay/Core/Config.php b/Okay/Core/Config.php index 9dd40bdb8..5dc0d0045 100644 --- a/Okay/Core/Config.php +++ b/Okay/Core/Config.php @@ -108,16 +108,23 @@ public function __set($name, $value) /*Формирование токена*/ public function token($text): string { - return md5($text . $this->salt); + return hash_hmac('sha256', (string) $text, $this->salt); } /*Проверка токена*/ public function checkToken($text, $token): bool { - if (!empty($token) && $token === $this->token($text)) { + if (!is_string($token) || $token === '') { + return false; + } + + if (hash_equals($this->token($text), $token)) { return true; } - return false; + + $legacy = md5((string) $text . $this->salt); + + return hash_equals($legacy, $token); } private function initConfig() diff --git a/Okay/Core/Console/Commands/Scheduler/SchedulerRunCommand.php b/Okay/Core/Console/Commands/Scheduler/SchedulerRunCommand.php index 1187d52cf..bfa372ac6 100644 --- a/Okay/Core/Console/Commands/Scheduler/SchedulerRunCommand.php +++ b/Okay/Core/Console/Commands/Scheduler/SchedulerRunCommand.php @@ -26,7 +26,7 @@ protected function configure(): void } protected function handle( - Modules $modules, + Modules $modules, Scheduler $scheduler ): int { $modules->startEnabledModules(); @@ -39,4 +39,4 @@ protected function handle( return Command::SUCCESS; } -} \ No newline at end of file +} diff --git a/Okay/Core/Console/Commands/Scheduler/SchedulerTaskCommand.php b/Okay/Core/Console/Commands/Scheduler/SchedulerTaskCommand.php index ec4b4c9a4..b836f02f0 100644 --- a/Okay/Core/Console/Commands/Scheduler/SchedulerTaskCommand.php +++ b/Okay/Core/Console/Commands/Scheduler/SchedulerTaskCommand.php @@ -32,7 +32,7 @@ protected function configure(): void } protected function handle( - Modules $modules, + Modules $modules, Scheduler $scheduler ) { $modules->startEnabledModules(); @@ -45,4 +45,4 @@ protected function handle( return Command::SUCCESS; } -} \ No newline at end of file +} diff --git a/Okay/Core/Languages.php b/Okay/Core/Languages.php index 2da662275..2a6cda3f3 100644 --- a/Okay/Core/Languages.php +++ b/Okay/Core/Languages.php @@ -134,7 +134,7 @@ public function getLangLabel($langId = null) $langId = $this->getLangId(); } - if (!isset($this->languagesList[$langId])) { + if ($langId === null || !isset($this->languagesList[$langId])) { return false; } @@ -148,7 +148,7 @@ public function getHrefLang($langId = null) $langId = $this->getLangId(); } - if (!isset($this->languagesList[$langId])) { + if ($langId === null || !isset($this->languagesList[$langId])) { return false; } @@ -163,7 +163,7 @@ public function getLangLink($langId = null) $langId = $this->getLangId(); } - if (!isset($this->languagesList[$langId])) { + if ($langId === null || !isset($this->languagesList[$langId])) { return false; } diff --git a/Okay/Core/Modules/AbstractModule.php b/Okay/Core/Modules/AbstractModule.php index d84a36ca8..da6edf273 100644 --- a/Okay/Core/Modules/AbstractModule.php +++ b/Okay/Core/Modules/AbstractModule.php @@ -1,9 +1,7 @@ design = $SL->getService(Design::class); } - -} \ No newline at end of file +} diff --git a/Okay/Core/Modules/AbstractModuleEntityFilter.php b/Okay/Core/Modules/AbstractModuleEntityFilter.php index 720a6e633..056ab5035 100644 --- a/Okay/Core/Modules/AbstractModuleEntityFilter.php +++ b/Okay/Core/Modules/AbstractModuleEntityFilter.php @@ -1,19 +1,16 @@ select = $select; } - -} \ No newline at end of file +} diff --git a/Okay/Core/Modules/DTO/TplChangeDTO.php b/Okay/Core/Modules/DTO/TplChangeDTO.php index cab957206..db75f57d3 100644 --- a/Okay/Core/Modules/DTO/TplChangeDTO.php +++ b/Okay/Core/Modules/DTO/TplChangeDTO.php @@ -295,4 +295,4 @@ public function setChildrenLike(string $childrenLike): self $this->childrenLike = $childrenLike; return $this; } -} \ No newline at end of file +} diff --git a/Okay/Core/Modules/Interfaces/PaymentFormInterface.php b/Okay/Core/Modules/Interfaces/PaymentFormInterface.php index 3ef050f03..fdb7dd839 100644 --- a/Okay/Core/Modules/Interfaces/PaymentFormInterface.php +++ b/Okay/Core/Modules/Interfaces/PaymentFormInterface.php @@ -1,15 +1,12 @@ config->get('marketplace_url') . 'api/v2/modules/access/user'; - $this->ensureSession(); $retryCnt = $_SESSION['request_timeout_try_cnt'] ?? 0; if (time() > ($_SESSION['request_timeout'] ?? 0) && ($response = $this->request($url, $request))) { @@ -100,8 +99,8 @@ public function updateLicenseInfo(): ?LicenseDTO if ($retryCnt < self::MAX_RETRY) { $retryCnt++; } + $_SESSION['request_timeout'] = time() + (self::REQUEST_TIMEOUT * $_SESSION['request_timeout_try_cnt']); $_SESSION['request_timeout_try_cnt'] = $retryCnt; - $_SESSION['request_timeout'] = time() + (self::REQUEST_TIMEOUT * $retryCnt); } return null; @@ -109,14 +108,9 @@ public function updateLicenseInfo(): ?LicenseDTO public function clearRequestRetry() { - $this->ensureSession(); unset($_SESSION['request_timeout_try_cnt']); unset($_SESSION['request_timeout']); } - private function ensureSession(): void - { - $_SESSION ??= []; - } public function isLicensedModule(string $vendor, string $moduleName): bool { @@ -293,13 +287,10 @@ private function getTplFiles($folderPath, $subFolder = ''): array { $tplFiles = []; - if (!is_dir($folderPath)) { - return $tplFiles; - } // Получить список файлов и папок - $files = scandir($folderPath); + $files = is_dir($folderPath) ? scandir($folderPath) : false; if ($files === false) { - return $tplFiles; + return []; } // Перебор полученных файлов и папок diff --git a/Okay/Core/Modules/UpdateObject.php b/Okay/Core/Modules/UpdateObject.php index 4cd826665..e42e36c25 100644 --- a/Okay/Core/Modules/UpdateObject.php +++ b/Okay/Core/Modules/UpdateObject.php @@ -1,9 +1,7 @@ versionCompare($version1, $version2) === 0; } -} \ No newline at end of file +} diff --git a/Okay/Core/Money/DecimalAmount.php b/Okay/Core/Money/DecimalAmount.php new file mode 100644 index 000000000..fa0357480 --- /dev/null +++ b/Okay/Core/Money/DecimalAmount.php @@ -0,0 +1,67 @@ + self::MAX_INTEGER_DIGITS) { + return null; + } + + $fractionalPart = $matches[2] ?? ''; + $fractionalPart = str_pad($fractionalPart, 2, '0'); + $minorUnits = ltrim($integerPart . $fractionalPart, '0'); + + return $minorUnits === '' ? '0' : $minorUnits; + } + + /** + * Compare two values after validating and normalizing both lexemes. + * + * @param mixed $left + * @param mixed $right + */ + public static function equals($left, $right): bool + { + $leftMinorUnits = self::toMinorUnits($left); + $rightMinorUnits = self::toMinorUnits($right); + + return $leftMinorUnits !== null + && $rightMinorUnits !== null + && $leftMinorUnits === $rightMinorUnits; + } +} diff --git a/Okay/Core/QueryFactory.php b/Okay/Core/QueryFactory.php index b24b4314f..ace8fa785 100644 --- a/Okay/Core/QueryFactory.php +++ b/Okay/Core/QueryFactory.php @@ -1,9 +1,7 @@ getProtocol(); + } - return $protocol; + public function getRemoteAddr(): string + { + return (self::$trustedRequestContext ?? TrustedRequestContext::fromServer($_SERVER, null))->getClientIp(); } /** @@ -415,9 +421,11 @@ public static function getProtocol() */ public function checkSession() { + // CSRF is enforced on unsafe methods only; admin state changes must use POST/PUT/PATCH/DELETE (see audit A1.3). if ($this->isUnsafeMethod()) { - $sessionId = $_POST['session_id'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? $_SERVER['HTTP_X_OKAY_SESSION_ID'] ?? null; - if (empty($sessionId) || $sessionId != session_id()) { + $token = $_POST['session_id'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? $_SERVER['HTTP_X_OKAY_SESSION_ID'] ?? null; + $stored = AdminCsrfToken::stored(); + if (!is_string($token) || $stored === null || !hash_equals($stored, $token)) { $_POST = []; return false; } diff --git a/Okay/Core/Routes/RouteFactory.php b/Okay/Core/Routes/RouteFactory.php index b723644a6..c5c77fd43 100644 --- a/Okay/Core/Routes/RouteFactory.php +++ b/Okay/Core/Routes/RouteFactory.php @@ -1,9 +1,7 @@ defaults; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/AllBlog/DefaultStrategy.php b/Okay/Core/Routes/Strategies/AllBlog/DefaultStrategy.php index af9ca8c3d..578837249 100644 --- a/Okay/Core/Routes/Strategies/AllBlog/DefaultStrategy.php +++ b/Okay/Core/Routes/Strategies/AllBlog/DefaultStrategy.php @@ -1,9 +1,7 @@ '(.*)'], [] ]; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/AllProducts/DefaultStrategy.php b/Okay/Core/Routes/Strategies/AllProducts/DefaultStrategy.php index 968c42bed..e3faeb3a6 100644 --- a/Okay/Core/Routes/Strategies/AllProducts/DefaultStrategy.php +++ b/Okay/Core/Routes/Strategies/AllProducts/DefaultStrategy.php @@ -1,9 +1,7 @@ '(.*)'], [] ]; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/BlogCategory/DefaultStrategy.php b/Okay/Core/Routes/Strategies/BlogCategory/DefaultStrategy.php index ad3a5fe18..664fb19e8 100644 --- a/Okay/Core/Routes/Strategies/BlogCategory/DefaultStrategy.php +++ b/Okay/Core/Routes/Strategies/BlogCategory/DefaultStrategy.php @@ -1,9 +1,7 @@ logger->notice('For generate route to category "'.$url.'" need execute SQL query. Or set url through "Okay\Core\Routes\CategoryRoute::setUrlSlugAlias()"'); + $this->logger->notice('For generate route to category "' . $url . '" need execute SQL query. Or set url through "Okay\Core\Routes\CategoryRoute::setUrlSlugAlias()"'); return ''; } @@ -59,19 +57,19 @@ public function generateSlugUrl($url) if ($route = BlogCategoryRoute::getUrlSlugAlias($url)) { return $route; } - + $category = $this->categoriesEntity->get((string) $url); $slug = trim($category->path_url, '/'); // Запоминаем в оперативке slug для этого урла BlogCategoryRoute::setUrlSlugAlias($url, $slug); - + $this->cacheEntity->add([ 'url' => $url, 'slug_url' => $slug, 'type' => 'blog_category', ]); - + return $slug; } @@ -80,22 +78,22 @@ public function generateRouteParams($url) $allCategories = $this->categoriesEntity->find(); $categoriesPathUrls = []; - foreach($allCategories as $category) { + foreach ($allCategories as $category) { $categoriesPathUrls[] = $category->path_url; } - + // Сортируем урлы категорий по длине, от большей к меньшей - usort($categoriesPathUrls, function($a, $b) { + usort($categoriesPathUrls, function ($a, $b) { $difference = strlen($b) - strlen($a); return $difference ?: strcmp($a, $b); }); - + $matchedRoute = null; foreach ($categoriesPathUrls as $categoryPathUrl) { if ($this->compareUrlStartsNoSuccess($categoryPathUrl, $url)) { continue; } - + $urlPath = trim($categoryPathUrl, '/'); $urlParts = explode('/', $urlPath); @@ -104,7 +102,7 @@ public function generateRouteParams($url) if (!empty($urlParts)) { $pathPrefix = implode('/', $urlParts) . '/'; } - + $matchedRoute = [ '{$url}', [ @@ -128,4 +126,4 @@ private function compareUrlStartsNoSuccess($categoryPathUrl, $url) $compareAccessUri = substr($url, 0, strlen($categoryPathUrl)); return $categoryPathUrl !== $compareAccessUri; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/BlogCategory/NoPrefixStrategy.php b/Okay/Core/Routes/Strategies/BlogCategory/NoPrefixStrategy.php index 92ee51144..eab3014a0 100644 --- a/Okay/Core/Routes/Strategies/BlogCategory/NoPrefixStrategy.php +++ b/Okay/Core/Routes/Strategies/BlogCategory/NoPrefixStrategy.php @@ -1,6 +1,5 @@ logger->notice('For generate route to category "'.$url.'" need execute SQL query. Or set url through "Okay\Core\Routes\CategoryRoute::setUrlSlugAlias()"'); + $this->logger->notice('For generate route to category "' . $url . '" need execute SQL query. Or set url through "Okay\Core\Routes\CategoryRoute::setUrlSlugAlias()"'); return ''; } @@ -84,20 +82,20 @@ public function generateRouteParams($url) $allCategories = $this->categoriesEntity->find(); $categoriesPathUrls = []; - foreach($allCategories as $category) { + foreach ($allCategories as $category) { $categoriesPathUrls[] = $category->path_url; } // Сортируем урлы категорий по длине, от большей к меньшей - usort($categoriesPathUrls, function($a, $b) { + usort($categoriesPathUrls, function ($a, $b) { $difference = strlen($b) - strlen($a); return $difference ?: strcmp($a, $b); }); - + $matchedRoute = null; foreach ($categoriesPathUrls as $categoryPathUrl) { $urlPath = trim($categoryPathUrl, '/'); - if ($this->compareUrlStartsNoSuccess($prefix.$urlPath, $url)) { + if ($this->compareUrlStartsNoSuccess($prefix . $urlPath, $url)) { continue; } @@ -107,9 +105,9 @@ public function generateRouteParams($url) if (!empty($urlParts)) { $pathPrefix = implode('/', $urlParts) . '/'; } - + $matchedRoute = [ - $prefix.'{$url}', + $prefix . '{$url}', [ '{$url}' => "{$pathPrefix}({$lastPart})", ], @@ -127,7 +125,7 @@ public function generateRouteParams($url) private function getMockRouteParams($prefix) { - return [$prefix.'{$url}', ['{$url}' => ''], []]; + return [$prefix . '{$url}', ['{$url}' => ''], []]; } private function compareUrlStartsNoSuccess($categoryPathUrl, $url) @@ -136,4 +134,4 @@ private function compareUrlStartsNoSuccess($categoryPathUrl, $url) $compareAccessUri = substr($url, 0, strlen($categoryPathUrl)); return $categoryPathUrl !== $compareAccessUri; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/Brand/DefaultStrategy.php b/Okay/Core/Routes/Strategies/Brand/DefaultStrategy.php index e1cf55aa9..3e77a8a1d 100644 --- a/Okay/Core/Routes/Strategies/Brand/DefaultStrategy.php +++ b/Okay/Core/Routes/Strategies/Brand/DefaultStrategy.php @@ -1,9 +1,7 @@ mockRouteParams = [ - '/'.$prefix.'/{$url}/?{$filtersUrl}', [ + '/' . $prefix . '/{$url}/?{$filtersUrl}', [ '{$filtersUrl}' => '(.*)' ], [] @@ -59,17 +57,17 @@ public function generateRouteParams($url) } return [ - '/'.$prefix.'/{$url}/?{$filtersUrl}', [ + '/' . $prefix . '/{$url}/?{$filtersUrl}', [ '{$url}' => '([^/]*)', '{$filtersUrl}' => '(.*)' ], [] ]; } - private function matchBrandUrlFromUri($url, $prefix) : ?string + private function matchBrandUrlFromUri($url, $prefix): ?string { preg_match("/^{$prefix}\/([^\/]*)/", $url, $matches); return $matches[1] ?? null; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/Brand/NoPrefixStrategy.php b/Okay/Core/Routes/Strategies/Brand/NoPrefixStrategy.php index ad5edd5e5..a3aee8118 100644 --- a/Okay/Core/Routes/Strategies/Brand/NoPrefixStrategy.php +++ b/Okay/Core/Routes/Strategies/Brand/NoPrefixStrategy.php @@ -1,9 +1,7 @@ '(.*)'], []]; + return ['/' . $prefix . '/{$url}/?{$filtersUrl}', ['{$filtersUrl}' => '(.*)'], []]; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/Category/NoPrefixAndPathStrategy.php b/Okay/Core/Routes/Strategies/Category/NoPrefixAndPathStrategy.php index f21c36f9b..f3b986c1c 100644 --- a/Okay/Core/Routes/Strategies/Category/NoPrefixAndPathStrategy.php +++ b/Okay/Core/Routes/Strategies/Category/NoPrefixAndPathStrategy.php @@ -1,9 +1,7 @@ logger->notice('For generate route to category "'.$url.'" need execute SQL query. Or set url through "Okay\Core\Routes\CategoryRoute::setUrlSlugAlias()"'); + $this->logger->notice('For generate route to category "' . $url . '" need execute SQL query. Or set url through "Okay\Core\Routes\CategoryRoute::setUrlSlugAlias()"'); return ''; } @@ -59,19 +57,19 @@ public function generateSlugUrl($url) if ($route = CategoryRoute::getUrlSlugAlias($url)) { return $route; } - + $category = $this->categoriesEntity->get((string) $url); $slug = trim($category->path_url, '/'); // Запоминаем в оперативке slug для этого урла CategoryRoute::setUrlSlugAlias($url, $slug); - + $this->cacheEntity->add([ 'url' => $url, 'slug_url' => $slug, 'type' => 'category', ]); - + return $slug; } @@ -80,22 +78,22 @@ public function generateRouteParams($url) $allCategories = $this->categoriesEntity->find(); $categoriesPathUrls = []; - foreach($allCategories as $category) { + foreach ($allCategories as $category) { $categoriesPathUrls[] = $category->path_url; } - + // Сортируем урлы категорий по длине, от большей к меньшей - usort($categoriesPathUrls, function($a, $b) { + usort($categoriesPathUrls, function ($a, $b) { $difference = strlen($b) - strlen($a); return $difference ?: strcmp($a, $b); }); - + $matchedRoute = null; foreach ($categoriesPathUrls as $categoryPathUrl) { if ($this->compareUrlStartsNoSuccess($categoryPathUrl, $url)) { continue; } - + $urlPath = trim($categoryPathUrl, '/'); $urlParts = explode('/', $urlPath); @@ -129,7 +127,7 @@ private function compareUrlStartsNoSuccess($categoryPathUrl, $url) if (strpos($url, 'category_features') !== false) { $url = substr($url, strlen('category_features') + 1); } - + $categoryPathUrl = ltrim($categoryPathUrl, '/'); $compareAccessUri = substr($url, 0, strlen($categoryPathUrl)); return $categoryPathUrl !== $compareAccessUri; @@ -140,7 +138,7 @@ private function matchFiltersUrl($categoryPathUrl, $url) if (strpos($url, 'category_features') !== false) { $url = substr($url, strlen('category_features') + 1); } - + return substr($url, strlen($categoryPathUrl)); } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/Category/NoPrefixStrategy.php b/Okay/Core/Routes/Strategies/Category/NoPrefixStrategy.php index 16087cbe7..2fe251747 100644 --- a/Okay/Core/Routes/Strategies/Category/NoPrefixStrategy.php +++ b/Okay/Core/Routes/Strategies/Category/NoPrefixStrategy.php @@ -1,6 +1,5 @@ matchFiltersUrl($categoryUrl, $url), '/'); - + return [ '/{$url}/?{$filtersUrl}', [ @@ -61,7 +60,7 @@ private function matchFiltersUrl($categoryUrl, $url) if (strpos($url, 'category_features') !== false) { $url = substr($url, strlen('category_features') + 1); } - + return substr($url, strlen($categoryUrl)); } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/Category/PrefixAndPathStrategy.php b/Okay/Core/Routes/Strategies/Category/PrefixAndPathStrategy.php index d15a42776..8bb714df9 100644 --- a/Okay/Core/Routes/Strategies/Category/PrefixAndPathStrategy.php +++ b/Okay/Core/Routes/Strategies/Category/PrefixAndPathStrategy.php @@ -1,9 +1,7 @@ logger->notice('For generate route to category "'.$url.'" need execute SQL query. Or set url through "Okay\Core\Routes\CategoryRoute::setUrlSlugAlias()"'); + $this->logger->notice('For generate route to category "' . $url . '" need execute SQL query. Or set url through "Okay\Core\Routes\CategoryRoute::setUrlSlugAlias()"'); return ''; } @@ -84,20 +82,20 @@ public function generateRouteParams($url) $allCategories = $this->categoriesEntity->find(); $categoriesPathUrls = []; - foreach($allCategories as $category) { + foreach ($allCategories as $category) { $categoriesPathUrls[] = $category->path_url; } // Сортируем урлы категорий по длине, от большей к меньшей - usort($categoriesPathUrls, function($a, $b) { + usort($categoriesPathUrls, function ($a, $b) { $difference = strlen($b) - strlen($a); return $difference ?: strcmp($a, $b); }); - + $matchedRoute = null; foreach ($categoriesPathUrls as $categoryPathUrl) { $urlPath = trim($categoryPathUrl, '/'); - if ($this->compareUrlStartsNoSuccess($prefix.'/'.$urlPath, $url)) { + if ($this->compareUrlStartsNoSuccess($prefix . '/' . $urlPath, $url)) { continue; } @@ -107,9 +105,9 @@ public function generateRouteParams($url) if (!empty($urlParts)) { $pathPrefix = implode('/', $urlParts) . '/'; } - $filter = trim($this->matchFiltersUrl($prefix.'/'.$urlPath, $url), '/'); + $filter = trim($this->matchFiltersUrl($prefix . '/' . $urlPath, $url), '/'); $matchedRoute = [ - '/'.$prefix.'/{$url}/?{$filtersUrl}', + '/' . $prefix . '/{$url}/?{$filtersUrl}', [ '{$url}' => "{$pathPrefix}({$lastPart})", '{$filtersUrl}' => "(" . $filter . ")", @@ -128,7 +126,7 @@ public function generateRouteParams($url) private function getMockRouteParams($prefix) { - return ['/'.$prefix.'/{$url}/?{$filtersUrl}', ['{$url}' => '', '{$filtersUrl}' => ''], []]; + return ['/' . $prefix . '/{$url}/?{$filtersUrl}', ['{$url}' => '', '{$filtersUrl}' => ''], []]; } private function compareUrlStartsNoSuccess($categoryPathUrl, $url) @@ -137,7 +135,7 @@ private function compareUrlStartsNoSuccess($categoryPathUrl, $url) if (strpos($url, 'category_features') !== false) { $url = substr($url, strlen('category_features') + 1); } - + $categoryPathUrl = ltrim($categoryPathUrl, '/'); $compareAccessUri = substr($url, 0, strlen($categoryPathUrl)); return $categoryPathUrl !== $compareAccessUri; @@ -148,7 +146,7 @@ private function matchFiltersUrl($categoryPathUrl, $url) if (strpos($url, 'category_features') !== false) { $url = substr($url, strlen('category_features') + 1); } - + return substr($url, strlen($categoryPathUrl) + 1); } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/Page/DefaultStrategy.php b/Okay/Core/Routes/Strategies/Page/DefaultStrategy.php index 36879f5b9..28057b030 100644 --- a/Okay/Core/Routes/Strategies/Page/DefaultStrategy.php +++ b/Okay/Core/Routes/Strategies/Page/DefaultStrategy.php @@ -1,9 +1,7 @@ '(.*)'], []]; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/Post/DefaultStrategy.php b/Okay/Core/Routes/Strategies/Post/DefaultStrategy.php index 660579800..899bafb48 100644 --- a/Okay/Core/Routes/Strategies/Post/DefaultStrategy.php +++ b/Okay/Core/Routes/Strategies/Post/DefaultStrategy.php @@ -1,9 +1,7 @@ ''], ['{$url}' => '']]; public function __construct() @@ -24,7 +21,7 @@ public function __construct() $this->blogEntity = $entityFactory->get(BlogEntity::class); } - + public function generateRouteParams($url) { $postUrl = $this->matchProductUrl($url); @@ -47,4 +44,4 @@ private function matchProductUrl($url) return ''; } -} \ No newline at end of file +} diff --git a/Okay/Core/Routes/Strategies/Product/DefaultStrategy.php b/Okay/Core/Routes/Strategies/Product/DefaultStrategy.php index 9464fa288..634dda963 100644 --- a/Okay/Core/Routes/Strategies/Product/DefaultStrategy.php +++ b/Okay/Core/Routes/Strategies/Product/DefaultStrategy.php @@ -1,9 +1,7 @@ '(\d*)'], []]; + return ['/' . $prefix . '/{$url}/?{$variantId}', ['{$variantId}' => '(\d*)'], []]; } -} \ No newline at end of file +} diff --git a/Okay/Core/Security/AdminCsrfToken.php b/Okay/Core/Security/AdminCsrfToken.php new file mode 100644 index 000000000..362521679 --- /dev/null +++ b/Okay/Core/Security/AdminCsrfToken.php @@ -0,0 +1,58 @@ +filesystem = $filesystem ?? new AdminLoginThrottleFilesystem(); + + if ($this->maxAttempts < 1 || $this->windowSeconds < 1 || $this->maintenanceIntervalSeconds < 0 || $this->maintenanceScanBudget < 1) { + $this->storageAvailable = false; + return; + } + + if (!$this->filesystem->isDirectory($this->storageDir) && !$this->filesystem->makeDirectory($this->storageDir)) { + $this->storageAvailable = false; + error_log('AdminLoginThrottle: cannot create store at ' . $this->storageDir); + return; + } + + if (!$this->filesystem->isWritable($this->storageDir)) { + $this->storageAvailable = false; + error_log('AdminLoginThrottle: store is not writable at ' . $this->storageDir); + } + } + + public function reserve(string $account, string $clientIp): bool + { + if (!$this->storageAvailable) { + return false; + } + + $accountKey = $this->accountKey($account); + $clientKey = $this->clientKey($clientIp); + if ($accountKey === null || $clientKey === null) { + return false; + } + + if (!$this->maintain()) { + return false; + } + + $states = [ + 'account' => $this->statePath('account', $accountKey), + 'client' => $this->statePath('client', $clientKey), + ]; + $locks = $this->acquireLocks(array_values(array_map([$this, 'lockPathForState'], $states))); + if ($locks === null) { + return false; + } + + try { + $now = $this->filesystem->now(); + $decoded = []; + foreach ($states as $type => $path) { + $state = $this->readState($path, $type, $type === 'account' ? $accountKey : $clientKey, $now); + if ($state === null || $state['count'] >= $this->maxAttempts) { + return false; + } + $decoded[$type] = $state; + } + + foreach ($states as $type => $path) { + $key = $type === 'account' ? $accountKey : $clientKey; + $state = [ + 'version' => 1, + 'type' => $type, + 'key' => $key, + 'count' => $decoded[$type]['count'] + 1, + 'last' => $now, + ]; + if (!$this->writeState($path, $state)) { + return false; + } + } + + return true; + } finally { + $this->releaseLocks($locks); + } + } + + /** + * Legacy client-only read compatibility for callers not yet migrated. + */ + public function isAllowed(string $clientIp): bool + { + if (!$this->storageAvailable) { + return false; + } + + $key = $this->clientKey($clientIp); + if ($key === null) { + return false; + } + + $path = $this->statePath('client', $key); + $locks = $this->acquireLocks([$this->lockPathForState($path)]); + if ($locks === null) { + return false; + } + + try { + $state = $this->readState($path, 'client', $key, $this->filesystem->now()); + return $state !== null && $state['count'] < $this->maxAttempts; + } finally { + $this->releaseLocks($locks); + } + } + + /** @deprecated Use reserve() before password verification. */ + public function registerFailure(string $clientIp): void + { + $this->reserve('__legacy_client_only__', $clientIp); + } + + public function clear(string $accountOrClient, ?string $clientIp = null): bool + { + if (!$this->storageAvailable) { + return false; + } + + $states = $clientIp === null + ? ['client' => $this->statePathOrNull('client', $this->clientKey($accountOrClient))] + : [ + 'account' => $this->statePathOrNull('account', $this->accountKey($accountOrClient)), + 'client' => $this->statePathOrNull('client', $this->clientKey($clientIp)), + ]; + if (in_array(null, $states, true)) { + return false; + } + + /** @var array $states */ + $locks = $this->acquireLocks(array_values(array_map([$this, 'lockPathForState'], $states))); + if ($locks === null) { + return false; + } + + try { + foreach ($states as $path) { + if ($this->filesystem->isFile($path) && !$this->filesystem->unlink($path)) { + return false; + } + if (!$this->filesystem->isFile($path) && $this->filesystem->exists($path)) { + return false; + } + } + return true; + } finally { + $this->releaseLocks($locks); + } + } + + private function maintain(): bool + { + $marker = $this->storageDir . '/login_maintenance.json'; + $markerLock = $this->storageDir . '/login_maintenance.lock'; + $locks = $this->acquireLocks([$markerLock]); + if ($locks === null) { + return false; + } + + try { + $now = $this->filesystem->now(); + if ($this->filesystem->isFile($marker)) { + $contents = $this->filesystem->read($marker); + if ($contents === false) { + return false; + } + $decoded = json_decode($contents, true); + if (!is_array($decoded) || !is_int($decoded['last'] ?? null) || $decoded['last'] < 0 || $decoded['last'] > $now) { + return false; + } + if ($now - $decoded['last'] < $this->maintenanceIntervalSeconds) { + return true; + } + } elseif ($this->filesystem->exists($marker)) { + return false; + } + + $paths = $this->filesystem->listFiles($this->storageDir, $this->maintenanceScanBudget); + if ($paths === false) { + return false; + } + + foreach ($paths as $path) { + $name = basename($path); + if (preg_match(self::STATE_PATTERN, $name) !== 1) { + continue; + } + $mtime = $this->filesystem->modifiedTime($path); + if ($mtime === false || $now - $mtime <= $this->windowSeconds) { + continue; + } + + $key = $this->lockPathForState($path); + $stateLocks = $this->acquireLocks([$key]); + if ($stateLocks === null) { + return false; + } + try { + if (!$this->filesystem->isFile($path)) { + continue; + } + $lockedMtime = $this->filesystem->modifiedTime($path); + if ($lockedMtime === false || $now - $lockedMtime <= $this->windowSeconds) { + continue; + } + if (!$this->filesystem->unlink($path)) { + return false; + } + } finally { + $this->releaseLocks($stateLocks); + } + } + + return $this->writeState($marker, ['last' => $now]); + } finally { + $this->releaseLocks($locks); + } + } + + /** @return array|null */ + private function readState(string $path, string $type, string $key, int $now): ?array + { + if (!$this->filesystem->isFile($path)) { + return $this->filesystem->exists($path) ? null : ['count' => 0, 'last' => 0]; + } + + $contents = $this->filesystem->read($path); + if ($contents === false) { + return null; + } + $decoded = json_decode($contents, true); + if ( + !is_array($decoded) + || ($decoded['version'] ?? null) !== 1 + || ($decoded['type'] ?? null) !== $type + || ($decoded['key'] ?? null) !== $key + || !is_int($decoded['count'] ?? null) + || !is_int($decoded['last'] ?? null) + || $decoded['count'] < 0 + || $decoded['count'] > $this->maxAttempts + || $decoded['last'] < 1 + || $decoded['last'] > $now + ) { + return null; + } + + if ($now - $decoded['last'] > $this->windowSeconds) { + return ['count' => 0, 'last' => 0]; + } + + return ['count' => $decoded['count'], 'last' => $decoded['last']]; + } + + /** @param array $state */ + private function writeState(string $path, array $state): bool + { + $temporary = $this->filesystem->temporaryFile($this->storageDir); + if ($temporary === false) { + return false; + } + + $handle = $this->filesystem->open($temporary, 'wb'); + $contents = json_encode($state, JSON_THROW_ON_ERROR); + $ok = $handle !== false + && $this->filesystem->write($handle, $contents) === strlen($contents) + && $this->filesystem->flush($handle); + $this->filesystem->close($handle); + if (!$ok || !$this->filesystem->rename($temporary, $path)) { + if ($this->filesystem->isFile($temporary)) { + $this->filesystem->unlink($temporary); + } + return false; + } + + return true; + } + + /** + * @param list $paths + * @return array|null + */ + private function acquireLocks(array $paths): ?array + { + sort($paths, SORT_STRING); + /** @var array $handles */ + $handles = []; + foreach ($paths as $path) { + $handle = $this->filesystem->open($path, 'c+b'); + $locked = false; + for ($attempt = 0; $handle !== false && $attempt < self::LOCK_ATTEMPTS; $attempt++) { + if ($this->filesystem->lockExclusive($handle)) { + $locked = true; + break; + } + usleep(1000); + } + if ($handle === false || !$locked) { + if ($handle !== false) { + $this->filesystem->close($handle); + } + $this->releaseLocks($handles); + return null; + } + $handles[$path] = $handle; + } + + return $handles; + } + + /** @param array $handles */ + private function releaseLocks(array $handles): void + { + foreach (array_reverse($handles, true) as $handle) { + $this->filesystem->unlock($handle); + $this->filesystem->close($handle); + } + } + + private function accountKey(string $account): ?string + { + $normalized = strtolower(trim($account)); + return $normalized === '' || preg_match('/[\x00-\x1F\x7F]/', $normalized) === 1 + ? null + : hash('sha256', 'account:' . $normalized); + } + + private function clientKey(string $clientIp): ?string + { + $ip = trim($clientIp); + if (filter_var($ip, FILTER_VALIDATE_IP) === false) { + return null; + } + $packed = inet_pton($ip); + $normalized = $packed === false ? null : inet_ntop($packed); + return is_string($normalized) ? hash('sha256', 'client:' . strtolower($normalized)) : null; + } + + private function statePath(string $type, string $key): string + { + return rtrim($this->storageDir, '/') . '/login_' . $type . '_' . $key . '.json'; + } + + private function statePathOrNull(string $type, ?string $key): ?string + { + return $key === null ? null : $this->statePath($type, $key); + } + + private function lockPathForState(string $statePath): string + { + return substr($statePath, 0, -5) . '.lock'; + } +} diff --git a/Okay/Core/Security/AdminLoginThrottleFilesystem.php b/Okay/Core/Security/AdminLoginThrottleFilesystem.php new file mode 100644 index 000000000..260b6db4b --- /dev/null +++ b/Okay/Core/Security/AdminLoginThrottleFilesystem.php @@ -0,0 +1,142 @@ +|false */ + public function listFiles(string $directory, int $budget): array|false + { + $handle = @opendir($directory); + if ($handle === false) { + return false; + } + + $paths = []; + try { + while (count($paths) < $budget && ($entry = readdir($handle)) !== false) { + if ($entry === '.' || $entry === '..') { + continue; + } + + $paths[] = $directory . '/' . $entry; + } + } finally { + closedir($handle); + } + + return $paths; + } + + public function modifiedTime(string $path): int|false + { + $mtime = @filemtime($path); + return is_int($mtime) ? $mtime : false; + } + + public function now(): int + { + return time(); + } +} diff --git a/Okay/Core/Security/AdminSession.php b/Okay/Core/Security/AdminSession.php index 3787d83d2..a20682677 100644 --- a/Okay/Core/Security/AdminSession.php +++ b/Okay/Core/Security/AdminSession.php @@ -4,6 +4,8 @@ namespace Okay\Core\Security; +use Okay\Core\Request; + final class AdminSession { public const SESSION_NAME = 'okay_admin_sid'; @@ -13,19 +15,7 @@ final class AdminSession */ public static function isSecureRequest(array $server): bool { - if ((int)($server['SERVER_PORT'] ?? 0) === 443) { - return true; - } - - if (in_array((string)($server['HTTPS'] ?? ''), ['on', '1'], true)) { - return true; - } - - if (($server['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https') { - return true; - } - - return ($server['HTTP_X_FORWARDED_SSL'] ?? '') === 'on'; + return self::getRequestContext($server)->isSecure(); } /** @@ -47,7 +37,7 @@ public static function regenerateId(): void { if (session_status() === PHP_SESSION_ACTIVE) { session_regenerate_id(true); - $_SESSION['id'] = session_id(); + $_SESSION['id'] = AdminCsrfToken::rotate(); } } @@ -188,4 +178,13 @@ private static function deleteCookie(array $server): void 'samesite' => 'Lax', ]); } + + /** + * @param array $server + */ + private static function getRequestContext(array $server): TrustedRequestContext + { + return Request::getTrustedRequestContext() + ?? TrustedRequestContext::fromServer($server, null); + } } diff --git a/Okay/Core/Security/CheckoutToken.php b/Okay/Core/Security/CheckoutToken.php deleted file mode 100644 index ad872d390..000000000 --- a/Okay/Core/Security/CheckoutToken.php +++ /dev/null @@ -1,78 +0,0 @@ -= $now - && hash_equals($stored['fingerprint'], $fingerprint) - ) { - return false; - } - - $_SESSION[self::FINGERPRINT_SESSION_KEY] = [ - 'fingerprint' => $fingerprint, - 'expires_at' => $now + self::FINGERPRINT_TTL_SECONDS, - ]; - - return true; - } - - public static function rotate(): string - { - $_SESSION[self::SESSION_KEY] = bin2hex(random_bytes(32)); - - return $_SESSION[self::SESSION_KEY]; - } - - private static function isToken(string $token): bool - { - return strlen($token) === 64 && ctype_xdigit($token); - } -} diff --git a/Okay/Core/Security/SafeRedirect.php b/Okay/Core/Security/SafeRedirect.php new file mode 100644 index 000000000..486e8ddd0 --- /dev/null +++ b/Okay/Core/Security/SafeRedirect.php @@ -0,0 +1,234 @@ + $scheme, + 'host' => $host, + 'port' => $port ?? ($scheme === 'https' ? 443 : 80), + 'explicit_port' => $port !== null, + 'path' => is_string($parts['path'] ?? null) ? $parts['path'] : '', + ] + $parts; + } + + /** + * @param array{scheme: string, host: string, port: int} $left + * @param array{scheme: string, host: string, port: int} $right + */ + private static function sameOrigin(array $left, array $right): bool + { + return $left['scheme'] === $right['scheme'] + && hash_equals($left['host'], $right['host']) + && $left['port'] === $right['port']; + } + + /** + * @param array{scheme: string, host: string, port: int, explicit_port: bool} $origin + */ + private static function buildOrigin(array $origin): string + { + $host = str_contains($origin['host'], ':') ? '[' . $origin['host'] . ']' : $origin['host']; + + return $origin['scheme'] . '://' . $host + . ($origin['explicit_port'] ? ':' . $origin['port'] : ''); + } + + /** + * @param mixed $host + */ + private static function normalizeHost($host): ?string + { + if (!is_string($host) || $host === '' || str_contains($host, '%') || preg_match('/[\s\\@?#]/', $host) === 1) { + return null; + } + + $host = strtolower(rtrim($host, '.')); + if ($host === '') { + return null; + } + + if (filter_var($host, FILTER_VALIDATE_IP) !== false) { + return $host; + } + + $hostPattern = '/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?))*$/i'; + if (preg_match($hostPattern, $host) !== 1) { + return null; + } + + return $host; + } + + /** + * @param array $parts + */ + private static function appendQueryAndFragment(string $path, array $parts): string + { + if (array_key_exists('query', $parts) && is_string($parts['query'])) { + $path .= '?' . $parts['query']; + } + if (array_key_exists('fragment', $parts) && is_string($parts['fragment'])) { + $path .= '#' . $parts['fragment']; + } + + return $path; + } + + private static function isUnsafePath(string $path): bool + { + $decoded = rawurldecode($path); + + return self::containsUnsafeCharacters($path) + || self::containsUnsafeCharacters($decoded) + || str_contains($path, '\\') + || str_contains($decoded, '\\') + || str_starts_with($decoded, '//') + || preg_match('#(^|/)\.\.(/|$)#', $decoded) === 1; + } + + private static function containsUnsafeCharacters(string $value): bool + { + return preg_match('/[\x00-\x1F\x7F]/', $value) === 1 || str_contains($value, '\\'); + } + + private static function hasScheme(string $value): bool + { + return preg_match('/^[a-z][a-z0-9+.-]*:\/\//i', $value) === 1; + } + + private static function hasSchemeLikePrefix(string $value): bool + { + return preg_match('/^[a-z][a-z0-9+.-]*:/i', $value) === 1; + } +} diff --git a/Okay/Core/Security/ThemeEditorResponse.php b/Okay/Core/Security/ThemeEditorResponse.php new file mode 100644 index 000000000..61a00ec32 --- /dev/null +++ b/Okay/Core/Security/ThemeEditorResponse.php @@ -0,0 +1,44 @@ + $success, + 'error' => $success ? null : ['code' => $code, 'message' => $message], + 'code' => $code, + 'message' => $message, + ]; + } + + public static function statusCode(bool $success, string $code): int + { + if ($success) { + return 200; + } + + return match ($code) { + 'method_not_allowed' => 405, + 'invalid_extension' => 415, + 'theme_locked' => 423, + 'write_failed' => 500, + 'editor_unauthorized', 'editor_disabled', 'csrf_invalid' => 403, + default => 400, + }; + } + + public static function send(Response $response, bool $success, string $code, string $message): void + { + $response->setStatusCode(self::statusCode($success, $code)); + $response->setContent(json_encode(self::payload($success, $code, $message), JSON_UNESCAPED_UNICODE), RESPONSE_JSON); + $response->sendContent(); + } +} diff --git a/Okay/Core/Security/TrustedClientIp.php b/Okay/Core/Security/TrustedClientIp.php new file mode 100644 index 000000000..70d94599b --- /dev/null +++ b/Okay/Core/Security/TrustedClientIp.php @@ -0,0 +1,26 @@ + $server + */ + public static function resolve( + array $server, + ?string $forwardedHeaderName, + ?string $trustedProxyList + ): string { + return TrustedRequestContext::fromServer( + $server, + $trustedProxyList, + $forwardedHeaderName + )->getClientIp(); + } +} diff --git a/Okay/Core/Security/TrustedRequestContext.php b/Okay/Core/Security/TrustedRequestContext.php new file mode 100644 index 000000000..4d0564ba9 --- /dev/null +++ b/Okay/Core/Security/TrustedRequestContext.php @@ -0,0 +1,283 @@ + $server + */ + public static function fromServer( + array $server, + ?string $trustedProxyList = null, + ?string $forwardedClientIpHeader = 'X-Forwarded-For' + ): self { + $remoteAddr = self::serverString($server, 'REMOTE_ADDR'); + $trustedProxies = self::parseTrustedProxies($trustedProxyList); + $trustedPeer = self::isTrustedIp($remoteAddr, $trustedProxies); + + return new self( + self::resolveClientIp($server, $remoteAddr, $trustedProxies, $trustedPeer, $forwardedClientIpHeader), + self::resolveProtocol($server, $trustedPeer) + ); + } + + public function getClientIp(): string + { + return $this->clientIp; + } + + public function getProtocol(): string + { + return $this->protocol; + } + + public function isSecure(): bool + { + return $this->protocol === 'https'; + } + + /** + * @param array $server + * @param list $trustedProxies + */ + private static function resolveClientIp( + array $server, + string $remoteAddr, + array $trustedProxies, + bool $trustedPeer, + ?string $forwardedClientIpHeader + ): string { + if (!$trustedPeer || !self::isForwardedForHeader($forwardedClientIpHeader)) { + return $remoteAddr; + } + + $forwarded = $server[self::FORWARDED_FOR_SERVER_KEY] ?? null; + if (!is_string($forwarded) || trim($forwarded) === '') { + return $remoteAddr; + } + + $hops = explode(',', $forwarded); + + $addresses = []; + foreach ($hops as $hop) { + $address = trim($hop); + if ($address === '' || filter_var($address, FILTER_VALIDATE_IP) === false) { + return $remoteAddr; + } + + $addresses[] = $address; + } + + for ($index = count($addresses) - 1; $index >= 0; $index--) { + if (!self::isTrustedIp($addresses[$index], $trustedProxies)) { + return $addresses[$index]; + } + } + + return $remoteAddr; + } + + /** + * @param array $server + */ + private static function resolveProtocol(array $server, bool $trustedPeer): string + { + $directProtocol = self::resolveDirectProtocol($server); + if (!$trustedPeer) { + return $directProtocol; + } + + $forwardedProto = $server[self::FORWARDED_PROTO_SERVER_KEY] ?? null; + if (is_string($forwardedProto) && trim($forwardedProto) !== '') { + $protocols = explode(',', $forwardedProto); + $normalizedProtocols = []; + foreach ($protocols as $protocol) { + $protocol = strtolower(trim($protocol)); + if (!in_array($protocol, ['http', 'https'], true)) { + return $directProtocol; + } + + $normalizedProtocols[] = $protocol; + } + + return $normalizedProtocols[0]; + } + + $forwardedSsl = strtolower(trim(self::serverString($server, self::FORWARDED_SSL_SERVER_KEY))); + if ($forwardedSsl === 'on') { + return 'https'; + } + if ($forwardedSsl === 'off') { + return 'http'; + } + + return $directProtocol; + } + + /** + * @param array $server + */ + private static function resolveDirectProtocol(array $server): string + { + $serverProtocol = strtolower(self::serverString($server, 'SERVER_PROTOCOL')); + if (str_starts_with($serverProtocol, 'https')) { + return 'https'; + } + + if ((int)($server['SERVER_PORT'] ?? 0) === 443) { + return 'https'; + } + + $https = strtolower(self::serverString($server, 'HTTPS')); + if (in_array($https, ['on', '1'], true)) { + return 'https'; + } + + return 'http'; + } + + private static function isForwardedForHeader(?string $headerName): bool + { + return $headerName === null + || strcasecmp(trim($headerName), 'X-Forwarded-For') === 0; + } + + /** + * @return list + */ + private static function parseTrustedProxies(?string $trustedProxyList): array + { + if (!is_string($trustedProxyList) || trim($trustedProxyList) === '') { + return []; + } + + $result = []; + foreach (preg_split('/[\s,]+/', trim($trustedProxyList)) ?: [] as $proxy) { + $proxy = trim($proxy); + if ($proxy === '' || self::isCatchAllCidr($proxy)) { + continue; + } + + if (filter_var($proxy, FILTER_VALIDATE_IP) !== false || self::isValidCidr($proxy)) { + $result[] = $proxy; + } + } + + return $result; + } + + /** + * @param list $trustedProxies + */ + private static function isTrustedIp(string $ip, array $trustedProxies): bool + { + if (filter_var($ip, FILTER_VALIDATE_IP) === false) { + return false; + } + + foreach ($trustedProxies as $trustedProxy) { + if (str_contains($trustedProxy, '/')) { + if (self::ipInCidr($ip, $trustedProxy)) { + return true; + } + continue; + } + + $ipBinary = inet_pton($ip); + $trustedBinary = inet_pton($trustedProxy); + if ($ipBinary !== false && $ipBinary === $trustedBinary) { + return true; + } + } + + return false; + } + + private static function isValidCidr(string $cidr): bool + { + $parts = explode('/', $cidr, 2); + if (count($parts) !== 2 || filter_var($parts[0], FILTER_VALIDATE_IP) === false) { + return false; + } + + if (!ctype_digit($parts[1])) { + return false; + } + + $binary = inet_pton($parts[0]); + if ($binary === false) { + return false; + } + + $maskBits = (int)$parts[1]; + return $maskBits > 0 && $maskBits <= strlen($binary) * 8; + } + + private static function isCatchAllCidr(string $value): bool + { + return in_array($value, ['0.0.0.0/0', '::/0'], true); + } + + private static function ipInCidr(string $ip, string $cidr): bool + { + $parts = explode('/', $cidr, 2); + if (count($parts) !== 2 || !ctype_digit($parts[1])) { + return false; + } + + $ipBinary = inet_pton($ip); + $subnetBinary = inet_pton($parts[0]); + if ($ipBinary === false || $subnetBinary === false || strlen($ipBinary) !== strlen($subnetBinary)) { + return false; + } + + $maskBits = (int)$parts[1]; + $maxBits = strlen($ipBinary) * 8; + if ($maskBits <= 0 || $maskBits > $maxBits) { + return false; + } + + $bytes = intdiv($maskBits, 8); + $bits = $maskBits % 8; + if ($bytes > 0 && substr($ipBinary, 0, $bytes) !== substr($subnetBinary, 0, $bytes)) { + return false; + } + + if ($bits === 0) { + return true; + } + + $mask = (~((1 << (8 - $bits)) - 1)) & 0xFF; + return (ord($ipBinary[$bytes]) & $mask) === (ord($subnetBinary[$bytes]) & $mask); + } + + /** + * @param array $server + */ + private static function serverString(array $server, string $key): string + { + $value = $server[$key] ?? ''; + + return is_string($value) ? trim($value) : ''; + } +} diff --git a/Okay/Core/SmartyPlugins/Plugins/BackendCompactProductList.php b/Okay/Core/SmartyPlugins/Plugins/BackendCompactProductList.php index bf86dbf7c..c7b75ffa6 100644 --- a/Okay/Core/SmartyPlugins/Plugins/BackendCompactProductList.php +++ b/Okay/Core/SmartyPlugins/Plugins/BackendCompactProductList.php @@ -1,9 +1,7 @@ design = $design; @@ -28,21 +25,21 @@ public function __construct(Design $design, Config $config, Settings $settings) public function run($params) { $isUseModuleDir = $this->design->isUseModuleDir(); - + $this->design->useDefaultDir(); - $this->design->assign('config', $this->config); - $this->design->assign('settings', $this->settings); - $this->design->assign('title', $params['title']); - $this->design->assign('label', $params['label']); + $this->design->assign('config', $this->config); + $this->design->assign('settings', $this->settings); + $this->design->assign('title', $params['title']); + $this->design->assign('label', $params['label']); $this->design->assign('placeholder', $params['placeholder']); - $this->design->assign('name', $params['name']); - $this->design->assign('products', $params['products']); - $this->design->assign('filter', $params['filter']); + $this->design->assign('name', $params['name']); + $this->design->assign('products', $params['products']); + $this->design->assign('filter', $params['filter']); $html = $this->design->fetch('components/compact_product_list.tpl'); - + if ($isUseModuleDir === true) { $this->design->useModuleDir(); } return $html; } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Convert.php b/Okay/Core/SmartyPlugins/Plugins/Convert.php index 6103dc4fa..b591908b3 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Convert.php +++ b/Okay/Core/SmartyPlugins/Plugins/Convert.php @@ -1,20 +1,17 @@ money = $money; @@ -24,4 +21,4 @@ public function run($price, $currency_id = null, $format = true, $revers = false { return $this->money->convert($price, $currency_id, $format, $revers, $precision); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/CssFile.php b/Okay/Core/SmartyPlugins/Plugins/CssFile.php index a5afa5bc2..8a24c1a73 100644 --- a/Okay/Core/SmartyPlugins/Plugins/CssFile.php +++ b/Okay/Core/SmartyPlugins/Plugins/CssFile.php @@ -1,9 +1,7 @@ frontTemplateConfig = $frontTemplateConfig; @@ -25,7 +23,7 @@ public function run($params) { $filename = ''; $dir = null; - + if (!empty($params['filename'])) { $filename = $params['filename']; } elseif (!empty($params['file'])) { @@ -39,7 +37,7 @@ public function run($params) if (!empty($params['backend']) || !empty($params['admin'])) { return $this->backendTemplateConfig->compileIndividualCss($filename, $dir); } - + return $this->frontTemplateConfig->compilePluginIndividualCss($filename, $dir); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Cut.php b/Okay/Core/SmartyPlugins/Plugins/Cut.php index 9f2502482..f67cb39da 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Cut.php +++ b/Okay/Core/SmartyPlugins/Plugins/Cut.php @@ -1,19 +1,17 @@ =0) { - return array_slice($array, $num, count($array)-$num, true); + if ($num >= 0) { + return array_slice($array, $num, count($array) - $num, true); } - return array_slice($array, 0, count($array)+$num, true); + return array_slice($array, 0, count($array) + $num, true); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Date.php b/Okay/Core/SmartyPlugins/Plugins/Date.php index 82a1663ef..f1b1623c6 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Date.php +++ b/Okay/Core/SmartyPlugins/Plugins/Date.php @@ -1,9 +1,7 @@ translations = $entityFactory->get(TranslationsEntity::class); $this->langEntity = $entityFactory->get(LanguagesEntity::class); $this->languages = $languages; - } public function setDateFormat($dateFormat) { $this->dateFormat = $dateFormat; } - - public function run($date, $format = null) + + public function run($date, $format = null) { if (is_numeric($date) || (!$time = strtotime($date))) { $time = $date; } if ($format !== null) { $language = $this->langEntity->get($this->languages->getLangId()); - + $translations = $this->translations->find(['lang' => $language->label]); - + $day_num = date('N', $time); $mon_num = date('n', $time); $custom_format = [ - 'cD' => addcslashes($translations["date_D_".$day_num]->value, 'A..z'), // Дни недели сокращенно - 'cl' => addcslashes($translations["date_l_".$day_num]->value, 'A..z'), // Дни недели полностью - 'cS' => addcslashes($translations["date_S_".$mon_num]->value, 'A..z'), // Месяцы сокращенно - 'cF' => addcslashes($translations["date_F_".$mon_num]->value, 'A..z'), // Месяцы полностью - 'cFR' => addcslashes($translations["date_FR_".$mon_num]->value, 'A..z'), // Месяцы полностью, родительный падеж + 'cD' => addcslashes($translations["date_D_" . $day_num]->value, 'A..z'), // Дни недели сокращенно + 'cl' => addcslashes($translations["date_l_" . $day_num]->value, 'A..z'), // Дни недели полностью + 'cS' => addcslashes($translations["date_S_" . $mon_num]->value, 'A..z'), // Месяцы сокращенно + 'cF' => addcslashes($translations["date_F_" . $mon_num]->value, 'A..z'), // Месяцы полностью + 'cFR' => addcslashes($translations["date_FR_" . $mon_num]->value, 'A..z'), // Месяцы полностью, родительный падеж ]; - + $format = strtr($format, $custom_format); } - + return date(!empty($format) ? $format : $this->dateFormat, $time); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/First.php b/Okay/Core/SmartyPlugins/Plugins/First.php index d45d89b67..7ac1ea0a2 100644 --- a/Okay/Core/SmartyPlugins/Plugins/First.php +++ b/Okay/Core/SmartyPlugins/Plugins/First.php @@ -1,19 +1,17 @@ designBlocks = $designBlocks; @@ -38,14 +35,14 @@ public function run($params) $html .= '
' . $params['block'] . '
'; } $html .= $this->designBlocks->getBlockHtml($params['block']); - + // Очистим все переменные, которые установили выше if (isset($params['vars'])) { foreach ($params['vars'] as $var => $value) { $this->design->assign($var, null); } } - + return $html; } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/GetTheme.php b/Okay/Core/SmartyPlugins/Plugins/GetTheme.php index 88c2eefa8..506a75a9a 100644 --- a/Okay/Core/SmartyPlugins/Plugins/GetTheme.php +++ b/Okay/Core/SmartyPlugins/Plugins/GetTheme.php @@ -1,9 +1,7 @@ frontTemplateConfig = $frontTemplateConfig; @@ -22,4 +20,4 @@ public function run() { return $this->frontTemplateConfig->getTheme(); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/JsFile.php b/Okay/Core/SmartyPlugins/Plugins/JsFile.php index d02a0a256..9ec24e888 100644 --- a/Okay/Core/SmartyPlugins/Plugins/JsFile.php +++ b/Okay/Core/SmartyPlugins/Plugins/JsFile.php @@ -1,9 +1,7 @@ frontTemplateConfig = $frontTemplateConfig; @@ -26,7 +24,7 @@ public function run($params) $filename = ''; $dir = null; $defer = false; - + if (!empty($params['filename'])) { $filename = $params['filename']; } elseif (!empty($params['file'])) { @@ -40,11 +38,11 @@ public function run($params) if (!empty($params['defer'])) { $defer = $params['defer']; } - + if (!empty($params['backend']) || !empty($params['admin'])) { return $this->backendTemplateConfig->compileIndividualJs($filename, $dir, $defer); } - + return $this->frontTemplateConfig->compilePluginIndividualJs($filename, $dir, $defer); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/JsonLdText.php b/Okay/Core/SmartyPlugins/Plugins/JsonLdText.php index b67612452..2aa5a1868 100644 --- a/Okay/Core/SmartyPlugins/Plugins/JsonLdText.php +++ b/Okay/Core/SmartyPlugins/Plugins/JsonLdText.php @@ -1,16 +1,14 @@ "\\\\", @@ -18,4 +16,4 @@ public function run(string $str) : string ]); return trim(htmlspecialchars(strip_tags($str))); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Phone.php b/Okay/Core/SmartyPlugins/Plugins/Phone.php index 62d293c6d..dee598dcf 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Phone.php +++ b/Okay/Core/SmartyPlugins/Plugins/Phone.php @@ -1,20 +1,17 @@ phone = $phone; @@ -24,4 +21,4 @@ public function run($phoneNumber, $numberFormat = null) { return $this->phone->format($phoneNumber, $numberFormat); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Plural.php b/Okay/Core/SmartyPlugins/Plugins/Plural.php index 9e7833d58..fc0ab2f93 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Plural.php +++ b/Okay/Core/SmartyPlugins/Plugins/Plural.php @@ -1,40 +1,38 @@ =11 && $p2<=19)) { + if ($p1 == 1 && !($p2 >= 11 && $p2 <= 19)) { return $singular; } - - if($p1>=2 && $p1<=4 && !($p2>=11 && $p2<=19)) { + + if ($p1 >= 2 && $p1 <= 4 && !($p2 >= 11 && $p2 <= 19)) { return $plural2; - } - + } + return $plural1; - } + } - if($number == 1) { + if ($number == 1) { return $singular; } - - return $plural1; + + return $plural1; } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/ReadSvg.php b/Okay/Core/SmartyPlugins/Plugins/ReadSvg.php index 11b4da246..d6e76e31b 100644 --- a/Okay/Core/SmartyPlugins/Plugins/ReadSvg.php +++ b/Okay/Core/SmartyPlugins/Plugins/ReadSvg.php @@ -1,22 +1,19 @@ config = $config; @@ -27,16 +24,16 @@ public function run($filename, $resizedDir = null) if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) != 'svg') { return ''; } - + if (empty($resizedDir)) { $resizedDir = $this->config->get('resized_images_dir'); } - + $file = $this->config->get('root_dir') . $resizedDir . $filename; if (file_exists($file)) { return file_get_contents($file); } - + return ''; } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Resize.php b/Okay/Core/SmartyPlugins/Plugins/Resize.php index 22252e06f..f7a4524c9 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Resize.php +++ b/Okay/Core/SmartyPlugins/Plugins/Resize.php @@ -1,20 +1,17 @@ image = $image; @@ -24,4 +21,4 @@ public function run($filename, $width = 0, $height = 0, $setWatermark = false, $ { return $this->image->getResizeModifier($filename, $width, $height, $setWatermark, $resizedDir, $cropPositionX, $cropPositionY); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Time.php b/Okay/Core/SmartyPlugins/Plugins/Time.php index 64f2fa06b..c8d94bde6 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Time.php +++ b/Okay/Core/SmartyPlugins/Plugins/Time.php @@ -1,9 +1,7 @@ config = $config; } - public function run($text) + public function run($text) { return $this->config->token($text); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Url.php b/Okay/Core/SmartyPlugins/Plugins/Url.php index 56e206181..de4e7773c 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Url.php +++ b/Okay/Core/SmartyPlugins/Plugins/Url.php @@ -1,9 +1,7 @@ -request->url(reset($params)); } - return $this->request->url($params); + return $this->request->url($params); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/UrlGenerator.php b/Okay/Core/SmartyPlugins/Plugins/UrlGenerator.php index 105f7956c..c1b023eef 100644 --- a/Okay/Core/SmartyPlugins/Plugins/UrlGenerator.php +++ b/Okay/Core/SmartyPlugins/Plugins/UrlGenerator.php @@ -1,16 +1,14 @@ -router->generateUrl($routeName, $params, $isAbsolute, $langId); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/Plugins/Webp.php b/Okay/Core/SmartyPlugins/Plugins/Webp.php index aba7649b1..4dde7093c 100644 --- a/Okay/Core/SmartyPlugins/Plugins/Webp.php +++ b/Okay/Core/SmartyPlugins/Plugins/Webp.php @@ -1,9 +1,7 @@ image = $image; @@ -21,4 +19,4 @@ public function run($filename) { return $this->image->convertFilenameToWebp($filename); } -} \ No newline at end of file +} diff --git a/Okay/Core/SmartyPlugins/SmartyPlugins.php b/Okay/Core/SmartyPlugins/SmartyPlugins.php index 77075d1e7..132e4ebe1 100644 --- a/Okay/Core/SmartyPlugins/SmartyPlugins.php +++ b/Okay/Core/SmartyPlugins/SmartyPlugins.php @@ -1,9 +1,7 @@ get($plugin['class']); $p->register($DI->get(Design::class), $DI->get(Module::class)); } - diff --git a/Okay/Core/TemplateConfig.php b/Okay/Core/TemplateConfig.php index 2a4aeddd9..ad36bbc90 100644 --- a/Okay/Core/TemplateConfig.php +++ b/Okay/Core/TemplateConfig.php @@ -1,16 +1,13 @@ frontTemplateConfig = $frontTemplateConfig; @@ -23,5 +20,4 @@ public function getTheme() trigger_error('Method ' . __METHOD__ . ' is deprecated. Please use Okay\Core\TemplateConfig\FrontTemplateConfig::getTheme()', E_USER_DEPRECATED); return $this->frontTemplateConfig->getTheme(); } - } diff --git a/Okay/Core/TemplateConfig/Js.php b/Okay/Core/TemplateConfig/Js.php index af28983c0..3064eac79 100644 --- a/Okay/Core/TemplateConfig/Js.php +++ b/Okay/Core/TemplateConfig/Js.php @@ -1,9 +1,7 @@ defer = $defer; return $this; } - + /** * @return mixed */ @@ -27,5 +25,4 @@ public function getDefer() { return $this->defer; } - -} \ No newline at end of file +} diff --git a/Okay/Core/TplMod/Nodes/HtmlCommentNode.php b/Okay/Core/TplMod/Nodes/HtmlCommentNode.php index 8a54c3918..840c4d07c 100644 --- a/Okay/Core/TplMod/Nodes/HtmlCommentNode.php +++ b/Okay/Core/TplMod/Nodes/HtmlCommentNode.php @@ -1,10 +1,7 @@ parser = $parser; @@ -25,20 +23,20 @@ public function __construct(Parser $parser, Config $config) public function buildFile($content, $mods) { $SL = ServiceLocator::getInstance(); - + /** @var Config $config */ $config = $SL->getService(Config::class); - + if ($config->get('disable_tpl_mod')) { return $content; // todo отключение модификаторов } - + $res = $this->parser->parse($content); - + $this->walkByFile($res, $mods); - + //print $this->build($res);die; // todo вывод содержимого файла - + return $this->build($res); } @@ -52,11 +50,11 @@ private function walkByFile(BaseNode $node, array $changes) foreach ($changes as $changeDTO) { if (!empty($changeDTO->getFind()) && strpos($node->getOriginalElement(), $changeDTO->getFind()) !== false) { $this->applyMod($node, $changeDTO); - } elseif (!empty($changeDTO->getLike()) && preg_match('~'.$changeDTO->getLike().'~', $node->getOriginalElement())) { + } elseif (!empty($changeDTO->getLike()) && preg_match('~' . $changeDTO->getLike() . '~', $node->getOriginalElement())) { $this->applyMod($node, $changeDTO); } } - + if ($node->children()) { foreach ($node->children() as $child) { $this->walkByFile($child, $changes); @@ -70,7 +68,7 @@ private function applyMod(BaseNode $node, TplChangeDTO $changeDTO) if ($changeDTO->isParent()) { $node = $node->parent(); } - + if (!empty($changeDTO->getClosestFind())) { while ($node = $node->parent()) { if (strpos($node->getOriginalElement(), $changeDTO->getClosestFind()) !== false) { @@ -79,12 +77,12 @@ private function applyMod(BaseNode $node, TplChangeDTO $changeDTO) } } elseif (!empty($changeDTO->getClosestLike())) { while ($node = $node->parent()) { - if (preg_match('~'.$changeDTO->getClosestLike().'~', $node->getOriginalElement())) { + if (preg_match('~' . $changeDTO->getClosestLike() . '~', $node->getOriginalElement())) { break; } } } - + if (!empty($changeDTO->getChildrenFind())) { if ($childNode = $this->findChildNode($node, $changeDTO->getChildrenFind())) { $node = $childNode; @@ -98,7 +96,7 @@ private function applyMod(BaseNode $node, TplChangeDTO $changeDTO) return; } } - + if (!empty($changeDTO->getAppend())) { $userNode = new TextNode($changeDTO->getAppend()); if ($this->debug === true && !empty($changeDTO->getComment())) { @@ -120,7 +118,7 @@ private function applyMod(BaseNode $node, TplChangeDTO $changeDTO) $node->appendBefore(new HtmlCommentNode("")); } } - + if (!empty($changeDTO->getPrepend())) { $userNode = new TextNode($changeDTO->getPrepend()); if ($this->debug === true && !empty($changeDTO->getComment())) { @@ -168,7 +166,7 @@ private function applyMod(BaseNode $node, TplChangeDTO $changeDTO) } unset($node); } - + private function findChildNode(BaseNode $node, $search) { $result = false; @@ -184,13 +182,13 @@ private function findChildNode(BaseNode $node, $search) } return $result; } - + private function likeChildNode(BaseNode $node, $search) { $result = false; if ($children = $node->children()) { foreach ($children as $child) { - if (preg_match('~'.$search.'~', $child->getOriginalElement())) { + if (preg_match('~' . $search . '~', $child->getOriginalElement())) { return $child; } if ($result = $this->likeChildNode($child, $search)) { @@ -200,7 +198,7 @@ private function likeChildNode(BaseNode $node, $search) } return $result; } - + private function build(BaseNode $node, $level = 0): string { $resultString = ''; @@ -208,9 +206,9 @@ private function build(BaseNode $node, $level = 0): string foreach ($node->children() as $child) { if (strpos($node->getOriginalElement(), 'getElement(); if (!empty($child->children())) { - $resultString .= $this->build($child, $level+1); + $resultString .= $this->build($child, $level + 1); } if (!empty($child->getCloseTag())) { @@ -231,9 +229,7 @@ private function build(BaseNode $node, $level = 0): string } $resultString .= $child->getCloseTag(); } - } return $resultString; } - -} \ No newline at end of file +} diff --git a/Okay/Core/config/constants.php b/Okay/Core/config/constants.php index cd54a3863..94ec4a155 100644 --- a/Okay/Core/config/constants.php +++ b/Okay/Core/config/constants.php @@ -36,4 +36,4 @@ // Настройки const ROBOTS_INDEX_FOLLOW = 1; const ROBOTS_NOINDEX_FOLLOW = 2; -const ROBOTS_NOINDEX_NOFOLLOW = 3; \ No newline at end of file +const ROBOTS_NOINDEX_NOFOLLOW = 3; diff --git a/Okay/Core/config/parameters.php b/Okay/Core/config/parameters.php index f1bd172d1..cc9d1a17f 100644 --- a/Okay/Core/config/parameters.php +++ b/Okay/Core/config/parameters.php @@ -6,7 +6,7 @@ * ВАЖНО! директивы вида {%var%} нужно передавать через методы конфигураторы. * Например, если передать параметр через конструктор (в блоке arguments), то такие параметры не будут заменены * на settings. Такие директивы нужно передавать через дополнительный метод, указанный в блоке calls - * + * * Money::class => [ 'class' => Money::class, 'arguments' => [ @@ -55,7 +55,7 @@ 'compile_css_dir' => 'cache/css/', 'compile_js_dir' => 'cache/js/', ], - + /** * Настройки адаптеров системы. Адаптер это по сути класс, который лежит в Okay\Core\Adapters\XXX * Где XXX уже подвид адаптеров diff --git a/Okay/Core/config/requests.php b/Okay/Core/config/requests.php index c7438a030..3da33e517 100644 --- a/Okay/Core/config/requests.php +++ b/Okay/Core/config/requests.php @@ -1,6 +1,5 @@ $pageRouteParams->getDefaults(), ], -]; \ No newline at end of file +]; diff --git a/Okay/Entities/CouponsEntity.php b/Okay/Entities/CouponsEntity.php index a2af5f14f..0f522eaf5 100644 --- a/Okay/Entities/CouponsEntity.php +++ b/Okay/Entities/CouponsEntity.php @@ -1,14 +1,11 @@ select->where($validFilter); } - } diff --git a/Okay/Entities/DiscountsEntity.php b/Okay/Entities/DiscountsEntity.php index 82b1783b8..16a77016b 100644 --- a/Okay/Entities/DiscountsEntity.php +++ b/Okay/Entities/DiscountsEntity.php @@ -1,9 +1,7 @@ serviceLocator->getService(Translit::class); - + $alias = (array) $alias; if (empty($alias['variable'])) { $alias['variable'] = $translit->translit($alias['name']); @@ -45,9 +43,9 @@ public function add($alias) // Если есть склонение с такой переменной, добавляем к ней число while ($this->get((string)$alias['variable'])) { if (preg_match('/(.+)_([0-9]+)$/', $alias['variable'], $parts)) { - $alias['variable'] = $parts[1].'_'.($parts[2]+1); + $alias['variable'] = $parts[1] . '_' . ($parts[2] + 1); } else { - $alias['variable'] = $alias['variable'].'_2'; + $alias['variable'] = $alias['variable'] . '_2'; } } return parent::add($alias); @@ -68,5 +66,4 @@ public function delete($ids) return parent::delete($ids); } - -} \ No newline at end of file +} diff --git a/Okay/Entities/FeaturesAliasesValuesEntity.php b/Okay/Entities/FeaturesAliasesValuesEntity.php index 69551f526..515e74425 100644 --- a/Okay/Entities/FeaturesAliasesValuesEntity.php +++ b/Okay/Entities/FeaturesAliasesValuesEntity.php @@ -1,9 +1,7 @@ select->join('LEFT', '__features_aliases AS fa', 'fa.id=f.feature_alias_id'); return parent::find($filter); } - -} \ No newline at end of file +} diff --git a/Okay/Entities/FeaturesValuesAliasesValuesEntity.php b/Okay/Entities/FeaturesValuesAliasesValuesEntity.php index dc0497e07..1d1d0ae2d 100644 --- a/Okay/Entities/FeaturesValuesAliasesValuesEntity.php +++ b/Okay/Entities/FeaturesValuesAliasesValuesEntity.php @@ -1,9 +1,7 @@ get((int)$id)) { $filename = $image->filename; - + parent::delete($id); - + // Если это изображение не используется у других товаров, удалим и файлы if ($this->count(['filename' => $filename]) == 0) { $file = pathinfo($filename, PATHINFO_FILENAME); @@ -47,13 +44,13 @@ public function delete($ids) // Удалить все ресайзы $resizedImages = glob($this->config->root_dir . $this->config->resized_images_dir . $file . '.*x*.' . $ext); - if(is_array($resizedImages)) { + if (is_array($resizedImages)) { foreach ($resizedImages as $f) { @unlink($f); } } $resizedImagesWebp = glob($this->config->root_dir . $this->config->resized_images_dir . $file . '.*x*.' . $ext . '.webp'); - if(is_array($resizedImagesWebp)) { + if (is_array($resizedImagesWebp)) { foreach ($resizedImagesWebp as $f) { @unlink($f); } @@ -66,5 +63,4 @@ public function delete($ids) return ExtenderFacade::execute([static::class, __FUNCTION__], null, func_get_args()); } - -} \ No newline at end of file +} diff --git a/Okay/Entities/LessonsEntity.php b/Okay/Entities/LessonsEntity.php index 920461094..58df41958 100644 --- a/Okay/Entities/LessonsEntity.php +++ b/Okay/Entities/LessonsEntity.php @@ -1,15 +1,12 @@ setUp(); $this->select->distinct(true); $this->select->cols($this->getAllFields()); - + $this->select->where('order_id in (:order_id)') ->bindValues([ 'order_id' => $ordersIds, ]); - - $this->select->where('id = (SELECT MAX(id) FROM '.self::getTable().' t WHERE t.order_id in (:sub_order_id) AND oh.order_id=t.order_id LIMIT 1)') + + $this->select->where('id = (SELECT MAX(id) FROM ' . self::getTable() . ' t WHERE t.order_id in (:sub_order_id) AND oh.order_id=t.order_id LIMIT 1)') ->bindValues([ 'sub_order_id' => $ordersIds, ]); @@ -48,5 +46,4 @@ public function getOrdersLastChanges($ordersIds) $results = $this->getResults(null, 'order_id'); return ExtenderFacade::execute([static::class, __FUNCTION__], $results, func_get_args()); } - } diff --git a/Okay/Entities/OrderStatusEntity.php b/Okay/Entities/OrderStatusEntity.php index 9eddbee01..474df54f9 100644 --- a/Okay/Entities/OrderStatusEntity.php +++ b/Okay/Entities/OrderStatusEntity.php @@ -1,15 +1,12 @@ $additionalFields + * @return bool|null True for the winner, false for an already-paid order, + * and null when the conditional update failed. + */ + public function markPaidIfUnpaid(int $orderId, array $additionalFields = []): ?bool + { + if ($orderId <= 0) { + return false; + } + + $update = $this->queryFactory->newUpdate(); + $update->table($this->getTable()) + ->set('paid', 1) + ->set('payment_date', 'now()') + ->where('id=:payment_order_id') + ->where('paid=0') + ->bindValue('payment_order_id', $orderId); + + foreach ($additionalFields as $field => $value) { + $update->set($field, $value); + } + + if (!$this->db->query($update)) { + return null; + } + if ($this->db->affectedRows() !== 1) { + return false; + } + + $this->markedPaid([$orderId], true); + $this->afterMarkedPaidUpdate([$orderId], true); + + return true; + } + /** * Метод вызывается при отметке или снятии отметки заказов как оплаченных перед обновлением в БД * @param list $ids массив айди заказов только при изменении статуса оплаты diff --git a/Okay/Entities/PaymentsEntity.php b/Okay/Entities/PaymentsEntity.php index 522a73e4a..ac38e26d2 100644 --- a/Okay/Entities/PaymentsEntity.php +++ b/Okay/Entities/PaymentsEntity.php @@ -85,7 +85,10 @@ public function getPaymentSettings($methodId) $result = $this->db->result('settings'); $settings = []; if (!empty($result) && is_string($result)) { - $settings = unserialize($result); + $decoded = @unserialize($result, ['allowed_classes' => false]); + if (is_array($decoded)) { + $settings = $decoded; + } } return ExtenderFacade::execute([static::class, __FUNCTION__], $settings, func_get_args()); diff --git a/Okay/Entities/SEOFilterPatternsEntity.php b/Okay/Entities/SEOFilterPatternsEntity.php index 4c509b652..6782d2fe7 100644 --- a/Okay/Entities/SEOFilterPatternsEntity.php +++ b/Okay/Entities/SEOFilterPatternsEntity.php @@ -1,14 +1,11 @@ setUp(); $this->select->cols($this->getAllFields())->limit(1); - $this->db->query($this->select); + $this->db->query($this->select); $info = $this->getResult(); if (!empty($info)) { @@ -60,7 +58,7 @@ public function getInfo() private function clearInfo() { $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("TRUNCATE ".self::getTable()); + $sql->setStatement("TRUNCATE " . self::getTable()); $result = (bool) $this->db->query($sql); return ExtenderFacade::execute([static::class, __FUNCTION__], $result, func_get_args()); } diff --git a/Okay/Entities/UserBrowsedProductsEntity.php b/Okay/Entities/UserBrowsedProductsEntity.php index 08673c808..f8a880b6d 100644 --- a/Okay/Entities/UserBrowsedProductsEntity.php +++ b/Okay/Entities/UserBrowsedProductsEntity.php @@ -1,9 +1,7 @@ count(['user_id' => $userId]) > 0) { - $query = $this->queryFactory->newSqlQuery(); $query->setStatement('SET @i=0;')->execute(); - + $select = $this->queryFactory->newSelect(); $select->from(self::getTable()) ->cols(['product_id']) @@ -38,13 +35,13 @@ public function sliceToLimit($userId, $limit = 100) ->bindValue('user_id', $userId) ->bindValue('limit', $limit) ->orderBy(['id DESC']); - + if ($productsToDelete = $select->results('product_id')) { $this->deleteByProductId($userId, $productsToDelete); } } } - + private function deleteByProductId($userId, $productsIds) { diff --git a/Okay/Entities/UserCartItemsEntity.php b/Okay/Entities/UserCartItemsEntity.php index 747192271..a9709c1ee 100644 --- a/Okay/Entities/UserCartItemsEntity.php +++ b/Okay/Entities/UserCartItemsEntity.php @@ -1,9 +1,7 @@ col('id')->findOne(['user_id' => $userId, 'variant_id' => $variantId])) { $this->update($id, ['amount' => $amount]); @@ -36,7 +34,7 @@ public function updateAmount($userId, $variantId, $amount) ]); } } - + public function deleteByVariantId($userId, $variantsIds) { @@ -48,5 +46,4 @@ public function deleteByVariantId($userId, $variantsIds) ->bindValue('user_id', $userId); $this->db->query($delete); } - } diff --git a/Okay/Entities/UserComparisonItemsEntity.php b/Okay/Entities/UserComparisonItemsEntity.php index d7d9662ff..88dbadbaf 100644 --- a/Okay/Entities/UserComparisonItemsEntity.php +++ b/Okay/Entities/UserComparisonItemsEntity.php @@ -1,9 +1,7 @@ bindValue('user_id', $userId); $this->db->query($delete); } - } diff --git a/Okay/Entities/UserGroupsEntity.php b/Okay/Entities/UserGroupsEntity.php index b257054fc..58547de3b 100644 --- a/Okay/Entities/UserGroupsEntity.php +++ b/Okay/Entities/UserGroupsEntity.php @@ -1,9 +1,7 @@ db->query($update); } - + return parent::delete($ids); } - } diff --git a/Okay/Entities/UserWishlistItemsEntity.php b/Okay/Entities/UserWishlistItemsEntity.php index cee5e89e5..046f66943 100644 --- a/Okay/Entities/UserWishlistItemsEntity.php +++ b/Okay/Entities/UserWishlistItemsEntity.php @@ -1,9 +1,7 @@ "\n'{$this->name}'\n" ]); } -} \ No newline at end of file +} diff --git a/Okay/Helpers/AiRequests/AiCategoryRequest.php b/Okay/Helpers/AiRequests/AiCategoryRequest.php index ec4aaa065..4212639a8 100644 --- a/Okay/Helpers/AiRequests/AiCategoryRequest.php +++ b/Okay/Helpers/AiRequests/AiCategoryRequest.php @@ -2,13 +2,12 @@ namespace Okay\Helpers\AiRequests; - class AiCategoryRequest extends AbstractAiRequest { public const ENTITY_TYPE = 'category'; public const FIELD_META_TITLE = 'meta_title'; public const FIELD_META_DESCRIPTION = 'meta_description'; - public const FIELD_META_KEYWORDS= 'meta_keywords'; + public const FIELD_META_KEYWORDS = 'meta_keywords'; public const FIELD_ANNOTATION = 'annotation'; public const FIELD_DESCRIPTION = 'description'; @@ -35,4 +34,4 @@ public function getRequestText(string $field): string '{$category}' => "\n'{$this->name}'\n" ]); } -} \ No newline at end of file +} diff --git a/Okay/Helpers/AiRequests/AiProductRequest.php b/Okay/Helpers/AiRequests/AiProductRequest.php index 86bc54ac0..348742853 100644 --- a/Okay/Helpers/AiRequests/AiProductRequest.php +++ b/Okay/Helpers/AiRequests/AiProductRequest.php @@ -12,7 +12,7 @@ class AiProductRequest extends AbstractAiRequest public const ENTITY_TYPE = 'product'; public const FIELD_META_TITLE = 'meta_title'; public const FIELD_META_DESCRIPTION = 'meta_description'; - public const FIELD_META_KEYWORDS= 'meta_keywords'; + public const FIELD_META_KEYWORDS = 'meta_keywords'; public const FIELD_ANNOTATION = 'annotation'; public const FIELD_DESCRIPTION = 'description'; @@ -82,4 +82,4 @@ public function getAdditionalInfo(): string { return $this->additionalInfo; } -} \ No newline at end of file +} diff --git a/Okay/Helpers/CommentsHelper.php b/Okay/Helpers/CommentsHelper.php index bda92a943..61da34a99 100644 --- a/Okay/Helpers/CommentsHelper.php +++ b/Okay/Helpers/CommentsHelper.php @@ -8,6 +8,7 @@ use Okay\Core\Modules\Extender\ExtenderFacade; use Okay\Core\Notify; use Okay\Core\Response; +use Okay\Core\Security\SafeRedirect; use Okay\Entities\BlogEntity; use Okay\Entities\CommentsEntity; use Okay\Entities\ProductsEntity; @@ -204,7 +205,7 @@ public function addCommentProcedure($objectType, $objectId) ExtenderFacade::execute(__METHOD__, $commentId, func_get_args()); - Response::redirectTo($_SERVER['REQUEST_URI'] . '#comment_' . $commentId); + Response::redirectTo(SafeRedirect::sameOriginPath((string) ($_SERVER['REQUEST_URI'] ?? '/')) . '#comment_' . $commentId); } } } diff --git a/Okay/Helpers/CommonHelper.php b/Okay/Helpers/CommonHelper.php index 3ea3e62fe..cadbae073 100644 --- a/Okay/Helpers/CommonHelper.php +++ b/Okay/Helpers/CommonHelper.php @@ -1,9 +1,7 @@ entityFactory = $entityFactory; $this->userHelper = $userHelper; } - + public function rootPostProcedure() { if (($callback = $this->commonRequest->postCallback()) !== null) { @@ -74,4 +72,4 @@ public function rootPostProcedure() ExtenderFacade::execute(__METHOD__, null, func_get_args()); } -} \ No newline at end of file +} diff --git a/Okay/Helpers/ComparisonHelper.php b/Okay/Helpers/ComparisonHelper.php index a845199d3..c98ef2403 100644 --- a/Okay/Helpers/ComparisonHelper.php +++ b/Okay/Helpers/ComparisonHelper.php @@ -1,9 +1,7 @@ get((string) $couponCode); $couponsEntity->update($coupon->id, [ - 'usages' => $coupon->usages+1 + 'usages' => $coupon->usages + 1 ]); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); } -} \ No newline at end of file +} diff --git a/Okay/Helpers/MainHelper.php b/Okay/Helpers/MainHelper.php index fb2855af5..1b5f91937 100644 --- a/Okay/Helpers/MainHelper.php +++ b/Okay/Helpers/MainHelper.php @@ -17,7 +17,6 @@ use Okay\Core\Request; use Okay\Core\Response; use Okay\Core\Router; -use Okay\Core\Security\CheckoutToken; use Okay\Core\Security\CustomerCsrfToken; use Okay\Core\ServiceLocator; use Okay\Core\Settings; @@ -254,7 +253,6 @@ public function setDesignDataProcedure() $customerCsrfToken = CustomerCsrfToken::get(); $design->assign('customer_csrf_token', $customerCsrfToken); $design->assignJsVar('customer_csrf_token', $customerCsrfToken); - $design->assign('checkout_token', CheckoutToken::get()); $design->assign('is_mobile', $design->isMobile()); $design->assign('is_tablet', $design->isTablet()); diff --git a/Okay/Helpers/MetadataHelpers/CartMetadataHelper.php b/Okay/Helpers/MetadataHelpers/CartMetadataHelper.php index 52e67ae6f..60f8cdae6 100644 --- a/Okay/Helpers/MetadataHelpers/CartMetadataHelper.php +++ b/Okay/Helpers/MetadataHelpers/CartMetadataHelper.php @@ -1,9 +1,7 @@ compileMetadata($translations->getTranslation('cart_title')); return ExtenderFacade::execute(__METHOD__, $metaTitle, func_get_args()); } - + public function getMetaKeywords(): string { return ExtenderFacade::execute(__METHOD__, '', func_get_args()); } - + public function getMetaDescription(): string { return ExtenderFacade::execute(__METHOD__, '', func_get_args()); } - -} \ No newline at end of file +} diff --git a/Okay/Helpers/MetadataHelpers/MetadataInterface.php b/Okay/Helpers/MetadataHelpers/MetadataInterface.php index 6d1589542..7dd132793 100644 --- a/Okay/Helpers/MetadataHelpers/MetadataInterface.php +++ b/Okay/Helpers/MetadataHelpers/MetadataInterface.php @@ -1,9 +1,7 @@ request->post('captcha_code', 'string'); - $error = null; + $error = $this->getCustomerCsrfError($this->request->post('customer_csrf_token')); + if ($error !== null) { + return ExtenderFacade::execute(__METHOD__, $error, func_get_args()); + } + if (!$this->validator->isName($comment->name, true)) { $error = 'empty_name'; } elseif (!$this->validator->isComment($comment->text, true)) { diff --git a/Okay/Helpers/WishListHelper.php b/Okay/Helpers/WishListHelper.php index 32b7f431a..263fb8f75 100644 --- a/Okay/Helpers/WishListHelper.php +++ b/Okay/Helpers/WishListHelper.php @@ -1,10 +1,7 @@ design = $design; $this->wishList = $wishList; $this->frontTemplateConfig = $frontTemplateConfig; $this->logger = $logger; } - + public function getAjaxWishListResult() { $this->design->assign('wishlist', $this->wishList->get()); - + $result = []; - + if (is_file('design/' . $this->frontTemplateConfig->getTheme() . '/html/wishlist_informer.tpl')) { $result['wishlist_informer'] = $this->design->fetch('wishlist_informer.tpl'); } else { @@ -51,5 +48,4 @@ public function getAjaxWishListResult() return ExtenderFacade::execute(__METHOD__, $result, func_get_args()); } - -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/AdminGuide/Backend/Controllers/AdminGuideAdmin.php b/Okay/Modules/OkayCMS/AdminGuide/Backend/Controllers/AdminGuideAdmin.php index 4d0a10011..ff161f1f9 100644 --- a/Okay/Modules/OkayCMS/AdminGuide/Backend/Controllers/AdminGuideAdmin.php +++ b/Okay/Modules/OkayCMS/AdminGuide/Backend/Controllers/AdminGuideAdmin.php @@ -1,9 +1,7 @@ response->setContent($this->design->fetch('description.tpl')); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/AdminGuide/Init/Init.php b/Okay/Modules/OkayCMS/AdminGuide/Init/Init.php index b282e9fd1..d4084fbef 100644 --- a/Okay/Modules/OkayCMS/AdminGuide/Init/Init.php +++ b/Okay/Modules/OkayCMS/AdminGuide/Init/Init.php @@ -1,12 +1,9 @@ registerBackendController('AdminGuideAdmin'); $this->addBackendControllerPermission('AdminGuideAdmin', 'okaycms__admin_guide'); } - -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/AutoDeploy/Backend/Controllers/AutoDeployAdmin.php b/Okay/Modules/OkayCMS/AutoDeploy/Backend/Controllers/AutoDeployAdmin.php index 31d0d8e26..ef1730a63 100644 --- a/Okay/Modules/OkayCMS/AutoDeploy/Backend/Controllers/AutoDeployAdmin.php +++ b/Okay/Modules/OkayCMS/AutoDeploy/Backend/Controllers/AutoDeployAdmin.php @@ -1,9 +1,7 @@ settings->get('deploy_build_key'); if (empty($currentBuildKey)) { - $this->settings->set('deploy_build_key', md5(microtime())); + $this->settings->set('deploy_build_key', bin2hex(random_bytes(32))); } - + $this->design->assign('new_migrations', $deployHelper->getNewMigrations()); - + $this->response->setContent($this->design->fetch('auto_deploy.tpl')); } - + public function createMigration(DeployHelper $deployHelper) { $result = false; - + if ($migrationName = $this->request->post('migration_name')) { $migrationName = str_replace(' ', '_', $migrationName); $migrationName = Translit::translit($migrationName); $result = $deployHelper->createMigration($migrationName); } - + $this->response->setContent(json_encode($result), RESPONSE_JSON); } - + public function saveChannel() { $this->settings->set('deploy_build_channel', $this->request->post('channel')); $this->response->setContent(true, RESPONSE_JSON); } - + public function executeMigrations(DeployHelper $deployHelper) { $deployHelper->executeMigrations(); $this->response->setContent(true, RESPONSE_JSON); } - + public function updateProject(DeployHelper $deployHelper) { $channel = $this->settings->get('deploy_build_channel'); @@ -57,4 +55,4 @@ public function updateProject(DeployHelper $deployHelper) } Response::redirectTo(Request::getDomainWithProtocol() . $this->request->url(['controller' => 'OkayCMS.AutoDeploy.AutoDeployAdmin'])); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/AutoDeploy/Controllers/BuildController.php b/Okay/Modules/OkayCMS/AutoDeploy/Controllers/BuildController.php index ae0ef4be2..77fba6131 100644 --- a/Okay/Modules/OkayCMS/AutoDeploy/Controllers/BuildController.php +++ b/Okay/Modules/OkayCMS/AutoDeploy/Controllers/BuildController.php @@ -1,9 +1,7 @@ set('deploy_last_status_text', ""); - + $currentBuildKey = $settings->get('deploy_build_key'); if (empty($currentBuildKey)) { $settings->set('deploy_last_status_text', "Empty deploy build key. Deploy is stopped!"); return false; - } elseif ($buildKey != $currentBuildKey) { + } elseif (!hash_equals((string) $currentBuildKey, (string) $buildKey)) { $settings->set('deploy_last_status_text', "Build key \"{$buildKey}\" is wrong. Deploy is stopped!"); return false; } + $currentChannel = $settings->get('deploy_build_channel'); + if ( + is_string($currentChannel) + && $currentChannel !== '' + && $channel !== $currentChannel + ) { + $settings->set('deploy_last_status_text', "Channel \"{$channel}\" is wrong. Deploy is stopped!"); + return false; + } + $deployHelper->executeHook($channel); - + $response->setContent('OK', RESPONSE_TEXT); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/AutoDeploy/Entities/MigrationsEntity.php b/Okay/Modules/OkayCMS/AutoDeploy/Entities/MigrationsEntity.php index 266f85c64..8206dc8f0 100644 --- a/Okay/Modules/OkayCMS/AutoDeploy/Entities/MigrationsEntity.php +++ b/Okay/Modules/OkayCMS/AutoDeploy/Entities/MigrationsEntity.php @@ -1,9 +1,7 @@ [ 'slug' => '/build_project/{$channel}/{$buildKey}', 'patterns' => [ - '{$buildKey}' => '([a-f0-9]{32})', + '{$buildKey}' => '([a-f0-9]{32,64})', ], 'params' => [ 'controller' => __NAMESPACE__ . '\Controllers\BuildController', @@ -14,4 +14,4 @@ ], 'always_active' => true, ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/AutoDeploy/Init/services.php b/Okay/Modules/OkayCMS/AutoDeploy/Init/services.php index 16101389e..163ac60ed 100644 --- a/Okay/Modules/OkayCMS/AutoDeploy/Init/services.php +++ b/Okay/Modules/OkayCMS/AutoDeploy/Init/services.php @@ -1,9 +1,7 @@ entity->get(BannersImagesEntity::class); - $bannersImagesIds = $bannersImagesEntity->cols(['id'])->find(['banner_id'=>$ids]); + $bannersImagesIds = $bannersImagesEntity->cols(['id'])->find(['banner_id' => $ids]); $bannersImagesEntity->delete($bannersImagesIds); return parent::delete($ids); } - + protected function filter__show_on($showOnEntitiesIds) { - foreach($showOnEntitiesIds as $entityField=>$values) { - if(empty($values)) { + foreach ($showOnEntitiesIds as $entityField => $values) { + if (empty($values)) { unset($showOnEntitiesIds[$entityField]); continue; } - + $showFilterArray[$entityField] = " {$entityField} = :{$entityField}_full OR {$entityField} LIKE :{$entityField}_prefix @@ -70,7 +67,6 @@ protected function filter__show_on($showOnEntitiesIds) ]); } $showFilterArray[] = "show_all_pages=1"; - $this->select->where('(' . implode(' OR ',$showFilterArray) . ')'); + $this->select->where('(' . implode(' OR ', $showFilterArray) . ')'); } - } diff --git a/Okay/Modules/OkayCMS/Banners/Entities/BannersImagesEntity.php b/Okay/Modules/OkayCMS/Banners/Entities/BannersImagesEntity.php index 8956a119a..fcf193519 100644 --- a/Okay/Modules/OkayCMS/Banners/Entities/BannersImagesEntity.php +++ b/Okay/Modules/OkayCMS/Banners/Entities/BannersImagesEntity.php @@ -1,9 +1,7 @@ [ 'imagesDir' => '{$banners_images_dir}', ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/Banners/Init/services.php b/Okay/Modules/OkayCMS/Banners/Init/services.php index 5b7cde1a7..fd75338d6 100644 --- a/Okay/Modules/OkayCMS/Banners/Init/services.php +++ b/Okay/Modules/OkayCMS/Banners/Init/services.php @@ -1,9 +1,7 @@ request = $request; } - + public function postBannerImage() { $bannersImage = new \stdClass(); @@ -64,7 +61,7 @@ public function mobileFileImage() $image = $this->request->files('image_mobile'); return ExtenderFacade::execute(__METHOD__, $image, func_get_args()); } - + public function postCheck() { $check = (array) $this->request->post('check'); @@ -82,4 +79,4 @@ public function postPositions() $positions = $this->request->post('positions'); return ExtenderFacade::execute(__METHOD__, $positions, func_get_args()); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/Banners/design/css.php b/Okay/Modules/OkayCMS/Banners/design/css.php index 263af619a..9a31ee742 100644 --- a/Okay/Modules/OkayCMS/Banners/design/css.php +++ b/Okay/Modules/OkayCMS/Banners/design/css.php @@ -5,4 +5,3 @@ return [ (new Css('banners.css')), ]; - diff --git a/Okay/Modules/OkayCMS/Banners/design/css/banners.css b/Okay/Modules/OkayCMS/Banners/design/css/banners.css index aa241df09..f1ef11404 100644 --- a/Okay/Modules/OkayCMS/Banners/design/css/banners.css +++ b/Okay/Modules/OkayCMS/Banners/design/css/banners.css @@ -227,7 +227,7 @@ font-weight: 400; line-height: 1.2; max-width: 100%; - color: #222; + color: #222; } .banner_advantages__title{ font-size: 14px; @@ -291,7 +291,7 @@ flex: 0 0 100%; max-width: 100%; } - .banner_group__variant3 .banner_group__content, + .banner_group__variant3 .banner_group__content, .banner_group__variant4 .banner_group__content { padding: 5% 5%; } diff --git a/Okay/Modules/OkayCMS/DeliveryFields/Backend/design/css.php b/Okay/Modules/OkayCMS/DeliveryFields/Backend/design/css.php index c07ef9520..c92b13add 100644 --- a/Okay/Modules/OkayCMS/DeliveryFields/Backend/design/css.php +++ b/Okay/Modules/OkayCMS/DeliveryFields/Backend/design/css.php @@ -4,4 +4,4 @@ return [ (new Css('delivery_field.css')), -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/DeliveryFields/Backend/lang/en.php b/Okay/Modules/OkayCMS/DeliveryFields/Backend/lang/en.php index 65f9ce423..7f9c9a007 100644 --- a/Okay/Modules/OkayCMS/DeliveryFields/Backend/lang/en.php +++ b/Okay/Modules/OkayCMS/DeliveryFields/Backend/lang/en.php @@ -1,4 +1,5 @@ 0) { $('.fn_validate_cart input[name^="delivery_fields"].required').each(function () { let errorText = $(this).data('error_text'); diff --git a/Okay/Modules/OkayCMS/FAQ/Backend/Controllers/FAQAdmin.php b/Okay/Modules/OkayCMS/FAQ/Backend/Controllers/FAQAdmin.php index 70a8e56b8..f5462fc97 100644 --- a/Okay/Modules/OkayCMS/FAQ/Backend/Controllers/FAQAdmin.php +++ b/Okay/Modules/OkayCMS/FAQ/Backend/Controllers/FAQAdmin.php @@ -1,9 +1,7 @@ design->assign('faq', $faq); $this->response->setContent($this->design->fetch('faq.tpl')); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/FAQ/Backend/lang/en.php b/Okay/Modules/OkayCMS/FAQ/Backend/lang/en.php index 49e07c39e..765124e5f 100644 --- a/Okay/Modules/OkayCMS/FAQ/Backend/lang/en.php +++ b/Okay/Modules/OkayCMS/FAQ/Backend/lang/en.php @@ -7,4 +7,4 @@ $lang['faq_added'] = 'FAQ added'; $lang['faq_updated'] = 'FAQ updated'; $lang['faq_answer'] = 'Answer'; -$lang['faq_question'] = 'Question'; \ No newline at end of file +$lang['faq_question'] = 'Question'; diff --git a/Okay/Modules/OkayCMS/FAQ/Backend/lang/ge.php b/Okay/Modules/OkayCMS/FAQ/Backend/lang/ge.php index ca62eac77..0289e0512 100644 --- a/Okay/Modules/OkayCMS/FAQ/Backend/lang/ge.php +++ b/Okay/Modules/OkayCMS/FAQ/Backend/lang/ge.php @@ -7,4 +7,4 @@ $lang['faq_added'] = 'დასძინა კითხვები'; $lang['faq_updated'] = 'კითხვები განახლებულია'; $lang['faq_answer'] = 'პასუხი'; -$lang['faq_question'] = 'კითხვა'; \ No newline at end of file +$lang['faq_question'] = 'კითხვა'; diff --git a/Okay/Modules/OkayCMS/FAQ/Backend/lang/ru.php b/Okay/Modules/OkayCMS/FAQ/Backend/lang/ru.php index 779b91b68..cb880ff1f 100644 --- a/Okay/Modules/OkayCMS/FAQ/Backend/lang/ru.php +++ b/Okay/Modules/OkayCMS/FAQ/Backend/lang/ru.php @@ -7,4 +7,4 @@ $lang['faq_added'] = 'FAQ добавлен'; $lang['faq_updated'] = 'FAQ обновлен'; $lang['faq_answer'] = 'Ответ'; -$lang['faq_question'] = 'Вопрос'; \ No newline at end of file +$lang['faq_question'] = 'Вопрос'; diff --git a/Okay/Modules/OkayCMS/FAQ/Backend/lang/ua.php b/Okay/Modules/OkayCMS/FAQ/Backend/lang/ua.php index da44db984..4d3ec40de 100644 --- a/Okay/Modules/OkayCMS/FAQ/Backend/lang/ua.php +++ b/Okay/Modules/OkayCMS/FAQ/Backend/lang/ua.php @@ -1,10 +1,10 @@ -design->assign('meta_description', $this->page->meta_description); $this->design->assign('breadcrumbs', [$this->page->name]); } - + $this->response->setContent('faq.tpl'); } } diff --git a/Okay/Modules/OkayCMS/FAQ/Entities/FAQEntity.php b/Okay/Modules/OkayCMS/FAQ/Entities/FAQEntity.php index dff584f46..cbb3f73a4 100644 --- a/Okay/Modules/OkayCMS/FAQ/Entities/FAQEntity.php +++ b/Okay/Modules/OkayCMS/FAQ/Entities/FAQEntity.php @@ -1,9 +1,7 @@ setTypeInt(11), ]); } - + public function init() { $this->registerBackendController('FAQsAdmin'); @@ -36,4 +34,4 @@ public function init() 'left_faq_title' => ['FAQsAdmin', 'FAQAdmin'], ]); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/FAQ/Init/routes.php b/Okay/Modules/OkayCMS/FAQ/Init/routes.php index 8b061d332..89952273f 100644 --- a/Okay/Modules/OkayCMS/FAQ/Init/routes.php +++ b/Okay/Modules/OkayCMS/FAQ/Init/routes.php @@ -10,4 +10,4 @@ 'method' => 'render', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/FAQ/design/css.php b/Okay/Modules/OkayCMS/FAQ/design/css.php index 34d237a71..8ca146ed5 100644 --- a/Okay/Modules/OkayCMS/FAQ/design/css.php +++ b/Okay/Modules/OkayCMS/FAQ/design/css.php @@ -5,4 +5,3 @@ return [ (new Css('faq.css')), ]; - diff --git a/Okay/Modules/OkayCMS/FAQ/design/js/faq.js b/Okay/Modules/OkayCMS/FAQ/design/js/faq.js index b518758d7..a0159b315 100644 --- a/Okay/Modules/OkayCMS/FAQ/design/js/faq.js +++ b/Okay/Modules/OkayCMS/FAQ/design/js/faq.js @@ -1,14 +1,14 @@ -$(document).on('click', '.faq__question', function() { +$(document).on('click', '.faq__question', function () { var outerBox = $(this).parents('.fn_faq'); var target = $(this).parents('.faq__item'); - if($(this).hasClass('active')!==true){ + if ($(this).hasClass('active') !== true) { $(outerBox).find('.faq__item .faq__question').removeClass('active'); } - if ($(this).next('.faq__content').is(':visible')){ + if ($(this).next('.faq__content').is(':visible')) { return false; - }else{ + } else { $(this).addClass('active'); $(outerBox).children('.faq__item').removeClass('visible'); $(outerBox).find('.faq__item').children('.faq__content').slideUp(300); diff --git a/Okay/Modules/OkayCMS/FastOrder/Backend/Controllers/DescriptionAdmin.php b/Okay/Modules/OkayCMS/FastOrder/Backend/Controllers/DescriptionAdmin.php index ab5351b6a..82599d197 100644 --- a/Okay/Modules/OkayCMS/FastOrder/Backend/Controllers/DescriptionAdmin.php +++ b/Okay/Modules/OkayCMS/FastOrder/Backend/Controllers/DescriptionAdmin.php @@ -1,9 +1,7 @@ response->setContent($this->design->fetch('description.tpl')); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/FastOrder/Controllers/FastOrderController.php b/Okay/Modules/OkayCMS/FastOrder/Controllers/FastOrderController.php index e7a7e6c88..3ace2e950 100644 --- a/Okay/Modules/OkayCMS/FastOrder/Controllers/FastOrderController.php +++ b/Okay/Modules/OkayCMS/FastOrder/Controllers/FastOrderController.php @@ -13,6 +13,7 @@ use Okay\Entities\VariantsEntity; use Okay\Helpers\CartHelper; use Okay\Helpers\OrdersHelper; +use Okay\Helpers\ValidateHelper as CustomerValidateHelper; use Okay\Entities\OrdersEntity; use Okay\Entities\PurchasesEntity; use Okay\Controllers\AbstractController; @@ -31,12 +32,19 @@ public function createOrder( CartHelper $cartHelper, VariantsEntity $variantsEntity, Cart $cart, - BackendExtender $validateExtend + BackendExtender $validateExtend, + CustomerValidateHelper $customerValidateHelper ) { if (!$this->request->method('post')) { return $this->response->setContent(json_encode(['errors' => ['Request must be post']]), RESPONSE_JSON); } + if ($customerValidateHelper->getCustomerCsrfError($this->request->post('customer_csrf_token')) !== null) { + return $this->response->setContent(json_encode([ + 'errors' => [$frontTranslations->getTranslation('form_error_csrf') ?: 'csrf'], + ]), RESPONSE_JSON); + } + $order = new \stdClass(); $order->name = $this->request->post('name'); $order->last_name = $this->request->post('last_name'); diff --git a/Okay/Modules/OkayCMS/FastOrder/Init/Init.php b/Okay/Modules/OkayCMS/FastOrder/Init/Init.php index cafef11df..09f59fbf9 100644 --- a/Okay/Modules/OkayCMS/FastOrder/Init/Init.php +++ b/Okay/Modules/OkayCMS/FastOrder/Init/Init.php @@ -1,9 +1,7 @@ addBackendControllerPermission('DescriptionAdmin', 'okaycms__fast_order'); $this->addFrontBlock('front_after_footer_content', 'fast_order_form.tpl'); - + $this->registerChainExtension( [BackendSettingsHelper::class, 'updateGeneralSettings'], [BackendExtender::class, 'updateSettings'] ); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/FastOrder/Init/SmartyPlugins.php b/Okay/Modules/OkayCMS/FastOrder/Init/SmartyPlugins.php index 8c68d9a6a..739287ac7 100644 --- a/Okay/Modules/OkayCMS/FastOrder/Init/SmartyPlugins.php +++ b/Okay/Modules/OkayCMS/FastOrder/Init/SmartyPlugins.php @@ -1,6 +1,5 @@ 'createOrder', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/FastOrder/Init/services.php b/Okay/Modules/OkayCMS/FastOrder/Init/services.php index a3d3b92af..8d494299f 100644 --- a/Okay/Modules/OkayCMS/FastOrder/Init/services.php +++ b/Okay/Modules/OkayCMS/FastOrder/Init/services.php @@ -1,9 +1,7 @@ design->assign('fastOrderProduct', $vars['product']); return $this->design->fetch('fast_order_btn.tpl'); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/FastOrder/design/html/fast_order_form.tpl b/Okay/Modules/OkayCMS/FastOrder/design/html/fast_order_form.tpl index c1198a645..e984249e6 100644 --- a/Okay/Modules/OkayCMS/FastOrder/design/html/fast_order_form.tpl +++ b/Okay/Modules/OkayCMS/FastOrder/design/html/fast_order_form.tpl @@ -6,6 +6,7 @@ onsubmit="sendAjaxFastOrderForm(); return false" {/if} > + {* The form heading *}
diff --git a/Okay/Modules/OkayCMS/Feeds/Core/Presets/PresetAdapterInterface.php b/Okay/Modules/OkayCMS/Feeds/Core/Presets/PresetAdapterInterface.php index 8f7c98d72..d5ad5a763 100644 --- a/Okay/Modules/OkayCMS/Feeds/Core/Presets/PresetAdapterInterface.php +++ b/Okay/Modules/OkayCMS/Feeds/Core/Presets/PresetAdapterInterface.php @@ -5,4 +5,4 @@ interface PresetAdapterInterface { public function render($feed): void; -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/Feeds/Init/parameters.php b/Okay/Modules/OkayCMS/Feeds/Init/parameters.php index 7dc57328a..6c9bdde08 100644 --- a/Okay/Modules/OkayCMS/Feeds/Init/parameters.php +++ b/Okay/Modules/OkayCMS/Feeds/Init/parameters.php @@ -54,4 +54,4 @@ ] ] ] -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/Feeds/Init/routes.php b/Okay/Modules/OkayCMS/Feeds/Init/routes.php index 95d0344a4..43d1ade83 100644 --- a/Okay/Modules/OkayCMS/Feeds/Init/routes.php +++ b/Okay/Modules/OkayCMS/Feeds/Init/routes.php @@ -16,4 +16,4 @@ ], 'to_front' => false ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/Feeds/Init/services.php b/Okay/Modules/OkayCMS/Feeds/Init/services.php index f1c0693a3..d761360ae 100644 --- a/Okay/Modules/OkayCMS/Feeds/Init/services.php +++ b/Okay/Modules/OkayCMS/Feeds/Init/services.php @@ -52,4 +52,4 @@ new PR('modules.okay_cms.feeds.presets') ] ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/Fondy/Backend/Controllers/DescriptionAdmin.php b/Okay/Modules/OkayCMS/Fondy/Backend/Controllers/DescriptionAdmin.php index 0a9e7a8ae..4d419f084 100644 --- a/Okay/Modules/OkayCMS/Fondy/Backend/Controllers/DescriptionAdmin.php +++ b/Okay/Modules/OkayCMS/Fondy/Backend/Controllers/DescriptionAdmin.php @@ -1,15 +1,13 @@ -response->setContent($this->design->fetch('description.tpl')); - } -} \ No newline at end of file +response->setContent($this->design->fetch('description.tpl')); + } +} diff --git a/Okay/Modules/OkayCMS/Fondy/Backend/lang/ua.php b/Okay/Modules/OkayCMS/Fondy/Backend/lang/ua.php index d4123d084..0cf080d02 100644 --- a/Okay/Modules/OkayCMS/Fondy/Backend/lang/ua.php +++ b/Okay/Modules/OkayCMS/Fondy/Backend/lang/ua.php @@ -1,8 +1,8 @@ - [ - 'slug' => 'payment/OkayCMS/Fondy/callback', - 'params' => [ - 'controller' => __NAMESPACE__ . '\Controllers\CallbackController', - 'method' => 'payOrder', - ], - ], -]; \ No newline at end of file + [ + 'slug' => 'payment/OkayCMS/Fondy/callback', + 'params' => [ + 'controller' => __NAMESPACE__ . '\Controllers\CallbackController', + 'method' => 'payOrder', + ], + ], +]; diff --git a/Okay/Modules/OkayCMS/Fondy/Init/services.php b/Okay/Modules/OkayCMS/Fondy/Init/services.php index 6c6543935..f7fee8c0a 100644 --- a/Okay/Modules/OkayCMS/Fondy/Init/services.php +++ b/Okay/Modules/OkayCMS/Fondy/Init/services.php @@ -1,20 +1,18 @@ - [ - 'class' => PaymentForm::class, - 'arguments' => [ - new SR(EntityFactory::class), - new SR(Money::class), - ], - ], -]; \ No newline at end of file + [ + 'class' => PaymentForm::class, + 'arguments' => [ + new SR(EntityFactory::class), + new SR(Money::class), + ], + ], +]; diff --git a/Okay/Modules/OkayCMS/GoogleMerchant/Backend/Controllers/GoogleMerchantAdmin.php b/Okay/Modules/OkayCMS/GoogleMerchant/Backend/Controllers/GoogleMerchantAdmin.php index 9132aedad..15bb31afd 100644 --- a/Okay/Modules/OkayCMS/GoogleMerchant/Backend/Controllers/GoogleMerchantAdmin.php +++ b/Okay/Modules/OkayCMS/GoogleMerchant/Backend/Controllers/GoogleMerchantAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { $postFeeds = $this->request->post('feeds'); @@ -43,15 +40,15 @@ public function fetch( if ($this->request->post('add_feed')) { $backendGoogleMerchantHelper->addFeed(); - } else if ($feedId = $this->request->post('remove_feed')) { + } elseif ($feedId = $this->request->post('remove_feed')) { $backendGoogleMerchantHelper->removeFeed($feedId); - } else if ($feedId = $this->request->post('add_all_categories')) { + } elseif ($feedId = $this->request->post('add_all_categories')) { $backendGoogleMerchantHelper->addAllCategories($feedId); - } else if($feedId = $this->request->post('remove_all_categories')) { + } elseif ($feedId = $this->request->post('remove_all_categories')) { $relationsEntity->removeAllCategoriesByFeedId($feedId); - } else if ($feedId = $this->request->post('add_all_brands')) { + } elseif ($feedId = $this->request->post('add_all_brands')) { $backendGoogleMerchantHelper->addAllBrands($feedId); - } else if($feedId = $this->request->post('remove_all_brands')) { + } elseif ($feedId = $this->request->post('remove_all_brands')) { $relationsEntity->removeAllBrandsByFeedId($feedId); } diff --git a/Okay/Modules/OkayCMS/GoogleMerchant/Backend/lang/ua.php b/Okay/Modules/OkayCMS/GoogleMerchant/Backend/lang/ua.php index 17aebebd2..ac9c79545 100644 --- a/Okay/Modules/OkayCMS/GoogleMerchant/Backend/lang/ua.php +++ b/Okay/Modules/OkayCMS/GoogleMerchant/Backend/lang/ua.php @@ -1,39 +1,39 @@ -select->cols($cols); } } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/GoogleMerchant/Init/routes.php b/Okay/Modules/OkayCMS/GoogleMerchant/Init/routes.php index 757cd28ae..668179c2a 100644 --- a/Okay/Modules/OkayCMS/GoogleMerchant/Init/routes.php +++ b/Okay/Modules/OkayCMS/GoogleMerchant/Init/routes.php @@ -13,4 +13,4 @@ 'method' => 'render', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/GoogleMerchant/Init/services.php b/Okay/Modules/OkayCMS/GoogleMerchant/Init/services.php index 5b2aafa23..32a50738b 100644 --- a/Okay/Modules/OkayCMS/GoogleMerchant/Init/services.php +++ b/Okay/Modules/OkayCMS/GoogleMerchant/Init/services.php @@ -1,9 +1,7 @@ request->method('post')) { $postFeeds = $this->request->post('feeds'); @@ -45,15 +42,15 @@ public function fetch( if ($this->request->post('add_feed')) { $backendHotlineHelper->addFeed(); - } else if ($feedId = $this->request->post('remove_feed')) { + } elseif ($feedId = $this->request->post('remove_feed')) { $backendHotlineHelper->removeFeed($feedId); - } else if ($feedId = $this->request->post('add_all_categories')) { + } elseif ($feedId = $this->request->post('add_all_categories')) { $backendHotlineHelper->addAllCategories($feedId); - } else if($feedId = $this->request->post('remove_all_categories')) { + } elseif ($feedId = $this->request->post('remove_all_categories')) { $relationsEntity->removeAllCategoriesByFeedId($feedId); - } else if ($feedId = $this->request->post('add_all_brands')) { + } elseif ($feedId = $this->request->post('add_all_brands')) { $backendHotlineHelper->addAllBrands($feedId); - } else if($feedId = $this->request->post('remove_all_brands')) { + } elseif ($feedId = $this->request->post('remove_all_brands')) { $relationsEntity->removeAllBrandsByFeedId($feedId); } @@ -91,6 +88,5 @@ private function updateCheckboxes() $this->settings->set('okaycms__hotline__pickup', $this->request->post('okaycms__hotline__pickup', 'integer')); $this->settings->set('okaycms__hotline__store', $this->request->post('okaycms__hotline__store', 'integer')); $this->settings->set('okaycms__hotline__upload_without_images', $this->request->post('okaycms__hotline__upload_without_images', 'integer')); - } } diff --git a/Okay/Modules/OkayCMS/Hotline/Entities/HotlineFeedsEntity.php b/Okay/Modules/OkayCMS/Hotline/Entities/HotlineFeedsEntity.php index 8099419f7..f0cb08501 100644 --- a/Okay/Modules/OkayCMS/Hotline/Entities/HotlineFeedsEntity.php +++ b/Okay/Modules/OkayCMS/Hotline/Entities/HotlineFeedsEntity.php @@ -1,9 +1,7 @@ select->cols($cols); } } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/Hotline/Init/routes.php b/Okay/Modules/OkayCMS/Hotline/Init/routes.php index 5059be8d9..25bafde2c 100644 --- a/Okay/Modules/OkayCMS/Hotline/Init/routes.php +++ b/Okay/Modules/OkayCMS/Hotline/Init/routes.php @@ -1,9 +1,7 @@ 'render', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/Hotline/Init/services.php b/Okay/Modules/OkayCMS/Hotline/Init/services.php index 75eae81fb..391ba004d 100644 --- a/Okay/Modules/OkayCMS/Hotline/Init/services.php +++ b/Okay/Modules/OkayCMS/Hotline/Init/services.php @@ -1,9 +1,7 @@ request->method('post') && $this->request->post('status_1c')) { $statuses = $this->request->post('status_1c'); - foreach($statuses as $id => $status) { + foreach ($statuses as $id => $status) { $orderStatusEntity->update($id, ['status_1c' => $status]); } - + $this->settings->set('integration1cBrandOptionName', $this->request->post('integration1cBrandOptionName')); $this->settings->set('integration1cGuidPriceFrom1C', $this->request->post('integration1cGuidPriceFrom1C')); $this->settings->set('integration1cGuidComparePriceFrom1C', $this->request->post('integration1cGuidComparePriceFrom1C')); - + $this->settings->set('integration1cFullUpdate', $this->request->post('integration1cFullUpdate', 'int')); $this->settings->set('integration1cOnlyEnabledCurrencies', $this->request->post('integration1cOnlyEnabledCurrencies', 'int')); $this->settings->set('integration1cStockFrom1c', $this->request->post('integration1cStockFrom1c', 'int')); @@ -38,4 +36,4 @@ public function fetch(EntityFactory $entityFactory) $this->response->setContent($this->design->fetch('description.tpl')); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/Integration1C/Backend/lang/ua.php b/Okay/Modules/OkayCMS/Integration1C/Backend/lang/ua.php index d9ca34ae1..6e2271236 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Backend/lang/ua.php +++ b/Okay/Modules/OkayCMS/Integration1C/Backend/lang/ua.php @@ -1,32 +1,32 @@ -response->setContentType(RESPONSE_TEXT); - + // Аутентификация (лигинимся под менеджером из админки) if ($integration1C->checkAuth() === false) { $this->response->addHeader("WWW-Authenticate: Basic realm=\"1C integration for OkayCMS {$this->config->version} {$this->config->version_type}\""); @@ -28,16 +25,15 @@ public function runIntegration( $this->response->sendHeaders(); return; } - + if ($this->request->get('mode') == 'checkauth') { $this->response->setContent("success\n"); - $this->response->setContent(session_name()."\n"); - $this->response->setContent(session_id()."\n"); + $this->response->setContent(session_name() . "\n"); + $this->response->setContent(session_id() . "\n"); } // Инициализация обмена if ($this->request->get('mode') == 'init') { - $integration1C->rrmdir($integration1C->getTmpDir()); // Очищаем все временнные данные @@ -53,10 +49,15 @@ public function runIntegration( } if ($this->request->get('mode') == 'file' && in_array($this->request->get('type'), array('catalog', 'sale'))) { - $filename = $this->request->get('filename'); $xmlFileName = $integration1C->getFullPath($filename); + if ($xmlFileName === '' || !$integration1C->isAllowedFilePath($xmlFileName)) { + $this->response->setContent("error import file\n"); + $this->response->sendContent(); + return; + } + // Загружаем файл $integration1C->uploadFile($xmlFileName); @@ -74,14 +75,12 @@ public function runIntegration( } if ($this->request->get('type') == 'sale') { - if ($this->request->get('mode') == 'success') { $this->settings->last_1c_orders_export_date = date("Y-m-d H:i:s"); $this->response->setContent("success\n"); $this->response->sendContent(); return; } elseif ($this->request->get('mode') == 'query') { - $export = $exportFactory->create('orders'); if ($xml = $export->export()) { @@ -100,9 +99,7 @@ public function runIntegration( if ($this->request->get('mode') == 'success') { $this->settings->last_1c_orders_export_date = date("Y-m-d H:i:s"); } - } elseif ($this->request->get('type') == 'catalog') { - if ($this->request->get('mode') == 'import') { $filename = $this->request->get('filename'); // Определяем какую фабрику импорта создать, импорта товаров или предложений @@ -115,10 +112,9 @@ public function runIntegration( } } } - + // Если определили импорт, тогда запустим его if (!empty($import) && $import instanceof AbstractImport) { - $filename = $this->request->get('filename'); $xmlFile = $integration1C->getFullPath($filename); @@ -127,4 +123,4 @@ public function runIntegration( $this->response->setContent($result); } } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/Integration1C/Init/routes.php b/Okay/Modules/OkayCMS/Integration1C/Init/routes.php index 527e683a5..9636e2aff 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Init/routes.php +++ b/Okay/Modules/OkayCMS/Integration1C/Init/routes.php @@ -11,4 +11,4 @@ ], 'always_active' => true, ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/Integration1C/Init/services.php b/Okay/Modules/OkayCMS/Integration1C/Init/services.php index 29c4308a0..cd15c7e7f 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Init/services.php +++ b/Okay/Modules/OkayCMS/Integration1C/Init/services.php @@ -1,9 +1,7 @@ integration1C = $integration1C; } - + abstract public function create($type); -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/Integration1C/Integration/Export/AbstractExport.php b/Okay/Modules/OkayCMS/Integration1C/Integration/Export/AbstractExport.php index 0d9727864..ec280901d 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Integration/Export/AbstractExport.php +++ b/Okay/Modules/OkayCMS/Integration1C/Integration/Export/AbstractExport.php @@ -2,23 +2,20 @@ namespace Okay\Modules\OkayCMS\Integration1C\Integration\Export; - use Okay\Modules\OkayCMS\Integration1C\Integration\Integration1C; abstract class AbstractExport { - /** @var Integration1C */ protected $integration1C; - + public function __construct(Integration1C $integration1C) { $this->integration1C = $integration1C; } - + /** * @return string */ abstract public function export(); - } diff --git a/Okay/Modules/OkayCMS/Integration1C/Integration/Export/ExportFactory/ExportFactory.php b/Okay/Modules/OkayCMS/Integration1C/Integration/Export/ExportFactory/ExportFactory.php index 9b5f4eae8..1d81e90d3 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Integration/Export/ExportFactory/ExportFactory.php +++ b/Okay/Modules/OkayCMS/Integration1C/Integration/Export/ExportFactory/ExportFactory.php @@ -1,15 +1,12 @@ integration1C = $integration1C; } - + /** * @param string $xmlFile Full path to xml file * @return string */ abstract public function import($xmlFile); - } diff --git a/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportFactory/ImportFactory.php b/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportFactory/ImportFactory.php index c06b11ad7..7f52d2053 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportFactory/ImportFactory.php +++ b/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportFactory/ImportFactory.php @@ -1,15 +1,12 @@ integration1C->beginXmlImport($xml_file); // Варианты - $z = new \XMLReader(); - $z->open($xml_file); - - while ($z->read() && $z->name !== 'Предложение'); + $z = $this->integration1C->openXmlReader($xml_file); + if ($z === null) { + return $this->integration1C->xmlImportError($xml_file); + } // Последний вариант, на котором остановились $lastVariantNum = 0; @@ -59,31 +60,54 @@ public function import($xml_file) // Номер текущего товара $currentVariantNum = 0; - while ($z->name === 'Предложение') { - if ($currentVariantNum >= $lastVariantNum) { - $xml = new \SimpleXMLElement($z->readOuterXML()); - // Варианты - $this->importVariant($xml); + $foundVariant = false; + while ($this->integration1C->readXmlReader($z)) { + if ($z->nodeType === \XMLReader::ELEMENT && $z->name === 'Предложение') { + $foundVariant = true; + $xml = $this->integration1C->loadXmlFragment($z->readOuterXML()); + if ($xml === null || !$this->isValidVariantNode($xml)) { + $z->close(); + + return $this->integration1C->xmlImportError($xml_file); + } + + if ($currentVariantNum >= $lastVariantNum) { + // Validate the complete node before any entity mutation. + $this->importVariant($xml); + + $execTime = microtime(true) - $this->integration1C->startTime; + if ($execTime + 1 >= $this->integration1C->maxExecTime) { + $nextVariantNum = $currentVariantNum + 1; + $this->integration1C->setToStorage('imported_variant_num', $nextVariantNum); + $z->close(); + $this->integration1C->finishXmlImport(); - $execTime = microtime(true) - $this->integration1C->startTime; - if ($execTime + 1 >= $this->integration1C->maxExecTime) { - // Запоминаем на каком предложении остановились - $this->integration1C->setToStorage('imported_variant_num', $currentVariantNum); + $result = "progress\n"; + $result .= "Выгружено ценовых предложений: $nextVariantNum\n"; - $result = "progress\n"; - $result .= "Выгружено ценовых предложений: $currentVariantNum\n"; - return $result; + return $result; + } } + $currentVariantNum++; } - $z->next('Предложение'); - $currentVariantNum++; } $z->close(); - $this->integration1C->setToStorage('imported_product_num', ''); + if ($this->integration1C->isXmlImportFailed() || !$foundVariant) { + return $this->integration1C->xmlImportError($xml_file); + } + + $this->integration1C->setToStorage('imported_variant_num', ''); + $this->integration1C->finishXmlImport(); + return "success\n"; } + protected function isValidVariantNode(\SimpleXMLElement $xml): bool + { + return trim((string)$xml->Ид) !== '' && isset($xml->Количество); + } + /** * @param $xmlVariant \SimpleXMLElement() * @return bool diff --git a/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportOrders.php b/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportOrders.php index 933afcaa7..1013adef9 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportOrders.php +++ b/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportOrders.php @@ -2,6 +2,7 @@ namespace Okay\Modules\OkayCMS\Integration1C\Integration\Import; +use Okay\Modules\OkayCMS\Integration1C\Integration\Integration1C; use Okay\Entities\OrdersEntity; use Okay\Entities\OrderStatusEntity; use Okay\Entities\PurchasesEntity; @@ -14,6 +15,27 @@ class ImportOrders extends AbstractImport */ public function import($xmlFile) { + $this->integration1C->beginXmlImport($xmlFile); + if (!$this->integration1C->validateXmlStructure($xmlFile)) { + return $this->integration1C->xmlImportError($xmlFile); + } + $this->integration1C->finishXmlImport(); + + $xml = Integration1C::loadXmlFile($xmlFile); + if ($xml === null || !isset($xml->Документ) || count($xml->Документ) === 0) { + return $this->integration1C->xmlImportError($xmlFile); + } + + foreach ($xml->Документ as $xmlOrder) { + if (!$this->isValidOrderNode($xmlOrder)) { + return $this->integration1C->xmlImportError($xmlFile); + } + foreach ($xmlOrder->Товары->Товар as $xmlProduct) { + if (!$this->isValidOrderProductNode($xmlProduct)) { + return $this->integration1C->xmlImportError($xmlFile); + } + } + } /** @var OrderStatusEntity $ordersStatusesEntity */ $ordersStatusesEntity = $this->integration1C->entityFactory->get(OrderStatusEntity::class); @@ -24,11 +46,6 @@ public function import($xmlFile) /** @var PurchasesEntity $purchasesEntity */ $purchasesEntity = $this->integration1C->entityFactory->get(PurchasesEntity::class); - $xml = \simplexml_load_file($xmlFile); - if (!$xml) { - return "error import file\n"; - } - $ordersStatuses = []; foreach ($ordersStatusesEntity->find() as $s) { $ordersStatuses[$s->status_1c] = $s; @@ -193,4 +210,22 @@ public function import($xmlFile) return "success\n"; } + + protected function isValidOrderNode(\SimpleXMLElement $xmlOrder): bool + { + return trim((string)$xmlOrder->Номер) !== '' + && trim((string)$xmlOrder->Дата) !== '' + && trim((string)$xmlOrder->Время) !== '' + && trim((string)$xmlOrder->Контрагенты->Контрагент->Наименование) !== '' + && isset($xmlOrder->Сумма) + && isset($xmlOrder->Товары->Товар); + } + + protected function isValidOrderProductNode(\SimpleXMLElement $xmlProduct): bool + { + return trim((string)$xmlProduct->Ид) !== '' + && trim((string)$xmlProduct->Наименование) !== '' + && isset($xmlProduct->Количество) + && isset($xmlProduct->ЦенаЗаЕдиницу); + } } diff --git a/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportProducts.php b/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportProducts.php index 7438887d4..673b2b3da 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportProducts.php +++ b/Okay/Modules/OkayCMS/Integration1C/Integration/Import/ImportProducts.php @@ -9,6 +9,7 @@ use Okay\Entities\ImagesEntity; use Okay\Entities\ProductsEntity; use Okay\Entities\VariantsEntity; +use Okay\Modules\OkayCMS\Integration1C\Integration\Integration1C; class ImportProducts extends AbstractImport { @@ -18,26 +19,43 @@ class ImportProducts extends AbstractImport */ public function import($xmlFile) { + $this->integration1C->beginXmlImport($xmlFile); + $firstBatch = empty($this->integration1C->getFromStorage('imported_product_num')); // Категории и свойства (только в первом запросе пакетной передачи) - if (empty($this->integration1C->getFromStorage('imported_product_num'))) { - $z = new \XMLReader(); - $z->open($xmlFile); - while ($z->read() && $z->name !== 'Классификатор'); - if ($z->name == 'Классификатор') { - $xml = new \SimpleXMLElement($z->readOuterXML()); - $z->close(); - $this->importCategories($xml); - $this->importFeatures($xml); - $this->importUnits($xml); + if ($firstBatch) { + $z = $this->integration1C->openXmlReader($xmlFile); + if ($z === null) { + return $this->integration1C->xmlImportError($xmlFile); + } + + $foundClassifier = false; + while ($this->integration1C->readXmlReader($z)) { + if ($z->nodeType === \XMLReader::ELEMENT && $z->name === 'Классификатор') { + $xml = $this->integration1C->loadXmlFragment($z->readOuterXML()); + if ($xml === null) { + $z->close(); + + return $this->integration1C->xmlImportError($xmlFile); + } + $foundClassifier = true; + $this->importCategories($xml); + $this->importFeatures($xml); + $this->importUnits($xml); + break; + } + } + $z->close(); + if ($this->integration1C->isXmlImportFailed() || !$foundClassifier) { + return $this->integration1C->xmlImportError($xmlFile); } } // Товары - $z = new \XMLReader(); - $z->open($xmlFile); - - while ($z->read() && $z->name !== 'Товар'); + $z = $this->integration1C->openXmlReader($xmlFile); + if ($z === null) { + return $this->integration1C->xmlImportError($xmlFile); + } // Последний товар, на котором остановились $lastProductNum = 0; @@ -48,32 +66,54 @@ public function import($xmlFile) // Номер текущего товара $currentProductNum = 0; - while ($z->name === 'Товар') { - if ($currentProductNum >= $lastProductNum) { - $xml = new \SimpleXMLElement($z->readOuterXML()); + $foundProduct = false; + while ($this->integration1C->readXmlReader($z)) { + if ($z->nodeType === \XMLReader::ELEMENT && $z->name === 'Товар') { + $foundProduct = true; + $xml = $this->integration1C->loadXmlFragment($z->readOuterXML()); + if ($xml === null || !$this->isValidProductNode($xml)) { + $z->close(); + + return $this->integration1C->xmlImportError($xmlFile); + } + + if ($currentProductNum >= $lastProductNum) { + // Validate the complete node before any entity mutation. + $this->importProduct($xml); - // Товары - $this->importProduct($xml); + $execTime = microtime(true) - $this->integration1C->startTime; + if ($execTime + 1 >= $this->integration1C->maxExecTime) { + $nextProductNum = $currentProductNum + 1; + $this->integration1C->setToStorage('imported_product_num', $nextProductNum); + $z->close(); + $this->integration1C->finishXmlImport(); - $execTime = microtime(true) - $this->integration1C->startTime; - if ($execTime + 1 >= $this->integration1C->maxExecTime) { - // Запоминаем на каком товаре остановились - $this->integration1C->setToStorage('imported_product_num', $currentProductNum); + $result = "progress\n"; + $result .= "Выгружено товаров: $nextProductNum\n"; - $result = "progress\n"; - $result .= "Выгружено товаров: $currentProductNum\n"; - return $result; + return $result; + } } + $currentProductNum++; } - $currentProductNum++; - $z->next('Товар'); } $z->close(); + if ($this->integration1C->isXmlImportFailed() || !$foundProduct) { + return $this->integration1C->xmlImportError($xmlFile); + } + $this->integration1C->setToStorage('imported_product_num', ''); + $this->integration1C->finishXmlImport(); + return "success\n"; } + protected function isValidProductNode(\SimpleXMLElement $xml): bool + { + return trim((string)$xml->Ид) !== '' && trim((string)$xml->Наименование) !== ''; + } + /** * @param $xml \SimpleXMLElement() */ @@ -700,7 +740,8 @@ protected function importImages($xmlProduct, $productId) // Обновляем основное изображение товара if (isset($xmlProduct->ОсновнаяКартинка)) { $image = (string)$xmlProduct->ОсновнаяКартинка; - if (!empty($image) && is_file($this->integration1C->getTmpDir() . $image) && is_writable($this->integration1C->config->original_images_dir)) { + $sourcePath = $this->integration1C->getFullPath($image); + if (!empty($image) && $sourcePath !== '' && is_file($sourcePath) && is_writable($this->integration1C->config->original_images_dir)) { $filename = basename($image); $imgId = $imagesEntity->cols(['id'])->find([ @@ -711,7 +752,7 @@ protected function importImages($xmlProduct, $productId) if (!empty($imgId)) { $imagesEntity->delete($imgId); } - rename($this->integration1C->getTmpDir() . $image, $this->integration1C->config->original_images_dir . $filename); + rename($sourcePath, $this->integration1C->config->original_images_dir . $filename); $imagesIds[] = $imagesEntity->add([ 'product_id' => $productId, 'filename' => $filename, @@ -725,9 +766,10 @@ protected function importImages($xmlProduct, $productId) foreach ($xmlProduct->Картинка as $img) { $image = (string)$img; $filename = basename($image); + $sourcePath = $this->integration1C->getFullPath($image); $originalImagesDir = $this->integration1C->config->root_dir . $this->integration1C->config->original_images_dir; - if (!empty($filename) && is_file($this->integration1C->getTmpDir() . $image) && is_writable($originalImagesDir)) { + if (!empty($filename) && $sourcePath !== '' && is_file($sourcePath) && is_writable($originalImagesDir)) { $imgId = $imagesEntity->cols(['id'])->find([ 'limit' => 1, 'product_id' => $productId, @@ -738,7 +780,7 @@ protected function importImages($xmlProduct, $productId) $imagesEntity->delete($imgId); } - rename($this->integration1C->getTmpDir() . $image, $originalImagesDir . $filename); + rename($sourcePath, $originalImagesDir . $filename); $imagesIds[] = $imagesEntity->add([ 'product_id' => $productId, 'filename' => $filename, diff --git a/Okay/Modules/OkayCMS/Integration1C/Integration/Import/Overrides/ImportProductsExample.php b/Okay/Modules/OkayCMS/Integration1C/Integration/Import/Overrides/ImportProductsExample.php index 28ef40653..8b3ee7230 100644 --- a/Okay/Modules/OkayCMS/Integration1C/Integration/Import/Overrides/ImportProductsExample.php +++ b/Okay/Modules/OkayCMS/Integration1C/Integration/Import/Overrides/ImportProductsExample.php @@ -1,15 +1,11 @@ Какие расширения файлов можно загружать из 1С */ @@ -207,6 +231,9 @@ public function checkAuth() public function uploadFile($xml_file) { + if (!$this->isAllowedFilePath($xml_file) || $this->getFromStorage('integration_1c_failed') === true) { + return false; + } // Создаем дерево категорий для файла $file_path = pathinfo(str_replace($this->dir, '', $xml_file), PATHINFO_DIRNAME); @@ -221,24 +248,386 @@ public function uploadFile($xml_file) } } - $f = fopen($xml_file, 'ab'); - if (!$f) { - return; + $f = @fopen($xml_file, 'ab'); + $input = @fopen('php://input', 'rb'); + if (!$f || !$input) { + if (is_resource($f)) { + fclose($f); + } + if (is_resource($input)) { + fclose($input); + } + $this->cleanupFailedArtifact($xml_file); + + return false; } - $input = file_get_contents('php://input'); - if ($input === false) { - fclose($f); - return; + $uploadedBytes = (int)$this->getFromStorage('integration_1c_uploaded_bytes'); + $acceptedBytes = 0; + $success = true; + while (!feof($input)) { + $chunk = @fread($input, 8192); + if ($chunk === false) { + $success = false; + break; + } + if ($chunk === '') { + break; + } + + $chunkLength = strlen($chunk); + if ($uploadedBytes + $acceptedBytes + $chunkLength > $this->xmlMaxExchangeBytes) { + $success = false; + break; + } + + if (@fwrite($f, $chunk) !== $chunkLength) { + $success = false; + break; + } + $acceptedBytes += $chunkLength; } - fwrite($f, $input); + fclose($input); fclose($f); + + if (!$success) { + $this->cleanupFailedArtifact($xml_file); + + return false; + } + + $this->setToStorage('integration_1c_uploaded_bytes', $uploadedBytes + $acceptedBytes); + + return true; } public function getFullPath($filename) { - return $this->dir . preg_replace('~\.\./~', '', $filename); + $filename = str_replace('\\', '/', (string) $filename); + if ($filename === '' || str_starts_with($filename, '/') || preg_match('#(^|/)\.\.(/|$)#', $filename)) { + return ''; + } + + $baseDir = realpath($this->dir); + if ($baseDir === false) { + @mkdir($this->dir, 0775, true); + $baseDir = realpath($this->dir); + } + if ($baseDir === false) { + return ''; + } + + $fullPath = $baseDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, ltrim($filename, '/')); + $normalizedBase = rtrim(str_replace('\\', '/', $baseDir), '/') . '/'; + $normalizedFull = str_replace('\\', '/', $fullPath); + + if (!str_starts_with($normalizedFull, $normalizedBase)) { + return ''; + } + + return $fullPath; + } + + public function isAllowedFilePath(string $filePath): bool + { + if ($filePath === '' || !$this->isPathContained($filePath)) { + return false; + } + + $ext = pathinfo($filePath, PATHINFO_EXTENSION); + + return $ext !== '' && in_array(strtolower($ext), $this->allowed_extensions, true); + } + + /** + * @return \SimpleXMLElement|null + */ + public static function loadXmlFile(string $xmlFile) + { + if (!is_file($xmlFile) || !is_readable($xmlFile)) { + return null; + } + + $size = filesize($xmlFile); + if ($size === false || $size > self::XML_MAX_EXCHANGE_BYTES) { + return null; + } + + return self::parseXml( + function (\XMLReader $reader, int $flags) use ($xmlFile) { + return @$reader->open($xmlFile, null, $flags); + }, + function (int $flags) use ($xmlFile) { + return @simplexml_load_file($xmlFile, 'SimpleXMLElement', $flags); + } + ); + } + + /** + * @return \SimpleXMLElement|null + */ + public static function loadXmlString(string $xmlString) + { + if (strlen($xmlString) > self::XML_MAX_EXCHANGE_BYTES) { + return null; + } + + return self::parseXml( + function (\XMLReader $reader, int $flags) use ($xmlString) { + return @$reader->XML($xmlString, null, $flags); + }, + function (int $flags) use ($xmlString) { + return @simplexml_load_string($xmlString, 'SimpleXMLElement', $flags); + } + ); + } + + public static function xmlParserFlags(): int + { + $flags = LIBXML_NONET; + if (defined('LIBXML_NO_XXE')) { + $flags |= constant('LIBXML_NO_XXE'); + } + + return $flags; + } + + public function beginXmlImport(string $xmlFile): void + { + $this->finishXmlImport(); + $this->xmlImportActive = true; + $this->xmlImportFailed = false; + $this->xmlImportFile = $xmlFile; + $this->xmlImportStartedAt = microtime(true); + $this->xmlImportNodeCount = 0; + $this->xmlPreviousInternalErrors = libxml_use_internal_errors(true); + libxml_clear_errors(); + } + + public function openXmlReader(string $xmlFile): ?\XMLReader + { + if (!$this->xmlImportActive || $this->xmlImportFile !== $xmlFile) { + $this->beginXmlImport($xmlFile); + } + + if (!is_file($xmlFile) || !is_readable($xmlFile)) { + $this->xmlImportFailed = true; + + return null; + } + + $size = filesize($xmlFile); + if ($size === false || $size > $this->xmlMaxExchangeBytes) { + $this->xmlImportFailed = true; + + return null; + } + + $reader = new \XMLReader(); + if (!@$reader->open($xmlFile, null, self::xmlParserFlags())) { + $this->xmlImportFailed = true; + + return null; + } + + return $reader; + } + + public function readXmlReader(\XMLReader $reader): bool + { + if ($this->xmlImportFailed || !@$reader->read()) { + if (libxml_get_errors()) { + $this->xmlImportFailed = true; + } + + return false; + } + + if ($this->isForbiddenXmlReaderNode($reader)) { + $this->xmlImportFailed = true; + + return false; + } + + $this->xmlImportNodeCount++; + if ( + $this->xmlImportNodeCount > $this->xmlMaxNodes + || $reader->depth > $this->xmlMaxDepth + || microtime(true) - $this->xmlImportStartedAt > $this->xmlMaxProcessingTime + ) { + $this->xmlImportFailed = true; + + return false; + } + + return true; + } + + public function loadXmlFragment(string $xmlString): ?\SimpleXMLElement + { + $reader = new \XMLReader(); + if (!@$reader->XML($xmlString, null, self::xmlParserFlags())) { + $this->xmlImportFailed = true; + + return null; + } + + while ($this->readXmlReader($reader)) { + } + $reader->close(); + + if ($this->xmlImportFailed) { + return null; + } + + $xml = self::loadXmlString($xmlString); + if ($xml === null) { + $this->xmlImportFailed = true; + } + + return $xml; + } + + public function validateXmlStructure(string $xmlFile): bool + { + $startedHere = !$this->xmlImportActive; + if ($startedHere) { + $this->beginXmlImport($xmlFile); + } + + $reader = $this->openXmlReader($xmlFile); + if ($reader !== null) { + while ($this->readXmlReader($reader)) { + } + $reader->close(); + } + + $valid = $reader !== null && !$this->xmlImportFailed && $this->xmlImportNodeCount > 0; + if ($startedHere) { + $this->finishXmlImport(); + } + + return $valid; + } + + public function isXmlImportFailed(): bool + { + return $this->xmlImportFailed; + } + + public function finishXmlImport(): void + { + if (!$this->xmlImportActive) { + return; + } + + libxml_clear_errors(); + libxml_use_internal_errors($this->xmlPreviousInternalErrors); + $this->xmlImportActive = false; + $this->xmlImportFile = ''; + $this->xmlImportStartedAt = 0.0; + $this->xmlImportNodeCount = 0; + $this->xmlPreviousInternalErrors = null; + } + + public function xmlImportError(string $xmlFile): string + { + $this->xmlImportFailed = true; + $this->cleanupFailedArtifact($xmlFile); + $this->finishXmlImport(); + + return "error import file\n"; + } + + private static function parseXml(callable $openReader, callable $loadXml): ?\SimpleXMLElement + { + $previous = libxml_use_internal_errors(true); + libxml_clear_errors(); + try { + $reader = new \XMLReader(); + if (!$openReader($reader, self::xmlParserFlags())) { + return null; + } + + $startedAt = microtime(true); + $nodeCount = 0; + while (@$reader->read()) { + if (self::isForbiddenXmlReaderNode($reader)) { + $reader->close(); + + return null; + } + + $nodeCount++; + if ( + $nodeCount > self::XML_MAX_NODES + || $reader->depth > self::XML_MAX_DEPTH + || microtime(true) - $startedAt > self::XML_MAX_PROCESSING_TIME + ) { + $reader->close(); + + return null; + } + } + + $hasErrors = libxml_get_errors() !== []; + $reader->close(); + if ($hasErrors) { + return null; + } + + $xml = $loadXml(self::xmlParserFlags()); + + return $xml !== false && libxml_get_errors() === [] ? $xml : null; + } finally { + libxml_clear_errors(); + libxml_use_internal_errors($previous); + } + } + + private static function isForbiddenXmlReaderNode(\XMLReader $reader): bool + { + $forbidden = [ + \XMLReader::DOC_TYPE, + \XMLReader::ENTITY, + \XMLReader::ENTITY_REF, + ]; + if (defined('XMLReader::DTD')) { + $forbidden[] = constant('XMLReader::DTD'); + } + + return in_array($reader->nodeType, $forbidden, true); + } + + private function cleanupFailedArtifact(string $xmlFile): void + { + if (is_file($xmlFile)) { + @unlink($xmlFile); + } + $this->setToStorage('integration_1c_failed', true); + } + + private function isPathContained(string $filePath): bool + { + $baseDir = realpath($this->dir); + if ($baseDir === false) { + return false; + } + + $dirName = dirname($filePath); + $resolvedDir = is_dir($dirName) ? realpath($dirName) : false; + if ($resolvedDir === false) { + $normalizedBase = rtrim(str_replace('\\', '/', $baseDir), '/') . '/'; + $normalizedFull = str_replace('\\', '/', $filePath); + + return str_starts_with($normalizedFull, $normalizedBase); + } + + $normalizedBase = rtrim(str_replace('\\', '/', $baseDir), '/') . '/'; + $normalizedDir = rtrim(str_replace('\\', '/', $resolvedDir), '/') . '/'; + + return str_starts_with($normalizedDir, $normalizedBase) || $normalizedDir === $normalizedBase; } public function validateFile($xml_file) @@ -246,17 +635,24 @@ public function validateFile($xml_file) $is_valid = true; $ext = pathinfo($xml_file, PATHINFO_EXTENSION); - if (empty($ext) || !in_array($ext, $this->allowed_extensions)) { + if ( + $this->getFromStorage('integration_1c_failed') === true + || !is_file($xml_file) + || !is_readable($xml_file) + || empty($ext) + || !in_array(strtolower($ext), $this->allowed_extensions, true) + ) { $is_valid = false; } - if ($is_valid === true && filesize($xml_file) == 0) { + $size = $is_valid ? filesize($xml_file) : false; + if ($is_valid === true && ($size === false || $size == 0 || $size > $this->xmlMaxExchangeBytes)) { $is_valid = false; } // Удалим файл, если он не валидный, чтобы не грузили все подряд - if ($is_valid === false) { - unlink($xml_file); + if (!$is_valid && is_file($xml_file)) { + @unlink($xml_file); } return $is_valid; diff --git a/Okay/Modules/OkayCMS/LiqPay/Backend/Controllers/DescriptionAdmin.php b/Okay/Modules/OkayCMS/LiqPay/Backend/Controllers/DescriptionAdmin.php index a9b661e80..044eb00f4 100644 --- a/Okay/Modules/OkayCMS/LiqPay/Backend/Controllers/DescriptionAdmin.php +++ b/Okay/Modules/OkayCMS/LiqPay/Backend/Controllers/DescriptionAdmin.php @@ -1,9 +1,7 @@ response->setContent($this->design->fetch('description.tpl')); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/LiqPay/Backend/lang/en.php b/Okay/Modules/OkayCMS/LiqPay/Backend/lang/en.php index 3b735e8d9..3c572462a 100644 --- a/Okay/Modules/OkayCMS/LiqPay/Backend/lang/en.php +++ b/Okay/Modules/OkayCMS/LiqPay/Backend/lang/en.php @@ -18,4 +18,4 @@ $lang['liq_pay_pay_types_moment_part'] = "Installment"; $lang['liq_pay_pay_types_cash'] = "Cash"; $lang['liq_pay_pay_types_invoice'] = "Account for e-mail"; -$lang['liq_pay_pay_types_qr'] = "QR Code"; \ No newline at end of file +$lang['liq_pay_pay_types_qr'] = "QR Code"; diff --git a/Okay/Modules/OkayCMS/LiqPay/Backend/lang/ge.php b/Okay/Modules/OkayCMS/LiqPay/Backend/lang/ge.php index f86f59b16..cb169517d 100644 --- a/Okay/Modules/OkayCMS/LiqPay/Backend/lang/ge.php +++ b/Okay/Modules/OkayCMS/LiqPay/Backend/lang/ge.php @@ -4,4 +4,4 @@ $lang['liq_pay_private_key'] = "პირადი გასაღები"; $lang['okaycms__liqpay__description_title'] = "გადახდის სისტემა LiqPay"; $lang['okaycms__liqpay__description_part_1'] = "იმისათვის, რომ მოდული მუშაობდეს, თქვენ უნდა მიუთითოთ ბმული"; -$lang['okaycms__liqpay__description_part_2'] = "თქვენს ანგარიშში პარამეტრებში - API გვერდზე ველებში \"სერვერ-სერვერის შეტყობინების მისამართები\" და \"კლიენტი-სერვერის შეტყობინების URL\", როგორც ეს მოცემულია ეკრანის სურათში. შემდეგ თქვენ უნდა მოხვდეთ ადმინში. პანელები OkayCMS- ში პარამეტრებში - გადახდის მეთოდების განყოფილებაში, შეარჩიეთ გადახდის მეთოდი, რომელთანაც გსურთ დაკავშირება გადახდა LiqPay სერვისით \"მოდულის ტიპი:\" ველში. ჩამოსაშლელი სიიდან აირჩიეთ \"OkayCMS / LiqPay\" და მიუთითეთ მონაცემები LiqPay პერსონალური ანგარიშიდან იმ ველებში, რომლებიც გამოჩნდება, რომლებიც ასევე აღინიშნება ეკრანის სურათში."; \ No newline at end of file +$lang['okaycms__liqpay__description_part_2'] = "თქვენს ანგარიშში პარამეტრებში - API გვერდზე ველებში \"სერვერ-სერვერის შეტყობინების მისამართები\" და \"კლიენტი-სერვერის შეტყობინების URL\", როგორც ეს მოცემულია ეკრანის სურათში. შემდეგ თქვენ უნდა მოხვდეთ ადმინში. პანელები OkayCMS- ში პარამეტრებში - გადახდის მეთოდების განყოფილებაში, შეარჩიეთ გადახდის მეთოდი, რომელთანაც გსურთ დაკავშირება გადახდა LiqPay სერვისით \"მოდულის ტიპი:\" ველში. ჩამოსაშლელი სიიდან აირჩიეთ \"OkayCMS / LiqPay\" და მიუთითეთ მონაცემები LiqPay პერსონალური ანგარიშიდან იმ ველებში, რომლებიც გამოჩნდება, რომლებიც ასევე აღინიშნება ეკრანის სურათში."; diff --git a/Okay/Modules/OkayCMS/LiqPay/Backend/lang/ua.php b/Okay/Modules/OkayCMS/LiqPay/Backend/lang/ua.php index ecae7818e..255032664 100644 --- a/Okay/Modules/OkayCMS/LiqPay/Backend/lang/ua.php +++ b/Okay/Modules/OkayCMS/LiqPay/Backend/lang/ua.php @@ -1,19 +1,19 @@ - 'payOrder', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/LiqPay/Init/services.php b/Okay/Modules/OkayCMS/LiqPay/Init/services.php index a8c00c80a..8514cea25 100644 --- a/Okay/Modules/OkayCMS/LiqPay/Init/services.php +++ b/Okay/Modules/OkayCMS/LiqPay/Init/services.php @@ -1,9 +1,7 @@ module_id != $novaposhta_module_id} style="display: none;" {/if}> +
module_id != $novaposhta_module_id} style="display: none;"{/if}>
{$btr->left_setting_np_title|escape}
@@ -7,47 +7,45 @@ {$isDoorDelivery = $delivery->settings['service_type'] == 'DoorsDoors' || $delivery->settings['service_type'] == 'WarehouseDoors'} -