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..17907bd35 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,14 +15,13 @@ - Додано вибір формату експорту товарів у 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-діапазону. ### Змінено +- Розділено конфігурацію для встановлення та розробки: `config/config.php` містить standalone-значення бази, а DDEV використовує їх лише через локальний `config/config.local.php`; production-інструкція більше не вимагає створення локального override. + - CSV-експорти більше не перекодовують фінальний файл у Windows-1251: товари, замовлення, користувачі, підписники і статистика записуються як валідний UTF-8, зберігаючи існуючий chunked AJAX progress і append-запис для великих файлів. - При CSV-імпорті UTF-8 BOM більше не потрапляє у перший заголовок колонки, тому зіставлення колонок не ламається через невидимий BOM-символ. - При CSV-імпорті всі текстові значення очищуються від незначущих пробілів зліва і справа, але внутрішні пробіли в значеннях зберігаються. @@ -45,21 +35,40 @@ - На 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-контракту. +- З репозиторію прибрано згенеровані CSV-експорти з демонстраційними каталожними даними; робочий каталог експорту зберігається через `.keep_folder` і наповнюється лише під час експорту. - У `.gitignore` додано nested runtime logs, щоб локальні/generated лог-файли не потрапляли у робоче дерево. +- Описано порядок безпечного впровадження змін у захисті входу до адмінпанелі, обміні з 1С та прийманні повідомлень від платіжних систем, а також перелік перевірок перед випуском. ### Виправлено +- Сторінка всіх брендів більше не завантажує фільтри товарів, тому відкривається швидше; фільтри залишилися доступними на сторінках окремих брендів. + +- Повторне надсилання checkout-форми більше не створює дублікати замовлення: одноразовий токен оновлюється після кожної спроби оформлення. +- Для Нової Пошти checkout тепер зберігає повну назву вулиці разом із її типом, коректно розділяє дані доставки у відділення та за адресою й відновлює їх після помилки валідації. +- У стандартній темі відновлено клієнтську перевірку формату номера телефону під час оформлення замовлення. - Виправлено autocomplete Нової Пошти в checkout: вибір міста/вулиці тепер прив'язаний до конкретного блоку доставки та input, а не до глобально вибраного radio delivery, тому кілька способів доставки на сторінці не перезаписують дані один одного. - Виправлено пошук вулиць Нової Пошти: autocomplete ініціалізується для вже вибраного міста адресної доставки, працює з першого символу, не кешує некоректні порожні відповіді та не блокує повторні пошуки після API-помилки. - Виправлено API-відповіді autocomplete Нової Пошти: при порожній або помилковій відповіді API endpoint повертає стабільний JSON із `suggestions: []`, а не payload з `error`, який ламав frontend autocomplete contract. - Виправлено 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 +76,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 +84,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 +116,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/BrandsController.php b/Okay/Controllers/BrandsController.php index b5a33413a..a83b9915c 100644 --- a/Okay/Controllers/BrandsController.php +++ b/Okay/Controllers/BrandsController.php @@ -29,62 +29,31 @@ public function render( $this->design->assign('filtersUrl', !empty($filtersUrl) ? $filtersUrl : '', true); $this->design->assign('ajax_filter_route', 'brands_features', true); - $catalogFeatures = $brandsHelper->getCatalogFeatures(); - $filterHelper->setFiltersUrl($filtersUrl); - $filterHelper->setFeatures($catalogFeatures); - $filterHelper->setFeaturesValuesFilter(['brand' => true]); $page = $brandsHelper->getBrandsPage(); $this->design->assign('page', $page); - if (($productsFilter = $brandsHelper->getProductsFilter(null, $filtersUrl)) === null) { + if (($productsFilter = $brandsHelper->getProductsFilter(null, '')) === null) { return false; } + unset( + $productsFilter['features'], + $productsFilter['brand_id'], + $productsFilter['other_filter'], + $productsFilter['price'] + ); if (($currentPage = $filterHelper->getCurrentPage($filtersUrl)) === false) { return false; } - $filterHelper->changeLangUrls($filtersUrl); - - $metaArray = $filterHelper->getMetaArray($filtersUrl); - - // Если в строке есть параметры которые не должны быть в фильтре, либо параметры с другой категории, бросаем 404 - if ( - !empty($metaArray['features_values']) - && array_intersect_key($metaArray['features_values'], $catalogFeatures) !== $metaArray['features_values'] - ) { - return false; - } + $filterHelper->changeLangUrls(''); $isFilterPage = $brandsHelper->isFilterPage($productsFilter); $this->design->assign('is_filter_page', $isFilterPage); - if (!$this->settings->get('deferred_load_features') || $this->request->get('ajax', 'boolean')) { - $brandsHelper->assignBrandsFilterProcedure( - $productsFilter, - $catalogFeatures - ); - } else { - // если включена отложенная загрузка фильтров, установим отдельно возможные значения свойств - $baseFeaturesValues = $catalogHelper->getBaseFeaturesValues(null, $this->settings->get('missing_products')); - - if (!empty($baseFeaturesValues)) { - foreach ($baseFeaturesValues as $values) { - foreach ($values as $value) { - if (isset($catalogFeatures[$value->feature_id])) { - $catalogFeatures[$value->feature_id]->features_values[$value->id] = $value; - } - } - } - } - foreach ($catalogFeatures as $k => $feature) { - if (!property_exists($feature, 'features_values') || empty($feature->features_values)) { - unset($catalogFeatures[$k]); - } - } - } + $brandsHelper->assignBrandsFilterProcedure($productsFilter, []); $brandsFilter = $brandsHelper->getBrandsFilter($productsFilter); @@ -141,22 +110,7 @@ public function getFilter( return false; } - $catalogFeatures = $brandsHelper->getCatalogFeatures(); - - $filterHelper->setFiltersUrl($filtersUrl); - $filterHelper->setFeatures($catalogFeatures); - $filterHelper->setFeaturesValuesFilter(['brand' => true]); - - if (($productsFilter = $brandsHelper->getProductsFilter(null, $filtersUrl)) === null) { - return false; - } - - $this->design->assign('is_filter_page', $brandsHelper->isFilterPage($productsFilter)); - - $brandsHelper->assignBrandsFilterProcedure( - $productsFilter, - $catalogFeatures - ); + $brandsHelper->assignBrandsFilterProcedure([], []); $this->design->assign('furlRoute', 'brands'); diff --git a/Okay/Controllers/CartController.php b/Okay/Controllers/CartController.php index 2537247cb..59ff92ad5 100644 --- a/Okay/Controllers/CartController.php +++ b/Okay/Controllers/CartController.php @@ -311,12 +311,10 @@ private function getCustomerCsrfError(ValidateHelper $validateHelper): ?string 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)); + return CheckoutToken::consumeSubmission( + $this->request->post('checkout_token'), + $this->getCheckoutSubmissionFingerprint($order, $cart), + ); } private function getCheckoutSubmissionFingerprint(object $order, Cart $cart): string 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 @@ 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 index ad872d390..e34a257b5 100644 --- a/Okay/Core/Security/CheckoutToken.php +++ b/Okay/Core/Security/CheckoutToken.php @@ -38,6 +38,18 @@ public static function consume(?string $token): bool return true; } + public static function consumeSubmission(mixed $token, string $fingerprint): bool + { + if ($token !== null && !is_string($token)) { + return false; + } + if (is_string($token) && $token !== '' && !self::consume($token)) { + return false; + } + + return self::consumeFingerprint($fingerprint); + } + public static function consumeFingerprint(string $fingerprint): bool { if (!self::isToken($fingerprint)) { 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/BrandsHelper.php b/Okay/Helpers/BrandsHelper.php index 43ff73565..788006c2f 100644 --- a/Okay/Helpers/BrandsHelper.php +++ b/Okay/Helpers/BrandsHelper.php @@ -80,13 +80,27 @@ public function assignBrandFilterProcedure(array $productsFilter, array $catalog */ public function assignBrandsFilterProcedure(array $productsFilter, array $catalogFeatures): void { - $this->catalogHelper->assignCatalogDataProcedure( - $productsFilter, - $catalogFeatures, - [], - [], - (int) $this->settings->get('features_max_count_products') - ); + if ($catalogFeatures === []) { + // The all-brands page is a brand directory, not a product catalog. + // Keep the assignments and extender hook for compatibility with themes and modules. + $this->design->assign('catalog_categories', []); + $this->design->assign('catalog_brands', []); + $this->design->assign('catalog_other_filters', []); + $this->design->assign('catalog_features', []); + $this->design->assign('catalog_prices', (object) ['min' => null, 'max' => null]); + $this->design->assign('selected_catalog_features', []); + $this->design->assign('selected_catalog_brands_ids', []); + $this->design->assign('selected_catalog_other_filters', []); + $this->design->assign('selected_catalog_prices', []); + } else { + $this->catalogHelper->assignCatalogDataProcedure( + $productsFilter, + $catalogFeatures, + [], + [], + (int) $this->settings->get('features_max_count_products') + ); + } ExtenderFacade::execute(__METHOD__, null, func_get_args()); } 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/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 @@ 3)'; -$lang['settings_np_city'] = 'Місто відправки'; -$lang['settings_np_service_type'] = 'Технологія доставки'; -$lang['settings_np_service_dd'] = 'Двері-Двері'; -$lang['settings_np_service_dw'] = 'Двері-Склад'; -$lang['settings_np_service_ww'] = 'Склад-Склад'; -$lang['settings_np_service_wd'] = 'Склад-Двері'; -$lang['payment_method_np_cod'] = 'Накладений платіж для НП'; -$lang['product_np_volume'] = 'Обсяг м3'; -$lang['order_np_term'] = 'Термін доставки (днів)'; -$lang['order_np_calc'] = 'Перерахувати ціну і терміни'; -$lang['order_np_redelivery'] = 'Накладений платіж'; -$lang['order_np_city'] = 'Місто'; -$lang['order_np_street'] = 'Вулиця'; -$lang['order_np_house'] = 'Будинок'; -$lang['order_np_apartment'] = 'Квартира'; -$lang['order_np_warehouse'] = 'Пункт видачі'; -$lang['order_np_area'] = 'Область'; -$lang['payment_np_cash_on_delivery'] = 'Дозволити оплату післяплатою'; -$lang['payment_np_payment_method_name'] = 'Спосіб оплати'; -$lang['payment_np_cash_on_delivery_type'] = 'Список способів оплати для оплати післяплатою'; -$lang['okaycms__np__import_field'] = 'Об\'єм варіанту'; -$lang['settings_np_last_update_cities'] = 'Дата поновлення кеша міст'; -$lang['settings_np_last_update_warehouses'] = 'Дата поновлення кеша пунктів видачі'; -$lang['np_update_cache_now'] = 'Оновити'; -$lang['np_update_address_info'] = 'Якщо ви хочете коректно оновити поле Адреса новими даними, будь ласка, перед вибором відділення очистіть поле Адреса, що знаходиться вище.'; -$lang['settings_np__description'] = 'Модуль Нової Пошти дозволяє в кошику вибирати місто і відділення і показувати вартість'; -$lang['settings_np_options'] = 'Параметри'; -$lang['tooltip_settings_np_api'] = 'Отримати ключ API ви можете в особистому кабінеті на сайті Нова Пошта'; -$lang['tooltip_settings_np_weight'] = 'Вага одного товару в замовленні, який буде використовуватися при прорахунку вартості доставки. Максимальна вага всього замовлення може бути не більше 1000 кг.'; -$lang['tooltip_settings_np_volume'] = 'Обсяг одного товару в замовленні, який буде використовуватися при прорахунку вартості доставки'; -$lang['tooltip_settings_np_city'] = 'Місто, з якого будуть відправлятися замовлення з інтернет-магазину'; -$lang['settings_np_options_updating'] = 'Оновлення даних'; -$lang['settings_np_update_date'] = 'Дата останнього оновлення даних про пункти видачі:'; -$lang['settings_np_update_label'] = 'Для прискорення роботи сайту, дані про пункти видачі Нової Пошти зберігаються в базі даних сайту.
Щоб оновити дані про пункти видачі, натисніть на кнопку "Оновити".'; -$lang['np_cron_update_cache_title'] = 'Порада'; -$lang['np_cron_update_cache_1'] = 'Для того, щоб дані оновлювалися автоматично, вам слід переконається, що на сервері налаштовано виконання за допомогою cron системного планувальника завдань:'; -$lang['np_cron_update_cache_2'] = 'щохвилини (* * * * *) і дані будуть оновлюватися автоматично раз на добу.'; -$lang['settings_np_include_volume'] = 'Включити в розрахунок вартості доставки обсяг вантажу.'; -$lang['settings_np_include_assessed'] = 'Включити в розрахунок вартості доставки оціночну вартість.'; -$lang['np_update_type_cities'] = 'Міста'; -$lang['np_cancel_update_cache'] = 'Відмінити'; -$lang['np_update_cache_finished'] = 'Завершено'; -$lang['np_api_key_error'] = 'Помилка API ключа'; -$lang['np_delete_delivery_type'] = 'Видалити тип доставки'; -$lang['np_add_delivery_type'] = 'Додати тип доставки'; -$lang['np_delivery_types_head'] = 'Типи доставки'; -$lang['np_uah_currency_not_exists'] = 'Не знайдено валюту UAH'; -$lang['np_uah_currency_not_exists_text'] = 'Для корректної роботи модуля необхідно створити валюту гривні з ISO кодом UAH'; -$lang['np_warehouse_not_selected'] = 'Оберіть пункт видачі'; -$lang['np_error_city_id'] = 'Пустий параметр city_id. Оберіть місто призначення з випадаючого списку.'; +3)'; +$lang['settings_np_city'] = 'Місто відправки'; +$lang['settings_np_service_type'] = 'Технологія доставки'; +$lang['settings_np_service_dd'] = 'Двері-Двері'; +$lang['settings_np_service_dw'] = 'Двері-Склад'; +$lang['settings_np_service_ww'] = 'Склад-Склад'; +$lang['settings_np_service_wd'] = 'Склад-Двері'; +$lang['payment_method_np_cod'] = 'Накладений платіж для НП'; +$lang['product_np_volume'] = 'Обсяг м3'; +$lang['order_np_term'] = 'Термін доставки (днів)'; +$lang['order_np_calc'] = 'Перерахувати ціну і терміни'; +$lang['order_np_redelivery'] = 'Накладений платіж'; +$lang['order_np_city'] = 'Місто'; +$lang['order_np_street'] = 'Вулиця'; +$lang['order_np_house'] = 'Будинок'; +$lang['order_np_apartment'] = 'Квартира'; +$lang['order_np_warehouse'] = 'Пункт видачі'; +$lang['order_np_area'] = 'Область'; +$lang['payment_np_cash_on_delivery'] = 'Дозволити оплату післяплатою'; +$lang['payment_np_payment_method_name'] = 'Спосіб оплати'; +$lang['payment_np_cash_on_delivery_type'] = 'Список способів оплати для оплати післяплатою'; +$lang['okaycms__np__import_field'] = 'Об\'єм варіанту'; +$lang['settings_np_last_update_cities'] = 'Дата поновлення кеша міст'; +$lang['settings_np_last_update_warehouses'] = 'Дата поновлення кеша пунктів видачі'; +$lang['np_update_cache_now'] = 'Оновити'; +$lang['np_update_address_info'] = 'Якщо ви хочете коректно оновити поле Адреса новими даними, будь ласка, перед вибором відділення очистіть поле Адреса, що знаходиться вище.'; +$lang['settings_np__description'] = 'Модуль Нової Пошти дозволяє в кошику вибирати місто і відділення і показувати вартість'; +$lang['settings_np_options'] = 'Параметри'; +$lang['tooltip_settings_np_api'] = 'Отримати ключ API ви можете в особистому кабінеті на сайті Нова Пошта'; +$lang['tooltip_settings_np_weight'] = 'Вага одного товару в замовленні, який буде використовуватися при прорахунку вартості доставки. Максимальна вага всього замовлення може бути не більше 1000 кг.'; +$lang['tooltip_settings_np_volume'] = 'Обсяг одного товару в замовленні, який буде використовуватися при прорахунку вартості доставки'; +$lang['tooltip_settings_np_city'] = 'Місто, з якого будуть відправлятися замовлення з інтернет-магазину'; +$lang['settings_np_options_updating'] = 'Оновлення даних'; +$lang['settings_np_update_date'] = 'Дата останнього оновлення даних про пункти видачі:'; +$lang['settings_np_update_label'] = 'Для прискорення роботи сайту, дані про пункти видачі Нової Пошти зберігаються в базі даних сайту.
Щоб оновити дані про пункти видачі, натисніть на кнопку "Оновити".'; +$lang['np_cron_update_cache_title'] = 'Порада'; +$lang['np_cron_update_cache_1'] = 'Для того, щоб дані оновлювалися автоматично, вам слід переконається, що на сервері налаштовано виконання за допомогою cron системного планувальника завдань:'; +$lang['np_cron_update_cache_2'] = 'щохвилини (* * * * *) і дані будуть оновлюватися автоматично раз на добу.'; +$lang['settings_np_include_volume'] = 'Включити в розрахунок вартості доставки обсяг вантажу.'; +$lang['settings_np_include_assessed'] = 'Включити в розрахунок вартості доставки оціночну вартість.'; +$lang['np_update_type_cities'] = 'Міста'; +$lang['np_cancel_update_cache'] = 'Відмінити'; +$lang['np_update_cache_finished'] = 'Завершено'; +$lang['np_api_key_error'] = 'Помилка API ключа'; +$lang['np_delete_delivery_type'] = 'Видалити тип доставки'; +$lang['np_add_delivery_type'] = 'Додати тип доставки'; +$lang['np_delivery_types_head'] = 'Типи доставки'; +$lang['np_uah_currency_not_exists'] = 'Не знайдено валюту UAH'; +$lang['np_uah_currency_not_exists_text'] = 'Для корректної роботи модуля необхідно створити валюту гривні з ISO кодом UAH'; +$lang['np_warehouse_not_selected'] = 'Оберіть пункт видачі'; +$lang['np_error_city_id'] = 'Пустий параметр city_id. Оберіть місто призначення з випадаючого списку.'; diff --git a/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPCityDTO.php b/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPCityDTO.php index 1feb5c61e..4dad8a4d5 100644 --- a/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPCityDTO.php +++ b/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPCityDTO.php @@ -45,4 +45,4 @@ public function setNameRu(string $nameRu): void { $this->nameRu = $nameRu; } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPWarehouseDTO.php b/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPWarehouseDTO.php index a1425b65c..88c50c9af 100644 --- a/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPWarehouseDTO.php +++ b/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPWarehouseDTO.php @@ -80,4 +80,4 @@ public function getNumber(): int { return $this->number; } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPWarehouseTypeDTO.php b/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPWarehouseTypeDTO.php index 1894af0df..ab45d0537 100644 --- a/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPWarehouseTypeDTO.php +++ b/Okay/Modules/OkayCMS/NovaposhtaCost/DTO/NPWarehouseTypeDTO.php @@ -38,4 +38,4 @@ public function getNameRu(): string { return $this->nameRu; } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/NovaposhtaCost/Entities/NPCostDeliveryDataEntity.php b/Okay/Modules/OkayCMS/NovaposhtaCost/Entities/NPCostDeliveryDataEntity.php index a10549bdc..a0699ce54 100644 --- a/Okay/Modules/OkayCMS/NovaposhtaCost/Entities/NPCostDeliveryDataEntity.php +++ b/Okay/Modules/OkayCMS/NovaposhtaCost/Entities/NPCostDeliveryDataEntity.php @@ -1,9 +1,7 @@ db->query($this->select); return $this->getResult(); } - -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/NovaposhtaCost/Entities/NPDeliveryTypesEntity.php b/Okay/Modules/OkayCMS/NovaposhtaCost/Entities/NPDeliveryTypesEntity.php index 64a7e26e9..b99da83eb 100644 --- a/Okay/Modules/OkayCMS/NovaposhtaCost/Entities/NPDeliveryTypesEntity.php +++ b/Okay/Modules/OkayCMS/NovaposhtaCost/Entities/NPDeliveryTypesEntity.php @@ -1,9 +1,7 @@ 'calc', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/NovaposhtaCost/Plugins/NewpostCityPlugin.php b/Okay/Modules/OkayCMS/NovaposhtaCost/Plugins/NewpostCityPlugin.php index 67a824ad0..7d7649bdb 100644 --- a/Okay/Modules/OkayCMS/NovaposhtaCost/Plugins/NewpostCityPlugin.php +++ b/Okay/Modules/OkayCMS/NovaposhtaCost/Plugins/NewpostCityPlugin.php @@ -1,9 +1,7 @@ citiesEntity->cols(['name'])->findOne(['ref' => $cityRef]); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/NovaposhtaCost/design/js.php b/Okay/Modules/OkayCMS/NovaposhtaCost/design/js.php index 03c1bded0..3cb4b870d 100644 --- a/Okay/Modules/OkayCMS/NovaposhtaCost/design/js.php +++ b/Okay/Modules/OkayCMS/NovaposhtaCost/design/js.php @@ -3,7 +3,6 @@ use Okay\Core\Router; use Okay\Core\TemplateConfig\Js; - $js = []; if (Router::getCurrentRouteName() == 'cart') { diff --git a/Okay/Modules/OkayCMS/Rozetka/Backend/Controllers/RozetkaXmlAdmin.php b/Okay/Modules/OkayCMS/Rozetka/Backend/Controllers/RozetkaXmlAdmin.php index debd82cf0..4a09542c5 100644 --- a/Okay/Modules/OkayCMS/Rozetka/Backend/Controllers/RozetkaXmlAdmin.php +++ b/Okay/Modules/OkayCMS/Rozetka/Backend/Controllers/RozetkaXmlAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { $postFeeds = $this->request->post('feeds'); @@ -27,9 +24,8 @@ public function fetch( if ($errors = $backendRozetkaHelper->validateFeeds($postFeeds)) { $this->design->assign('errors', $errors); } else { - $this->settings->update('okaycms__rozetka_xml__company', $this->request->post('okaycms__rozetka_xml__company')); - + $postRelatedCategories = $this->request->post('related_categories'); $postRelatedBrands = $this->request->post('related_brands'); @@ -41,15 +37,15 @@ public function fetch( if ($this->request->post('add_feed')) { $backendRozetkaHelper->addFeed(); - } else if ($feedId = $this->request->post('remove_feed')) { + } elseif ($feedId = $this->request->post('remove_feed')) { $backendRozetkaHelper->removeFeed($feedId); - } else if ($feedId = $this->request->post('add_all_categories')) { + } elseif ($feedId = $this->request->post('add_all_categories')) { $backendRozetkaHelper->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')) { $backendRozetkaHelper->addAllBrands($feedId); - } else if($feedId = $this->request->post('remove_all_brands')) { + } elseif ($feedId = $this->request->post('remove_all_brands')) { $relationsEntity->removeAllBrandsByFeedId($feedId); } @@ -82,7 +78,5 @@ private function updateCheckboxes() $this->settings->set('upload_only_available_to_rozetka', $this->request->post('upload_non_available', 'integer')); $this->settings->set('use_full_description_in_upload_rozetka', $this->request->post('full_description', 'integer')); $this->settings->set('okaycms__rozetka_xml__upload_without_images', $this->request->post('okaycms__rozetka_xml__upload_without_images', 'integer')); - } - } diff --git a/Okay/Modules/OkayCMS/Rozetka/Backend/lang/ua.php b/Okay/Modules/OkayCMS/Rozetka/Backend/lang/ua.php index 0befe3d65..67b145dee 100644 --- a/Okay/Modules/OkayCMS/Rozetka/Backend/lang/ua.php +++ b/Okay/Modules/OkayCMS/Rozetka/Backend/lang/ua.php @@ -1,27 +1,27 @@ -select->cols($cols); } } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/Rozetka/Init/routes.php b/Okay/Modules/OkayCMS/Rozetka/Init/routes.php index 48dfadd8b..a8d0e6b37 100644 --- a/Okay/Modules/OkayCMS/Rozetka/Init/routes.php +++ b/Okay/Modules/OkayCMS/Rozetka/Init/routes.php @@ -1,9 +1,7 @@ [ 'slug' => 'rozetka/{$url}.xml', @@ -15,4 +13,4 @@ 'method' => 'render', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/Rozetka/Init/services.php b/Okay/Modules/OkayCMS/Rozetka/Init/services.php index 4e0b4d68a..13344e387 100644 --- a/Okay/Modules/OkayCMS/Rozetka/Init/services.php +++ b/Okay/Modules/OkayCMS/Rozetka/Init/services.php @@ -1,9 +1,7 @@ response->setContent($this->design->fetch('description.tpl')); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Backend/Controllers/RefundAdmin.php b/Okay/Modules/OkayCMS/RozetkaPay/Backend/Controllers/RefundAdmin.php index f96d00412..564749109 100755 --- a/Okay/Modules/OkayCMS/RozetkaPay/Backend/Controllers/RefundAdmin.php +++ b/Okay/Modules/OkayCMS/RozetkaPay/Backend/Controllers/RefundAdmin.php @@ -10,16 +10,21 @@ class RefundAdmin extends IndexAdmin { public function fetch() { - $this->response->redirectTo($_SERVER["HTTP_REFERER"]); + $orderId = (int) $this->request->get('order', 'integer'); + $redirect = $this->request->getRootUrl() . '/backend/index.php?controller=OrdersAdmin'; + if ($orderId > 0) { + $redirect = $this->request->getRootUrl() . '/backend/index.php?controller=OrderAdmin&id=' . $orderId; + } + $this->response->redirectTo($redirect); } public function execute(Refund $refund) { - if(isset($_GET['order'])) { + if (isset($_GET['order'])) { $order = $this->getOrder($_GET['order']); $refundResult = $refund->refund($order); $ordersEntity = $this->entityFactory->get(OrdersEntity::class); - if(isset($refundResult->is_success) && $refundResult->is_success){ + if (isset($refundResult->is_success) && $refundResult->is_success) { $ordersEntity->update((int) $_GET['order'], ['payment_details' => $refundResult]); if ($order->paid === 1) { $ordersEntity->update((int) $_GET['order'], ['paid' => 0]); @@ -36,4 +41,4 @@ private function getOrder($orderId) $order = $ordersEntity->findOne(['id' => $orderId]); return $order; } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/en.php b/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/en.php index 934aef77f..b2e66eebd 100755 --- a/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/en.php +++ b/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/en.php @@ -5,4 +5,4 @@ $lang['okaycms__rozetkapay__description_title'] = "Payment system RozetkaPay"; $lang['okaycms__rozetkapay__description_part_1'] = "For the module to work, you will need to specify a link"; $lang['okaycms__rozetkapay__description_part_2'] = "in your account on the page Mazagin Settings - Notifications in the fields indicated in the screenshot. Then you will need to be in the admin. panels in OkayCMS in the Settings - Payment Methods section, select the payment method to which you want to connect payment via RozetkaPay service, in the \"Module Type:\" field, select \"OkayCMS / RozetkaPay\" from the drop-down list and specify data from RozetkaPay personal account in the fields that appear that are there in the \"Merchant Details\" block"; -$lang['rozetka_pay_refund'] = "Refund"; \ No newline at end of file +$lang['rozetka_pay_refund'] = "Refund"; diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ge.php b/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ge.php index 08123c735..9c4796deb 100755 --- a/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ge.php +++ b/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ge.php @@ -5,4 +5,4 @@ $lang['okaycms__rozetkapay__description_title'] = "გადახდის სისტემა RozetkaPay"; $lang['okaycms__rozetkapay__description_part_1'] = "იმისათვის, რომ მოდული მუშაობდეს, თქვენ უნდა მიუთითოთ ბმული"; $lang['okaycms__rozetkapay__description_part_2'] = "თქვენს ანგარიშზე გვერდზე Mazagin პარამეტრები - შეტყობინებები ეკრანის სურათზე მითითებულ ველებში. შემდეგ თქვენ უნდა მოხვდეთ ადმინში. პანელები OkayCMS- ში პარამეტრებში - გადახდის მეთოდების განყოფილებაში, შეარჩიეთ გადახდის მეთოდი, რომელთანაც გსურთ გადახდა დაკავშირება RozetkaPay სერვისით, \"მოდულის ტიპი:\" ველში, ჩამოვარდნილი სიიდან აირჩიეთ \"OkayCMS / RozetkaPay\" და ჩამოთვალეთ მონაცემები RozetkaPay- ის პირადი ანგარიშიდან იმ სფეროებში, რომლებიც გამოჩნდება. რომლებიც \"სავაჭრო დეტალების\" ბლოკშია"; -$lang['rozetka_pay_refund'] = "თანხის დაბრუნება"; \ No newline at end of file +$lang['rozetka_pay_refund'] = "თანხის დაბრუნება"; diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ru.php b/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ru.php index 9f6f97126..39d9d0c84 100755 --- a/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ru.php +++ b/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ru.php @@ -5,4 +5,4 @@ $lang['okaycms__rozetkapay__description_title'] = "Платежная система RozetkaPay"; $lang['okaycms__rozetkapay__description_part_1'] = "Для работы модуля вам нужно будет указать ссылку"; $lang['okaycms__rozetkapay__description_part_2'] = "в личном кабинете на странице Настройки мазагина - Уведомления в поля указанные на скриншоте. Затем вам нужно будет в админ. панели в OkayCMS в разделе Настройки - Способы оплаты выбрать тот способ оплаты, к которому вы хотите подключить оплату через сервис RozetkaPay, в поле \"Тип модуля:\". Выбрать из выпадающего списка \"OkayCMS/RozetkaPay\" и указать в появившиеся поля данные с личного кабинета RozetkaPay, которые находятся там в блоке \"Реквизиты мерчанта\""; -$lang['rozetka_pay_refund'] = "Возврат средств"; \ No newline at end of file +$lang['rozetka_pay_refund'] = "Возврат средств"; diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ua.php b/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ua.php index a1f62b7fa..cecdf10e8 100755 --- a/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ua.php +++ b/Okay/Modules/OkayCMS/RozetkaPay/Backend/lang/ua.php @@ -1,8 +1,8 @@ -response->setContentType(RESPONSE_TEXT); + $rawCallback = $this->readCallbackBody(); + if ($rawCallback === null) { + $this->reject('Wrong data'); + } - $rawCallback = file_get_contents("php://input"); - $data = json_decode($rawCallback === false ? '' : $rawCallback); - /** @var CallbackData|null $data */ + $headers = CallbackSecurity::requestHeaders(); + $authenticatedMethodIds = []; + $configuredMethods = $paymentsEntity->find(['module' => 'OkayCMS/RozetkaPay']); + foreach ($configuredMethods as $configuredMethod) { + $settings = $paymentsEntity->getPaymentSettings((int)$configuredMethod->id); + if ( + !is_array($settings) + || !isset($settings['rozetkapay_secretkey']) + || !is_string($settings['rozetkapay_secretkey']) + || $settings['rozetkapay_secretkey'] === '' + ) { + continue; + } + if (CallbackSecurity::verifySignature($rawCallback, $settings['rozetkapay_secretkey'], $headers)) { + $authenticatedMethodIds[(int)$configuredMethod->id] = true; + } + } + + if ($authenticatedMethodIds === []) { + $this->reject('Invalid callback signature'); + } + + $callbackAmount = CallbackSecurity::amountLexeme($rawCallback); + try { + $data = json_decode( + $rawCallback, + false, + 32, + JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING + ); + } catch (\JsonException $exception) { + $this->reject('Wrong data'); + } + /** @var \stdClass|null $data */ - if (empty($data->external_id)) { - $this->response->setContent("Wrong data")->setStatusCode(400); - $this->response->sendContent(); - exit; + if ( + !is_object($data) + || empty($data->external_id) + || !isset($data->id) + || empty($data->details) + || !is_object($data->details) + ) { + $this->reject('Wrong data'); } $orderId = $data->external_id; - $order = $ordersEntity->get((int) $orderId); + $order = $ordersEntity->get((int)$orderId); if (empty($order)) { - $postfix = \Okay\Modules\OkayCMS\RozetkaPay\Models\Gateway\CreatePayment::POSTFIX_FOR_TEST; + $postfix = CreatePayment::POSTFIX_FOR_TEST; $orderId = str_replace($postfix, '', (string)$orderId); - $order = $ordersEntity->get((int) $orderId); + $order = $ordersEntity->get((int)$orderId); if (empty($order)) { $logger->warning("RozetkaPay notice: 'Order not found'. Order №{$orderId}"); - $this->response->setContent("Order not found")->setStatusCode(400); - $this->response->sendContent(); - exit; + $this->reject('Order not found'); } } - $createDetails = $this->getPaymentDetails((int)$orderId, $queryFactory, OrdersEntity::getTable()); + $method = $paymentsEntity->get((int)$order->payment_method_id); + if ( + empty($method) + || $method->module !== 'OkayCMS/RozetkaPay' + || !isset($authenticatedMethodIds[(int)$method->id]) + ) { + $logger->warning("RozetkaPay notice: 'Invalid payment method'. Order №{$orderId}"); + $this->reject('Invalid payment method'); + } + $currency = $currenciesEntity->get((int)$method->currency_id); + if ( + empty($currency) + || !isset($data->details->currency) + || !is_string($data->details->currency) + || (string)$currency->code !== $data->details->currency + ) { + $logger->warning("RozetkaPay notice: 'Invalid currency'. Order №{$orderId}"); + $this->reject('Invalid currency'); + } + + $createDetails = $this->getPaymentDetails((int)$orderId, $queryFactory, OrdersEntity::getTable()); if ( empty($createDetails) || !isset($createDetails->details) || !isset($createDetails->details->amount) || !isset($createDetails->id) ) { - $this->response->setContent("Wrong CreatePayment data in order entity")->setStatusCode(400); - $this->response->sendContent(); - exit; + $this->reject('Wrong CreatePayment data in order entity'); } - $method = $paymentsEntity->get((int) $order->payment_method_id); - if (empty($method) || $method->module !== "OkayCMS/RozetkaPay") { - $logger->warning("RozetkaPay notice: 'Invalid payment method'. Order №{$orderId}"); - $this->response->setContent("Invalid payment method")->setStatusCode(400); - $this->response->sendContent(); - exit; - } - - $amount = $data->details->amount; - $w4pAmount = round((float)$amount, 2); $orderAmount = $money->convert($order->total_price, $method->currency_id, false, false, 2); - if ($orderAmount != $w4pAmount) { + $callbackMinorUnits = $callbackAmount === null ? null : DecimalAmount::toMinorUnits($callbackAmount); + $orderMinorUnits = DecimalAmount::toMinorUnits($orderAmount); + if ($callbackMinorUnits === null || $orderMinorUnits === null || $callbackMinorUnits !== $orderMinorUnits) { $logger->warning("RozetkaPay notice: 'Invalid total order price'. Order №{$orderId}"); - $this->response->setContent("Invalid total order price")->setStatusCode(400); - $this->response->sendContent(); - exit; + $this->reject('Invalid total order price'); } - if ($data->id !== $createDetails->id) { + if ((string)$data->id !== (string)$createDetails->id) { $logger->warning("RozetkaPay notice: 'Invalid request id'. Order №{$orderId}"); - $this->response->setContent("Invalid total order price")->setStatusCode(400); - $this->response->sendContent(); - exit; + $this->reject('Invalid request id'); } - if ( - !empty($data->details->status_code) - && $data->details->status_code == 'transaction_successful' - && !$order->paid - ) { - $ordersEntity->update((int) $order->id, ['paid' => 1]); - $ordersEntity->close((int) $order->id); - $ordersEntity->update((int)$order->id, ['payment_details' => json_encode($data)]); - $notify->emailOrderUser((int) $order->id); - $notify->emailOrderAdmin((int) $order->id); + if (!isset($data->details->status_code) || !is_string($data->details->status_code)) { + $this->reject('Wrong data'); + } + + if ($data->details->status_code === 'transaction_successful') { + $paymentDetails = json_encode($data, JSON_THROW_ON_ERROR); + $transition = $ordersEntity->markPaidIfUnpaid((int)$order->id, [ + 'payment_details' => $paymentDetails, + ]); + if ($transition === null) { + $this->response->setContent('Payment processing failed')->setStatusCode(500); + $this->response->sendContent(); + exit; + } + if ($transition === true) { + $ordersEntity->close((int)$order->id); + $notify->emailOrderUser((int)$order->id); + $notify->emailOrderAdmin((int)$order->id); + } } $this->response->setContent(json_encode(['status' => true]), RESPONSE_JSON); } /** - * @param $id - * @param $queryFactory - * @param $table + * @param int $id + * @param QueryFactory $queryFactory + * @param string $table * @return CreatePaymentDetails|null */ protected function getPaymentDetails($id, $queryFactory, $table) @@ -118,8 +176,42 @@ protected function getPaymentDetails($id, $queryFactory, $table) ->bindValue('id', $id) ->results('payment_details'); + if (!isset($data[0]) || !is_string($data[0])) { + return null; + } + $paymentDetails = json_decode($data[0]); /** @var CreatePaymentDetails|null $paymentDetails */ - return $paymentDetails; + return is_object($paymentDetails) ? $paymentDetails : null; + } + + private function reject(string $message): never + { + $this->response->setContentType(RESPONSE_TEXT); + $this->response->setContent($message)->setStatusCode(400); + $this->response->sendContent(); + exit; + } + + private function readCallbackBody(): ?string + { + $contentLength = $_SERVER['CONTENT_LENGTH'] ?? null; + if (is_string($contentLength) && ctype_digit($contentLength) && (int)$contentLength > self::MAX_CALLBACK_BYTES) { + return null; + } + + $stream = fopen('php://input', 'rb'); + if ($stream === false) { + return null; + } + + $body = stream_get_contents($stream, self::MAX_CALLBACK_BYTES + 1); + fclose($stream); + + if (!is_string($body) || $body === '' || strlen($body) > self::MAX_CALLBACK_BYTES) { + return null; + } + + return $body; } } diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Helpers/CallbackSecurity.php b/Okay/Modules/OkayCMS/RozetkaPay/Helpers/CallbackSecurity.php new file mode 100644 index 000000000..dfd5f2e57 --- /dev/null +++ b/Okay/Modules/OkayCMS/RozetkaPay/Helpers/CallbackSecurity.php @@ -0,0 +1,230 @@ +> $headers + */ + public static function verifySignature(string $rawJson, string $password, array $headers): bool + { + $signature = self::singleSignatureHeader($headers); + if ($signature === null) { + return false; + } + + $body = strtr(base64_encode($rawJson), '+/', '-_'); + $expected = strtr(base64_encode(sha1($password . $body . $password, true)), '+/', '-_'); + + return hash_equals($expected, $signature); + } + + /** @return array */ + public static function requestHeaders(): array + { + if (function_exists('getallheaders')) { + $headers = getallheaders(); + if (is_array($headers)) { + return $headers; + } + } + + $signature = $_SERVER['HTTP_X_ROZETKAPAY_SIGNATURE'] ?? null; + return is_string($signature) ? ['X-ROZETKAPAY-SIGNATURE' => $signature] : []; + } + + public static function amountLexeme(string $rawJson): ?string + { + $detailsToken = self::topLevelToken($rawJson, 'details'); + if ($detailsToken === null) { + return null; + } + $amountToken = self::objectToken($detailsToken, 'amount'); + if ($amountToken === null) { + return null; + } + + if ($amountToken[0] === '"') { + try { + $amount = json_decode( + $amountToken, + true, + 16, + JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING + ); + } catch (\JsonException $exception) { + return null; + } + } else { + $amount = $amountToken; + } + + return DecimalAmount::toMinorUnits($amount) === null ? null : (string)$amount; + } + + /** @param array $headers */ + private static function singleSignatureHeader(array $headers): ?string + { + $matches = []; + foreach ($headers as $name => $value) { + if (!is_string($name) || strcasecmp($name, 'X-ROZETKAPAY-SIGNATURE') !== 0) { + continue; + } + if (is_array($value) || !is_string($value) || $value === '') { + return null; + } + $matches[] = $value; + } + + return count($matches) === 1 ? $matches[0] : null; + } + + private static function topLevelToken(string $json, string $field): ?string + { + return self::objectToken($json, $field); + } + + private static function objectToken(string $json, string $field): ?string + { + $tokens = self::objectTokens($json); + return $tokens[$field] ?? null; + } + + /** @return array|null */ + private static function objectTokens(string $json): ?array + { + $position = 0; + $length = strlen($json); + self::skipWhitespace($json, $position, $length); + if ($position >= $length || $json[$position] !== '{') { + return null; + } + $position++; + self::skipWhitespace($json, $position, $length); + $tokens = []; + + if ($position < $length && $json[$position] === '}') { + $position++; + self::skipWhitespace($json, $position, $length); + return $position === $length ? $tokens : null; + } + + while ($position < $length) { + $keyToken = self::readStringToken($json, $position, $length); + if ($keyToken === null) { + return null; + } + try { + $key = json_decode($keyToken, true, 16, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + return null; + } + if (!is_string($key) || array_key_exists($key, $tokens)) { + return null; + } + self::skipWhitespace($json, $position, $length); + if ($position >= $length || $json[$position] !== ':') { + return null; + } + $position++; + self::skipWhitespace($json, $position, $length); + $valueStart = $position; + if (!self::skipValue($json, $position, $length)) { + return null; + } + $tokens[$key] = substr($json, $valueStart, $position - $valueStart); + self::skipWhitespace($json, $position, $length); + + if ($position < $length && $json[$position] === '}') { + $position++; + self::skipWhitespace($json, $position, $length); + return $position === $length ? $tokens : null; + } + if ($position >= $length || $json[$position] !== ',') { + return null; + } + $position++; + self::skipWhitespace($json, $position, $length); + } + + return null; + } + + private static function skipValue(string $json, int &$position, int $length): bool + { + if ($position >= $length) { + return false; + } + if ($json[$position] === '"') { + return self::readStringToken($json, $position, $length) !== null; + } + if ($json[$position] === '{' || $json[$position] === '[') { + $opening = $json[$position]; + $closing = $opening === '{' ? '}' : ']'; + $position++; + $depth = 1; + while ($position < $length && $depth > 0) { + if ($json[$position] === '"') { + if (self::readStringToken($json, $position, $length) === null) { + return false; + } + continue; + } + if ($json[$position] === $opening) { + $depth++; + if ($depth > self::MAX_JSON_DEPTH) { + return false; + } + } elseif ($json[$position] === $closing) { + $depth--; + } + $position++; + } + return $depth === 0; + } + + $start = $position; + while ($position < $length && !str_contains(" \t\r\n,]}", $json[$position])) { + $position++; + } + + return $position > $start; + } + + private static function readStringToken(string $json, int &$position, int $length): ?string + { + if ($position >= $length || $json[$position] !== '"') { + return null; + } + $start = $position++; + while ($position < $length) { + $character = $json[$position]; + if ($character === '"') { + $position++; + return substr($json, $start, $position - $start); + } + if (ord($character) < 32) { + return null; + } + if ($character === '\\') { + $position += 2; + continue; + } + $position++; + } + + return null; + } + + private static function skipWhitespace(string $json, int &$position, int $length): void + { + while ($position < $length && str_contains(" \t\r\n", $json[$position])) { + $position++; + } + } +} diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Init/Init.php b/Okay/Modules/OkayCMS/RozetkaPay/Init/Init.php index 85d5d0060..8b5b1beb8 100755 --- a/Okay/Modules/OkayCMS/RozetkaPay/Init/Init.php +++ b/Okay/Modules/OkayCMS/RozetkaPay/Init/Init.php @@ -1,13 +1,11 @@ setModuleType(MODULE_TYPE_PAYMENT); @@ -23,4 +21,4 @@ public function init() $this->registerBackendController('RefundAdmin'); $this->addBackendControllerPermission('RefundAdmin', 'okaycms__rozetkapay'); } -} \ No newline at end of file +} diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Init/routes.php b/Okay/Modules/OkayCMS/RozetkaPay/Init/routes.php index 29582380b..a8e2d9fc8 100755 --- a/Okay/Modules/OkayCMS/RozetkaPay/Init/routes.php +++ b/Okay/Modules/OkayCMS/RozetkaPay/Init/routes.php @@ -10,4 +10,4 @@ 'method' => 'payOrder', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/RozetkaPay/Init/services.php b/Okay/Modules/OkayCMS/RozetkaPay/Init/services.php index 78db1ae89..8d754c7fc 100755 --- a/Okay/Modules/OkayCMS/RozetkaPay/Init/services.php +++ b/Okay/Modules/OkayCMS/RozetkaPay/Init/services.php @@ -1,9 +1,7 @@ 60, CURLOPT_USERAGENT => 'rozetkapay-php-sdk', CURLOPT_SSL_VERIFYHOST => 2, - CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_SSL_VERIFYPEER => true, CURLOPT_TIMEOUT => 60 ]; diff --git a/Okay/Modules/OkayCMS/WayForPay/Backend/Controllers/DescriptionAdmin.php b/Okay/Modules/OkayCMS/WayForPay/Backend/Controllers/DescriptionAdmin.php index 1139afb8e..e1ee400d9 100644 --- a/Okay/Modules/OkayCMS/WayForPay/Backend/Controllers/DescriptionAdmin.php +++ b/Okay/Modules/OkayCMS/WayForPay/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/WayForPay/Backend/lang/en.php b/Okay/Modules/OkayCMS/WayForPay/Backend/lang/en.php index ddf50dee5..2d60aa3e7 100644 --- a/Okay/Modules/OkayCMS/WayForPay/Backend/lang/en.php +++ b/Okay/Modules/OkayCMS/WayForPay/Backend/lang/en.php @@ -4,4 +4,4 @@ $lang['way_for_pay_secret_key'] = "Merchant secret key"; $lang['okaycms__wayforpay__description_title'] = "Payment system WayForPay"; $lang['okaycms__wayforpay__description_part_1'] = "For the module to work, you will need to specify a link"; -$lang['okaycms__wayforpay__description_part_2'] = "in your account on the page Mazagin Settings - Notifications in the fields indicated in the screenshot. Then you will need to be in the admin. panels in OkayCMS in the Settings - Payment Methods section, select the payment method to which you want to connect payment via WayForPay service, in the \"Module Type:\" field, select \"OkayCMS / WayForPay\" from the drop-down list and specify data from WayForPay personal account in the fields that appear that are there in the \"Merchant Details\" block"; \ No newline at end of file +$lang['okaycms__wayforpay__description_part_2'] = "in your account on the page Mazagin Settings - Notifications in the fields indicated in the screenshot. Then you will need to be in the admin. panels in OkayCMS in the Settings - Payment Methods section, select the payment method to which you want to connect payment via WayForPay service, in the \"Module Type:\" field, select \"OkayCMS / WayForPay\" from the drop-down list and specify data from WayForPay personal account in the fields that appear that are there in the \"Merchant Details\" block"; diff --git a/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ge.php b/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ge.php index 11f36249b..3690fd2ad 100644 --- a/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ge.php +++ b/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ge.php @@ -4,4 +4,4 @@ $lang['way_for_pay_secret_key'] = "ვაჭრის საიდუმლო გასაღები"; $lang['okaycms__wayforpay__description_title'] = "გადახდის სისტემა WayForPay"; $lang['okaycms__wayforpay__description_part_1'] = "იმისათვის, რომ მოდული მუშაობდეს, თქვენ უნდა მიუთითოთ ბმული"; -$lang['okaycms__wayforpay__description_part_2'] = "თქვენს ანგარიშზე გვერდზე Mazagin პარამეტრები - შეტყობინებები ეკრანის სურათზე მითითებულ ველებში. შემდეგ თქვენ უნდა მოხვდეთ ადმინში. პანელები OkayCMS- ში პარამეტრებში - გადახდის მეთოდების განყოფილებაში, შეარჩიეთ გადახდის მეთოდი, რომელთანაც გსურთ გადახდა დაკავშირება WayForPay სერვისით, \"მოდულის ტიპი:\" ველში, ჩამოვარდნილი სიიდან აირჩიეთ \"OkayCMS / WayForPay\" და ჩამოთვალეთ მონაცემები WayForPay- ის პირადი ანგარიშიდან იმ სფეროებში, რომლებიც გამოჩნდება. რომლებიც \"სავაჭრო დეტალების\" ბლოკშია"; \ No newline at end of file +$lang['okaycms__wayforpay__description_part_2'] = "თქვენს ანგარიშზე გვერდზე Mazagin პარამეტრები - შეტყობინებები ეკრანის სურათზე მითითებულ ველებში. შემდეგ თქვენ უნდა მოხვდეთ ადმინში. პანელები OkayCMS- ში პარამეტრებში - გადახდის მეთოდების განყოფილებაში, შეარჩიეთ გადახდის მეთოდი, რომელთანაც გსურთ გადახდა დაკავშირება WayForPay სერვისით, \"მოდულის ტიპი:\" ველში, ჩამოვარდნილი სიიდან აირჩიეთ \"OkayCMS / WayForPay\" და ჩამოთვალეთ მონაცემები WayForPay- ის პირადი ანგარიშიდან იმ სფეროებში, რომლებიც გამოჩნდება. რომლებიც \"სავაჭრო დეტალების\" ბლოკშია"; diff --git a/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ru.php b/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ru.php index d2e52f190..b81cc8296 100644 --- a/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ru.php +++ b/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ru.php @@ -4,4 +4,4 @@ $lang['way_for_pay_secret_key'] = "Секретный ключ мерчанта"; $lang['okaycms__wayforpay__description_title'] = "Платежная система WayForPay"; $lang['okaycms__wayforpay__description_part_1'] = "Для работы модуля вам нужно будет указать ссылку"; -$lang['okaycms__wayforpay__description_part_2'] = "в личном кабинете на странице Настройки мазагина - Уведомления в поля указанные на скриншоте. Затем вам нужно будет в админ. панели в OkayCMS в разделе Настройки - Способы оплаты выбрать тот способ оплаты, к которому вы хотите подключить оплату через сервис WayForPay, в поле \"Тип модуля:\". Выбрать из выпадающего списка \"OkayCMS/WayForPay\" и указать в появившиеся поля данные с личного кабинета WayForPay, которые находятся там в блоке \"Реквизиты мерчанта\""; \ No newline at end of file +$lang['okaycms__wayforpay__description_part_2'] = "в личном кабинете на странице Настройки мазагина - Уведомления в поля указанные на скриншоте. Затем вам нужно будет в админ. панели в OkayCMS в разделе Настройки - Способы оплаты выбрать тот способ оплаты, к которому вы хотите подключить оплату через сервис WayForPay, в поле \"Тип модуля:\". Выбрать из выпадающего списка \"OkayCMS/WayForPay\" и указать в появившиеся поля данные с личного кабинета WayForPay, которые находятся там в блоке \"Реквизиты мерчанта\""; diff --git a/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ua.php b/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ua.php index 8eb09f03a..b44f4620c 100644 --- a/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ua.php +++ b/Okay/Modules/OkayCMS/WayForPay/Backend/lang/ua.php @@ -1,7 +1,7 @@ -readCallbackBody(); + if ($rawCallback === null) { + $this->reject('Wrong data'); + } - $this->response->setContentType(RESPONSE_TEXT); + $merchantAccount = CallbackPayload::stringField($rawCallback, 'merchantAccount'); + if ($merchantAccount === null) { + $this->reject('Wrong data'); + } + + $authenticatedMethodIds = []; + $configuredMethods = $paymentsEntity->find(['module' => 'OkayCMS/WayForPay']); + foreach ($configuredMethods as $configuredMethod) { + $settings = $paymentsEntity->getPaymentSettings((int)$configuredMethod->id); + if ( + !is_array($settings) + || !isset($settings['wayforpay_secretkey'], $settings['wayforpay_merchant']) + || !is_string($settings['wayforpay_secretkey']) + || $settings['wayforpay_secretkey'] === '' + || (string)$settings['wayforpay_merchant'] !== $merchantAccount + ) { + continue; + } + + if (CallbackPayload::verifySignature($rawCallback, $settings['wayforpay_secretkey']) !== null) { + $authenticatedMethodIds[(int)$configuredMethod->id] = true; + } + } + + if ($authenticatedMethodIds === []) { + $this->reject('Invalid merchant signature'); + } + + try { + $data = json_decode( + $rawCallback, + false, + 32, + JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING + ); + } catch (\JsonException $exception) { + $this->reject('Wrong data'); + } + /** @var \stdClass|null $data */ - $rawCallback = file_get_contents("php://input"); - $data = json_decode($rawCallback === false ? '' : $rawCallback); - if (empty($data->orderReference)) { - $this->response->setContent("Wrong data")->setStatusCode(400); - $this->response->sendContent(); - exit; + if (!is_object($data) || empty($data->orderReference)) { + $this->reject('Wrong data'); } $orderId = $data->orderReference; - $order = $ordersEntity->get((int) $orderId); + $order = $ordersEntity->get((int)$orderId); if (empty($order)) { $logger->warning("WayForPay notice: 'Order not found'. Order №{$orderId}"); - $this->response->setContent("Order not found")->setStatusCode(400); - $this->response->sendContent(); - exit; + $this->reject('Order not found'); } - $method = $paymentsEntity->get((int) $order->payment_method_id); - if (empty($method)) { + $method = $paymentsEntity->get((int)$order->payment_method_id); + if ( + empty($method) + || $method->module !== 'OkayCMS/WayForPay' + || !isset($authenticatedMethodIds[(int)$method->id]) + ) { $logger->warning("WayForPay notice: 'Invalid payment method'. Order №{$orderId}"); - $this->response->setContent("Invalid payment method")->setStatusCode(400); - $this->response->sendContent(); - exit; + $this->reject('Invalid payment method'); } - $amount = !empty($data->amount) ? $data->amount : null; - $w4pAmount = round($amount, 2); + $currency = $currenciesEntity->get((int)$method->currency_id); + if ( + empty($currency) + || !isset($data->currency) + || !is_string($data->currency) + || (string)$currency->code !== $data->currency + ) { + $logger->warning("WayForPay notice: 'Invalid currency'. Order №{$orderId}"); + $this->reject('Invalid currency'); + } + + $callbackAmount = CallbackPayload::amountLexeme($rawCallback); $orderAmount = $money->convert($order->total_price, $method->currency_id, false, false, 2); - if ($orderAmount != $w4pAmount) { + $callbackMinorUnits = $callbackAmount === null ? null : DecimalAmount::toMinorUnits($callbackAmount); + $orderMinorUnits = DecimalAmount::toMinorUnits($orderAmount); + if ($callbackMinorUnits === null || $orderMinorUnits === null || $callbackMinorUnits !== $orderMinorUnits) { $logger->warning("WayForPay notice: 'Invalid total order price'. Order №{$orderId}"); - $this->response->setContent("Invalid total order price")->setStatusCode(400); - $this->response->sendContent(); - exit; + $this->reject('Invalid total order price'); } - $settings = unserialize($method->settings); - - $sign = array(); - foreach ($keysForSignature as $dataKey) { - if (array_key_exists($dataKey, $data)) { - $sign [] = $data->$dataKey; - } + if (!isset($data->transactionStatus) || !is_string($data->transactionStatus)) { + $this->reject('Wrong data'); } - $sign = implode(';', $sign); - $sign = hash_hmac('md5', $sign, $settings['wayforpay_secretkey']); - if (!empty($data->merchantSignature) && $data->merchantSignature != $sign) { - $logger->warning("WayForPay notice: 'Invalid merchant signature'. Order №{$orderId}"); - $this->response->setContent("Invalid merchant signature")->setStatusCode(400); - $this->response->sendContent(); - exit; + $settings = $paymentsEntity->getPaymentSettings((int)$method->id); + if (!is_array($settings) || empty($settings['wayforpay_secretkey'])) { + $logger->warning("WayForPay notice: 'Invalid payment settings'. Order №{$orderId}"); + $this->reject('Invalid payment settings'); } $responseToGateway = [ 'orderReference' => $data->orderReference, - 'status' => 'accept', - 'time' => time() + 'status' => 'accept', + 'time' => time(), ]; + $responseToGateway['signature'] = hash_hmac( + 'md5', + implode(';', array_values($responseToGateway)), + (string)$settings['wayforpay_secretkey'] + ); + + if ($data->transactionStatus === 'Approved') { + $transition = $ordersEntity->markPaidIfUnpaid((int)$order->id); + if ($transition === null) { + $this->response->setContent('Payment processing failed')->setStatusCode(500); + $this->response->sendContent(); + exit; + } + if ($transition === true) { + $ordersEntity->close((int)$order->id); + $notify->emailOrderUser((int)$order->id); + $notify->emailOrderAdmin((int)$order->id); + } + } + + $this->response->setContent(json_encode($responseToGateway), RESPONSE_JSON); + } + + private function reject(string $message): never + { + $this->response->setContentType(RESPONSE_TEXT); + $this->response->setContent($message)->setStatusCode(400); + $this->response->sendContent(); + exit; + } + + private function readCallbackBody(): ?string + { + $contentLength = $_SERVER['CONTENT_LENGTH'] ?? null; + if (is_string($contentLength) && ctype_digit($contentLength) && (int)$contentLength > self::MAX_CALLBACK_BYTES) { + return null; + } - $sign = array(); - foreach ($responseToGateway as $dataKey => $dataValue) { - $sign [] = $dataValue; + $stream = fopen('php://input', 'rb'); + if ($stream === false) { + return null; } - $sign = implode(';', $sign); - $sign = hash_hmac('md5', $sign, $settings['wayforpay_secretkey']); - $responseToGateway['signature'] = $sign; + $body = stream_get_contents($stream, self::MAX_CALLBACK_BYTES + 1); + fclose($stream); - if (!empty($data->transactionStatus) && $data->transactionStatus == 'Approved' && !$order->paid) { - $ordersEntity->update((int) $order->id, ['paid' => 1]); - $ordersEntity->close((int) $order->id); - $notify->emailOrderUser((int) $order->id); - $notify->emailOrderAdmin((int) $order->id); + if (!is_string($body) || $body === '' || strlen($body) > self::MAX_CALLBACK_BYTES) { + return null; } - $this->response->setContent(json_encode($responseToGateway), RESPONSE_JSON); + return $body; } } diff --git a/Okay/Modules/OkayCMS/WayForPay/Helpers/CallbackPayload.php b/Okay/Modules/OkayCMS/WayForPay/Helpers/CallbackPayload.php new file mode 100644 index 000000000..2b97670b1 --- /dev/null +++ b/Okay/Modules/OkayCMS/WayForPay/Helpers/CallbackPayload.php @@ -0,0 +1,306 @@ + $keys + * @return array|null + */ + public static function signedValues(string $rawJson, array $keys): ?array + { + $tokens = self::topLevelTokens($rawJson); + if ($tokens === null) { + return null; + } + + $values = []; + foreach ($keys as $key) { + if (!array_key_exists($key, $tokens)) { + return null; + } + + $value = self::tokenToSignatureValue($tokens[$key]); + if ($value === null) { + return null; + } + $values[$key] = $value; + } + + return $values; + } + + public static function stringField(string $rawJson, string $field): ?string + { + $tokens = self::topLevelTokens($rawJson); + if ($tokens === null || !array_key_exists($field, $tokens)) { + return null; + } + + $value = self::tokenToSignatureValue($tokens[$field]); + return $value === null ? null : $value; + } + + public static function amountLexeme(string $rawJson): ?string + { + $tokens = self::topLevelTokens($rawJson); + if ($tokens === null || !array_key_exists('amount', $tokens)) { + return null; + } + + $token = $tokens['amount']; + if ($token[0] === '"') { + try { + $value = json_decode($token, true, 16, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING); + } catch (\JsonException $exception) { + return null; + } + } else { + $value = $token; + } + + return DecimalAmount::toMinorUnits($value) === null ? null : (string)$value; + } + + /** + * @param string $rawJson + * @param string $secret + * @return array|null + */ + public static function verifySignature(string $rawJson, string $secret): ?array + { + $keys = [ + 'merchantAccount', + 'orderReference', + 'amount', + 'currency', + 'authCode', + 'cardPan', + 'transactionStatus', + 'reasonCode', + ]; + $values = self::signedValues($rawJson, $keys); + $merchantSignature = self::stringField($rawJson, 'merchantSignature'); + if ($values === null || $merchantSignature === null || $merchantSignature === '') { + return null; + } + + $expected = hash_hmac('md5', implode(';', array_values($values)), $secret); + if (!hash_equals($expected, $merchantSignature)) { + return null; + } + + return $values; + } + + /** + * @return array|null + */ + private static function topLevelTokens(string $json): ?array + { + $position = 0; + $length = strlen($json); + self::skipWhitespace($json, $position, $length); + if ($position >= $length || $json[$position] !== '{') { + return null; + } + $position++; + self::skipWhitespace($json, $position, $length); + $tokens = []; + + if ($position < $length && $json[$position] === '}') { + $position++; + self::skipWhitespace($json, $position, $length); + return $position === $length ? $tokens : null; + } + + while ($position < $length) { + $keyToken = self::readStringToken($json, $position, $length); + if ($keyToken === null) { + return null; + } + try { + $key = json_decode($keyToken, true, 16, JSON_THROW_ON_ERROR); + } catch (\JsonException $exception) { + return null; + } + if (!is_string($key) || array_key_exists($key, $tokens)) { + return null; + } + + self::skipWhitespace($json, $position, $length); + if ($position >= $length || $json[$position] !== ':') { + return null; + } + $position++; + self::skipWhitespace($json, $position, $length); + $valueStart = $position; + if (!self::skipValue($json, $position, $length, 0)) { + return null; + } + $tokens[$key] = substr($json, $valueStart, $position - $valueStart); + + self::skipWhitespace($json, $position, $length); + if ($position >= $length) { + return null; + } + if ($json[$position] === '}') { + $position++; + self::skipWhitespace($json, $position, $length); + return $position === $length ? $tokens : null; + } + if ($json[$position] !== ',') { + return null; + } + $position++; + self::skipWhitespace($json, $position, $length); + } + + return null; + } + + private static function skipValue(string $json, int &$position, int $length, int $depth): bool + { + if ($position >= $length) { + return false; + } + + if ($depth > self::MAX_JSON_DEPTH) { + return false; + } + + if ($json[$position] === '"') { + return self::readStringToken($json, $position, $length) !== null; + } + + if ($json[$position] === '{') { + $position++; + self::skipWhitespace($json, $position, $length); + if ($position < $length && $json[$position] === '}') { + $position++; + return true; + } + while ($position < $length) { + if (self::readStringToken($json, $position, $length) === null) { + return false; + } + self::skipWhitespace($json, $position, $length); + if ($position >= $length || $json[$position] !== ':') { + return false; + } + $position++; + self::skipWhitespace($json, $position, $length); + if (!self::skipValue($json, $position, $length, $depth + 1)) { + return false; + } + self::skipWhitespace($json, $position, $length); + if ($position < $length && $json[$position] === '}') { + $position++; + return true; + } + if ($position >= $length || $json[$position] !== ',') { + return false; + } + $position++; + self::skipWhitespace($json, $position, $length); + } + return false; + } + + if ($json[$position] === '[') { + $position++; + self::skipWhitespace($json, $position, $length); + if ($position < $length && $json[$position] === ']') { + $position++; + return true; + } + while ($position < $length) { + if (!self::skipValue($json, $position, $length, $depth + 1)) { + return false; + } + self::skipWhitespace($json, $position, $length); + if ($position < $length && $json[$position] === ']') { + $position++; + return true; + } + if ($position >= $length || $json[$position] !== ',') { + return false; + } + $position++; + self::skipWhitespace($json, $position, $length); + } + return false; + } + + $start = $position; + while ($position < $length && !str_contains(" \t\r\n,]}", $json[$position])) { + $position++; + } + + return $position > $start; + } + + private static function readStringToken(string $json, int &$position, int $length): ?string + { + if ($position >= $length || $json[$position] !== '"') { + return null; + } + $start = $position++; + while ($position < $length) { + $character = $json[$position]; + if ($character === '"') { + $position++; + return substr($json, $start, $position - $start); + } + if (ord($character) < 32) { + return null; + } + if ($character === '\\') { + $position += 2; + continue; + } + $position++; + } + + return null; + } + + private static function skipWhitespace(string $json, int &$position, int $length): void + { + while ($position < $length && str_contains(" \t\r\n", $json[$position])) { + $position++; + } + } + + private static function tokenToSignatureValue(string $token): ?string + { + if ($token === '') { + return null; + } + if ($token[0] === '"') { + try { + $value = json_decode($token, true, 16, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING); + } catch (\JsonException $exception) { + return null; + } + return is_string($value) ? $value : null; + } + if ($token === 'true') { + return '1'; + } + if ($token === 'false' || $token === 'null') { + return ''; + } + if ($token[0] === '-' || $token[0] === '+' || ctype_digit($token[0])) { + return $token; + } + + return null; + } +} diff --git a/Okay/Modules/OkayCMS/WayForPay/Init/routes.php b/Okay/Modules/OkayCMS/WayForPay/Init/routes.php index 780095e9d..2931057f5 100644 --- a/Okay/Modules/OkayCMS/WayForPay/Init/routes.php +++ b/Okay/Modules/OkayCMS/WayForPay/Init/routes.php @@ -10,4 +10,4 @@ 'method' => 'payOrder', ], ], -]; \ No newline at end of file +]; diff --git a/Okay/Modules/OkayCMS/WayForPay/Init/services.php b/Okay/Modules/OkayCMS/WayForPay/Init/services.php index e13cb7c76..bdf0a6aa9 100644 --- a/Okay/Modules/OkayCMS/WayForPay/Init/services.php +++ b/Okay/Modules/OkayCMS/WayForPay/Init/services.php @@ -1,9 +1,7 @@ product_name.' '.$purchase->variant_name; + foreach ($purchases as $purchase) { + $purchasesNames[] = $purchase->product_name . ' ' . $purchase->variant_name; } return $purchasesNames; @@ -124,7 +121,7 @@ private function getPurchasePrices($purchases, $currencyId) { $purchasesPrices = []; - foreach($purchases as $purchase) { + foreach ($purchases as $purchase) { $purchasesPrices[] = $this->money->convert($purchase->price, $currencyId, false, false, 2); } @@ -135,7 +132,7 @@ private function getPurchaseCount($purchases) { $purchasesCount = []; - foreach($purchases as $purchase) { + foreach ($purchases as $purchase) { $purchasesCount[] = $purchase->amount; } @@ -180,14 +177,14 @@ private function formatPhone($phone) { $phone = str_replace(['+', ' ', '(', ')'], ['','','',''], $phone); - if(strlen($phone) == 10){ - return '38'.$phone; + if (strlen($phone) == 10) { + return '38' . $phone; } - if(strlen($phone) == 11){ - return '3'.$phone; + if (strlen($phone) == 11) { + return '3' . $phone; } return $phone; } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 399802d2c..9aac71d49 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,45 @@ -## OkayCMS v4.5.2 +# OkayCMS 4.6.0 -OkayCMS is a PHP CMS for creating backend and frontend applications. +OkayCMS is a PHP CMS for building e-commerce storefronts and administration +applications. -- Homepage: [https://okay-cms.com](https://okay-cms.com) -- Documentation: [https://github.com/OkayCMS/OkayCMS/tree/master/docs](https://github.com/OkayCMS/OkayCMS/tree/master/docs) -- Repository: [https://github.com/OkayCMS/OkayCMS](https://github.com/OkayCMS/OkayCMS) +- Homepage: [okay-cms.com](https://okay-cms.com) +- Developer documentation: [docs/README.md](docs/README.md) +- Upstream project: [OkayCMS/OkayCMS](https://github.com/OkayCMS/OkayCMS) +- Changes in this fork: [CHANGELOG.md](CHANGELOG.md) -OkayCMS is released under the LGPL license. +OkayCMS is released under the LGPL-3.0-or-later license. -Copyright 2015-2024 OkayCMS +Copyright 2015-2026 OkayCMS -## Про цей форк +## Requirements -Це незалежна версія OkayCMS на основі **офіційного останнього релізу**. +- PHP 8.5 +- Composer +- MySQL or MariaDB +- PHP extensions declared in [composer.json](composer.json) -Головна мета — зробити систему сучасною та безпечнішою: +## Installation -- оновити проєкт під актуальну версію PHP; -- оновити всі залежності й сторонні бібліотеки до актуальних версій або замінити їх на сучасні аналоги, які ще підтримуються; -- виправити знайдені помилки та проблеми безпеки, виявлені під час оновлення коду. +Install the locked dependencies: -Іншими словами: зберегти звичну OkayCMS, але з сучасним технічним фундаментом і виправленими відомими ризиками. +```bash +composer install +``` -Інформація про всі поточні та заплановані зміни знаходиться в файлі [CHANGELOG.md](CHANGELOG.md). +`config/config.php` ships standalone database defaults (`localhost` / `root` / +`okaycms-git`). Copy it to the gitignored `config/config.local.php` only for +local database credentials or other environment-specific overrides. Production +must configure `config/config.php` directly and must not deploy +`config/config.local.php`. + +The alternative Docker Compose development environment is documented in +[dev/README.md](dev/README.md). + +## Quality checks + +Run the complete read-only project gate: + +```bash +composer check +``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..1be9d2dbb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security Policy + +## Reporting a vulnerability + +Use public GitHub Issues for ordinary bugs and feature requests. + +Do not disclose vulnerabilities, secrets, exploit details, or sensitive customer +data in a public issue or pull request. Report security-sensitive findings through +[GitHub Private Vulnerability Reporting](https://github.com/MAX-IT-Tech/OkayCMS/security/advisories/new). + +Include the affected OkayCMS version, a concise impact description, reproduction +steps, and any suggested mitigation. Remove credentials and personal data from +all examples. diff --git a/backend/Controllers/AuthAdmin.php b/backend/Controllers/AuthAdmin.php index a19d52c99..70cb89311 100644 --- a/backend/Controllers/AuthAdmin.php +++ b/backend/Controllers/AuthAdmin.php @@ -6,13 +6,17 @@ use Okay\Core\Managers; use Okay\Core\Notify; use Okay\Core\Security\AdminSession; +use Okay\Core\Security\AdminLoginThrottle; use Okay\Core\Security\AdminRecoveryToken; +use Okay\Core\Security\SafeRedirect; use Okay\Core\Validator; use Okay\Entities\LessonsEntity; use Okay\Entities\ManagersEntity; class AuthAdmin extends IndexAdmin { + private const DUMMY_PASSWORD_HASH = '$argon2id$v=19$m=65536,t=4,p=1$SGZIdHBkY1dqWWI3VE5wSA$3jIhTMVLLSRgVcuP6yGOUuax6djwk3Oqfu7Ruwk9P+U'; + public function fetch( Managers $managers, ManagersEntity $managersEntity, @@ -87,13 +91,27 @@ public function fetch( } } elseif ($this->request->method('post')) { /*Авторизация в админ.панель*/ - $login = $this->request->post('login'); + $login = (string) $this->request->post('login'); $pass = $this->request->post('password'); - $manager = $managersEntity->get((string)$login); - if ($manager) { - $passwordIsValid = $managers->checkPassword($pass, $manager->password); + $ip = $this->request->getRemoteAddr(); + + $throttle = new AdminLoginThrottle((string) $this->config->get('root_dir') . 'compiled/cache'); + + if (!$throttle->reserve($login, $ip)) { + $this->design->assign('login', $login); + $this->design->assign('error_message', 'auth_wrong'); + $this->response->setContent($this->design->fetch('auth.tpl')); + return; + } + + $manager = $managersEntity->get($login); + $passwordIsValid = $managers->checkPassword( + is_string($pass) ? $pass : '', + $manager ? (string) $manager->password : self::DUMMY_PASSWORD_HASH + ); + if ($manager) { /*Подсчитываем количество неправильны попыток входа*/ $limit = 10; $now = date('Y-m-d'); @@ -113,9 +131,21 @@ public function fetch( /*Входим в админку*/ $_SESSION['admin'] = $manager->login; AdminSession::regenerateId(); + if (!$throttle->clear((string) $manager->login, $ip)) { + unset($_SESSION['admin']); + $this->design->assign('login', $login); + $this->design->assign('error_message', 'auth_wrong'); + $this->response->setContent($this->design->fetch('auth.tpl')); + return; + } $managersEntity->update((int)$manager->id, ['cnt_try' => 0, 'last_try' => null]); $managersEntity->updateLastActivityDate($manager->id); - $loginRedirectResource = (!empty($_SESSION['before_auth_url']) ? $_SESSION['before_auth_url'] : $this->request->getRootUrl() . '/backend/index.php'); + $fallback = $this->request->getRootUrl() . '/backend/index.php'; + $loginRedirectResource = SafeRedirect::sameOriginUrl( + (string) ($_SESSION['before_auth_url'] ?? ''), + $this->request->getRootUrl(), + $fallback + ); unset($_SESSION['before_auth_url']); $allManagers = $managersEntity->order('id ASC')->find(); @@ -126,13 +156,14 @@ public function fetch( } $response->redirectTo($loginRedirectResource); } elseif ($manager->cnt_try > $limit) { - $this->design->assign('error_message', 'limit_try'); + $this->design->assign('login', $login); + $this->design->assign('error_message', 'auth_wrong'); + $managersEntity->update((int) $manager->id, ['cnt_try' => $manager->cnt_try, 'last_try' => $last]); } else { /*неверный пароль менеджера*/ $this->design->assign('login', $login); $this->design->assign('error_message', 'auth_wrong'); - $this->design->assign('limit_cnt', $limit - $manager->cnt_try); - $managersEntity->update((int)$manager->id, ['cnt_try' => $manager->cnt_try, 'last_try' => $last]); + $managersEntity->update((int) $manager->id, ['cnt_try' => $manager->cnt_try, 'last_try' => $last]); } } else { /*менеджер не найден*/ diff --git a/backend/Controllers/AuthorAdmin.php b/backend/Controllers/AuthorAdmin.php index fe8953883..dd59b2dba 100644 --- a/backend/Controllers/AuthorAdmin.php +++ b/backend/Controllers/AuthorAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { @@ -58,5 +55,4 @@ public function fetch( $this->design->assign('author', $author); $this->response->setContent($this->design->fetch('author.tpl')); } - } diff --git a/backend/Controllers/BlogCategoryAdmin.php b/backend/Controllers/BlogCategoryAdmin.php index b15164d31..ea8106f8d 100644 --- a/backend/Controllers/BlogCategoryAdmin.php +++ b/backend/Controllers/BlogCategoryAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { $category = $blogCategoriesRequest->postCategory(); @@ -34,9 +31,8 @@ public function fetch( $this->postRedirectGet->storeMessageSuccess('added'); $this->postRedirectGet->storeNewEntityId($category->id); } else { - $categoryBeforeUpdate = $backendBlogCategoriesHelper->getCategory($category->id); - + // Обновление категории $category = $backendBlogCategoriesHelper->prepareUpdate($category->id, $category); $backendBlogCategoriesHelper->update($category->id, $category); @@ -49,7 +45,7 @@ public function fetch( $routerCacheEntity->deleteBlogCache(); } } - + $this->postRedirectGet->storeMessageSuccess('updated'); } @@ -79,7 +75,7 @@ public function fetch( $categories = $backendBlogCategoriesHelper->getCategoriesTree(); - $this->design->assign('category', $category); + $this->design->assign('category', $category); $this->design->assign('categories', $categories); $this->response->setContent($this->design->fetch('blog_category.tpl')); } diff --git a/backend/Controllers/BrandAdmin.php b/backend/Controllers/BrandAdmin.php index ca028516a..b0c4e726a 100644 --- a/backend/Controllers/BrandAdmin.php +++ b/backend/Controllers/BrandAdmin.php @@ -1,20 +1,17 @@ request->method('post')) { $brand = $brandsRequest->postBrand(); @@ -61,5 +58,4 @@ public function fetch( $this->design->assign('brand', $brand); $this->response->setContent($this->design->fetch('brand.tpl')); } - } diff --git a/backend/Controllers/CategoryAdmin.php b/backend/Controllers/CategoryAdmin.php index 0573363e3..a8485b908 100644 --- a/backend/Controllers/CategoryAdmin.php +++ b/backend/Controllers/CategoryAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { $category = $categoriesRequest->postCategory(); @@ -36,9 +33,8 @@ public function fetch( $this->postRedirectGet->storeMessageSuccess('added'); $this->postRedirectGet->storeNewEntityId($category->id); } else { - $categoryBeforeUpdate = $backendCategoriesHelper->getCategory($category->id); - + // Обновление категории $category = $backendCategoriesHelper->prepareUpdate($category->id, $category); $backendCategoriesHelper->update($category->id, $category); @@ -51,7 +47,7 @@ public function fetch( $routerCacheEntity->deleteProductsCache(); } } - + $this->postRedirectGet->storeMessageSuccess('updated'); } @@ -81,7 +77,7 @@ public function fetch( $categories = $backendCategoriesHelper->getCategoriesTree(); - $this->design->assign('category', $category); + $this->design->assign('category', $category); $this->design->assign('categories', $categories); $this->response->setContent($this->design->fetch('category.tpl')); } diff --git a/backend/Controllers/CategoryStatsAdmin.php b/backend/Controllers/CategoryStatsAdmin.php index ccf5860cf..ff21148a5 100644 --- a/backend/Controllers/CategoryStatsAdmin.php +++ b/backend/Controllers/CategoryStatsAdmin.php @@ -1,9 +1,7 @@ request->get('brand','integer')) { + if ($brandId = $this->request->get('brand', 'integer')) { $brand = $backendBrandsHelper->getBrand((int) $brandId); $this->design->assign('brand', $brand); } - if ($categoryId = $this->request->get('category','integer')) { + if ($categoryId = $this->request->get('category', 'integer')) { $category = $backendCategoriesHelper->getCategory((int) $categoryId); $brands = $backendBrandsHelper->findBrandsByCategory($category); $this->design->assign('category', $category); - $this->design->assign('brands', $brands); + $this->design->assign('brands', $brands); } $categories = $categoriesEntity->getCategoriesTree(); @@ -47,11 +44,10 @@ public function fetch( $categories_list = $backendCategoryStatsHelper->getStatistic($filter); $this->design->assign('categories_list', $categories_list); - $this->design->assign('categories', $categories); - $this->design->assign('total_price', $this->total_price); - $this->design->assign('total_amount', $this->total_amount); + $this->design->assign('categories', $categories); + $this->design->assign('total_price', $this->total_price); + $this->design->assign('total_amount', $this->total_amount); $this->response->setContent($this->design->fetch('category_stats.tpl')); } - } diff --git a/backend/Controllers/DeliveryAdmin.php b/backend/Controllers/DeliveryAdmin.php index b66c5d24b..943069413 100644 --- a/backend/Controllers/DeliveryAdmin.php +++ b/backend/Controllers/DeliveryAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { @@ -65,11 +62,10 @@ public function fetch( $paymentMethods = $paymentsEntity->find(); $deliveryModules = $modules->getDeliveryModules($this->manager->lang); - $this->design->assign('payment_methods', $paymentMethods); - $this->design->assign('delivery', $delivery); + $this->design->assign('payment_methods', $paymentMethods); + $this->design->assign('delivery', $delivery); $this->design->assign('delivery_modules', $deliveryModules); $this->response->setContent($this->design->fetch('delivery.tpl')); } - } diff --git a/backend/Controllers/DiscountsSettingsAdmin.php b/backend/Controllers/DiscountsSettingsAdmin.php index c73045c21..bcd8dbf55 100644 --- a/backend/Controllers/DiscountsSettingsAdmin.php +++ b/backend/Controllers/DiscountsSettingsAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { @@ -35,4 +33,4 @@ public function fetch( $this->response->setContent($this->design->fetch('discounts_settings.tpl')); } -} \ No newline at end of file +} diff --git a/backend/Controllers/ImportLogAdmin.php b/backend/Controllers/ImportLogAdmin.php index e3a4915c7..a56acbc00 100644 --- a/backend/Controllers/ImportLogAdmin.php +++ b/backend/Controllers/ImportLogAdmin.php @@ -124,64 +124,74 @@ public function fetch( } /*Выборка лога последнего успешного импорта*/ - private function getLogs($filter = array(), $is_count = true) + /** + * @param array $filter + * @return array{0: string, 1: array} + */ + public static function buildLogsQuery(array $filter, bool $isCount): array { - $keyword_filter = ''; - $status_filter = ''; - $sql_limit = ''; - - if (isset($filter['keyword'])) { - $keywords = explode(' ', $filter['keyword']); - foreach ($keywords as $keyword) { + $binds = []; + $keywordFilter = ''; + $statusFilter = ''; + $sqlLimit = ''; + + if (isset($filter['keyword']) && is_string($filter['keyword'])) { + $index = 0; + foreach (explode(' ', $filter['keyword']) as $keyword) { $kw = trim($keyword); - if ($kw !== '') { - $keyword_filter .= "AND ( - il.product_name LIKE '%$kw%' - OR il.variant_name LIKE '%$kw%' - ) "; + if ($kw === '') { + continue; } + $placeholder = 'il_kw_' . $index; + $keywordFilter .= " AND ( il.product_name LIKE :{$placeholder} ESCAPE '\\\\'" + . " OR il.variant_name LIKE :{$placeholder} ESCAPE '\\\\' ) "; + $binds[$placeholder] = '%' . self::escapeLike($kw) . '%'; + $index++; } } + if (isset($filter['status'])) { - $filterStatusParam = $filter['status']; - $status_filter = "AND il.status='$filterStatusParam'"; + $statusFilter = ' AND il.status = :il_status '; + $binds['il_status'] = (string) $filter['status']; } - if ($is_count) { + if ($isCount) { $select = 'count(il.id) as cnt'; } else { $select = 'il.*'; - $limit = 100; - if (isset($filter['limit'])) { - $limit = max(1, intval($filter['limit'])); - } - if (isset($filter['page'])) { - $page = max(1, intval($filter['page'])); - } - - $firstParam = ($page - 1) * $limit; - $secondParam = $limit; - $sql_limit = " LIMIT $firstParam, $secondParam "; + $limit = isset($filter['limit']) ? max(1, (int) $filter['limit']) : 100; + $page = isset($filter['page']) ? max(1, (int) $filter['page']) : 1; + $sqlLimit = ' LIMIT :il_offset, :il_limit '; + $binds['il_offset'] = ($page - 1) * $limit; + $binds['il_limit'] = $limit; } - $query = "SELECT $select + $query = "SELECT {$select} FROM __import_log AS il - WHERE - 1 - $keyword_filter - $status_filter - ORDER BY id DESC - $sql_limit - "; + WHERE 1 {$keywordFilter} {$statusFilter} + ORDER BY id DESC {$sqlLimit}"; + + return [$query, $binds]; + } + + private static function escapeLike(string $value): string + { + return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value); + } + + private function getLogs($filter = array(), $is_count = true) + { + [$query, $binds] = self::buildLogsQuery($filter, $is_count); $sql = $this->queryFactory->newSqlQuery(); $sql->setStatement($query); + $sql->bindValues($binds); $this->db->query($sql); if ($is_count) { return $this->db->result('cnt'); - } else { - return $this->db->results(); } + + return $this->db->results(); } } diff --git a/backend/Controllers/LanguageAdmin.php b/backend/Controllers/LanguageAdmin.php index cf96cbf59..e5edf1e13 100644 --- a/backend/Controllers/LanguageAdmin.php +++ b/backend/Controllers/LanguageAdmin.php @@ -1,27 +1,24 @@ getLangList(); $updateLanguage = new \stdClass(); - + /*Принимаем информацию о языке сайта*/ if ($this->request->method('post')) { $updateLanguage->id = $this->request->post('id', 'integer'); $updateLanguage->enabled = $this->request->post('enabled', 'integer'); - + if (empty($updateLanguage->id)) { $lang = $langList[$this->request->post('lang')]; $updateLanguage->name = $lang->name; @@ -30,7 +27,7 @@ public function fetch( if (!$updateLanguage->label) { $this->postRedirectGet->storeMessageError('label_empty'); - } elseif (($l = $languagesEntity->get((string)$updateLanguage->label)) && $l->id!=$updateLanguage->id) { + } elseif (($l = $languagesEntity->get((string)$updateLanguage->label)) && $l->id != $updateLanguage->id) { $this->postRedirectGet->storeMessageError('label_exists'); } else { /*Добавление/Обновление языка*/ @@ -39,10 +36,9 @@ public function fetch( $this->postRedirectGet->storeNewEntityId($updateLanguage->id); } } else { - $currentLangId = $languagesCore->getLangId(); $otherLanguagesNames = $this->request->post('other_languages_names'); - foreach($otherLanguagesNames as $langId=>$langName) { + foreach ($otherLanguagesNames as $langId => $langName) { $languagesCore->setLangId($langId); $languagesEntity->update($updateLanguage->id, ['name' => $langName]); } @@ -55,7 +51,7 @@ public function fetch( } else { $updateLanguage->id = $this->request->get('id', 'integer'); } - + if (!empty($updateLanguage->id)) { $updateLanguage = $languagesEntity->getMultiLanguage($updateLanguage->id); } @@ -63,10 +59,9 @@ public function fetch( $languages = $languagesEntity->mappedBy('id')->find(); $this->design->assign('lang_list', $langList); - $this->design->assign('language', $updateLanguage); + $this->design->assign('language', $updateLanguage); $this->design->assign('languages', $languages); $this->response->setContent($this->design->fetch('language.tpl')); } - } diff --git a/backend/Controllers/LearningAdmin.php b/backend/Controllers/LearningAdmin.php index 193c78599..17b079826 100644 --- a/backend/Controllers/LearningAdmin.php +++ b/backend/Controllers/LearningAdmin.php @@ -1,9 +1,7 @@ request->post('skip_learning')); } -} \ No newline at end of file +} diff --git a/backend/Controllers/LicenseAdmin.php b/backend/Controllers/LicenseAdmin.php index 1c3709049..5cacf35a5 100644 --- a/backend/Controllers/LicenseAdmin.php +++ b/backend/Controllers/LicenseAdmin.php @@ -1,15 +1,11 @@ response->setContent($this->design->fetch('license.tpl')); } - } diff --git a/backend/Controllers/ModulesLicenseInfoAdmin.php b/backend/Controllers/ModulesLicenseInfoAdmin.php index c3ee747ac..e866e7a2c 100644 --- a/backend/Controllers/ModulesLicenseInfoAdmin.php +++ b/backend/Controllers/ModulesLicenseInfoAdmin.php @@ -1,14 +1,11 @@ response->setContent($this->design->fetch('modules_license_info_admin.tpl')); } - -} \ No newline at end of file +} diff --git a/backend/Controllers/OpenAiAdmin.php b/backend/Controllers/OpenAiAdmin.php index 9339ed54c..224581076 100644 --- a/backend/Controllers/OpenAiAdmin.php +++ b/backend/Controllers/OpenAiAdmin.php @@ -1,15 +1,12 @@ request->post('statuses')) { if ($positions = $orderSettingsRequest->postPositions()) { @@ -34,7 +31,6 @@ public function fetch( $ordersStatuses = $backendOrdersHelper->findStatuses(); $statusesNotToDelete = []; if (!empty($idsToDelete) && ($idsNotToDelete = $backendOrderSettingsHelper->statusCanBeDeleted($idsToDelete)) !== false) { - foreach ($idsToDelete as $idToDelete) { if (in_array($idToDelete, $idsNotToDelete)) { $statusesNotToDelete[] = $ordersStatuses[$idToDelete]->name; @@ -43,7 +39,7 @@ public function fetch( } } } - + if (!empty($statusesNotToDelete)) { $backendPostRedirectGet->storeMessageError( $backendTranslations->getTranslation('error_delete_statuses') @@ -51,10 +47,10 @@ public function fetch( . implode(', ', $statusesNotToDelete) ); } - + $backendPostRedirectGet->redirect(); } - + /*Метки заказов*/ if ($this->request->post('labels')) { @@ -74,12 +70,10 @@ public function fetch( // Отображение $ordersStatuses = $backendOrdersHelper->findStatuses(); $this->design->assign('orders_statuses', $ordersStatuses); - + $labels = $backendOrdersHelper->findLabels(); $this->design->assign('labels', $labels); $this->response->setContent($this->design->fetch('order_settings.tpl')); } - } - diff --git a/backend/Controllers/PaymentMethodAdmin.php b/backend/Controllers/PaymentMethodAdmin.php index e24012af5..86a1e9113 100644 --- a/backend/Controllers/PaymentMethodAdmin.php +++ b/backend/Controllers/PaymentMethodAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { - $paymentMethod = $backendPaymentsRequest->postPayment(); $paymentSettings = $backendPaymentsRequest->postSettings(); $paymentDeliveries = $backendPaymentsRequest->postPaymentDeliveries(); @@ -67,10 +63,10 @@ public function fetch( $paymentModules = $modules->getPaymentModules($this->manager->lang); $currencies = $currenciesEntity->find(); - $this->design->assign('deliveries', $deliveries); - $this->design->assign('payment_method', $paymentMethod); + $this->design->assign('deliveries', $deliveries); + $this->design->assign('payment_method', $paymentMethod); $this->design->assign('payment_modules', $paymentModules); - $this->design->assign('currencies', $currencies); + $this->design->assign('currencies', $currencies); $this->response->setContent($this->design->fetch('payment_method.tpl')); } diff --git a/backend/Controllers/ProductAdmin.php b/backend/Controllers/ProductAdmin.php index 3e8de1718..e3f0adc35 100644 --- a/backend/Controllers/ProductAdmin.php +++ b/backend/Controllers/ProductAdmin.php @@ -1,9 +1,7 @@ -request->method('post') && !empty($_POST)) { @@ -54,7 +51,7 @@ public function fetch( $product = $backendProductsHelper->getProduct($preparedProduct->id); $routerCacheEntity->deleteByUrl(RouterCacheEntity::TYPE_PRODUCT, $product->url); - + $this->postRedirectGet->storeMessageSuccess('updated'); } @@ -70,7 +67,7 @@ public function fetch( $images = $productRequest->postImages(); $droppedImages = $productRequest->fileDroppedImages(); $backendProductsHelper->updateImages($product, $images, $droppedImages); - + // Промо-изображения $specImages = $productRequest->postSpecialImages(); $specDroppedImages = $productRequest->fileDroppedSpecialImages(); @@ -122,19 +119,19 @@ public function fetch( $product->brand_id = $brand_id; } - $this->design->assign('product', $product); - $this->design->assign('special_images', $specialImages); + $this->design->assign('product', $product); + $this->design->assign('special_images', $specialImages); $this->design->assign('product_categories', $productCategories); - $this->design->assign('product_variants', $productVariants); - $this->design->assign('product_images', $productImages); - $this->design->assign('features', $features); - $this->design->assign('features_values', $featuresValues); - $this->design->assign('related_products', $relatedProducts); + $this->design->assign('product_variants', $productVariants); + $this->design->assign('product_images', $productImages); + $this->design->assign('features', $features); + $this->design->assign('features_values', $featuresValues); + $this->design->assign('related_products', $relatedProducts); $brandsCount = $brandsEntity->count(); $brands = $brandsEntity->find(['limit' => $brandsCount]); - $this->design->assign('brands', $brands); + $this->design->assign('brands', $brands); $this->design->assign('categories', $categoriesTree); $this->design->assign('currencies', $currenciesEntity->find()); diff --git a/backend/Controllers/ScriptsAdmin.php b/backend/Controllers/ScriptsAdmin.php index 2da62c383..701fddcd8 100644 --- a/backend/Controllers/ScriptsAdmin.php +++ b/backend/Controllers/ScriptsAdmin.php @@ -59,6 +59,11 @@ public function fetch(FrontTemplateConfig $frontTemplateConfig) $this->design->assign('theme', $currentTheme); $this->design->assign('scripts', $scripts); + $this->design->assign( + 'theme_editor_enabled', + (bool) $this->config->get('allow_theme_file_editor') + && $this->managers->access('scripts', $this->manager) + ); $this->response->setContent($this->design->fetch('scripts.tpl')); } } diff --git a/backend/Controllers/SeoPatternsAdmin.php b/backend/Controllers/SeoPatternsAdmin.php index 3bbc6559c..aaaa7ab82 100644 --- a/backend/Controllers/SeoPatternsAdmin.php +++ b/backend/Controllers/SeoPatternsAdmin.php @@ -1,16 +1,13 @@ design->setTemplatesDir('backend/design/html'); $this->design->setCompiledDir('backend/design/compiled'); - if ($this->request->post("ajax")){ + if ($this->request->post("ajax")) { /*Получение категории*/ if ($this->request->post("action") == "get") { $result = new \stdClass(); @@ -39,7 +36,7 @@ public function fetch( $result->success = false; } } - + $result->tpl = $this->design->fetch("seo_patterns_ajax.tpl"); $this->response->setContent(json_encode($result), RESPONSE_JSON); return; @@ -62,7 +59,6 @@ public function fetch( $this->design->assign("category", $defaultProductsSeoPattern); $result->success = true; } else { - $categoryId = $this->request->post("category_id", "integer"); if ($category = $categoriesEntity->get($categoryId)) { $categoryToUpdate = new \stdClass(); @@ -72,7 +68,7 @@ public function fetch( $categoryToUpdate->auto_annotation = $this->request->post('auto_annotation'); $categoryToUpdate->auto_description = $this->request->post('auto_description'); $categoryToUpdate->auto_h1 = $this->request->post('auto_h1'); - + $categoriesEntity->update($category->id, $categoryToUpdate); $category = $categoriesEntity->get($categoryId); $this->design->assign('features', $featuresEntity->find(array('category_id' => $category->id))); @@ -86,7 +82,6 @@ public function fetch( $result->tpl = $this->design->fetch("seo_patterns_ajax.tpl"); $this->response->setContent(json_encode($result), RESPONSE_JSON); return; - } } diff --git a/backend/Controllers/SettingsCatalogAdmin.php b/backend/Controllers/SettingsCatalogAdmin.php index 29ed103e3..8dd035bbf 100644 --- a/backend/Controllers/SettingsCatalogAdmin.php +++ b/backend/Controllers/SettingsCatalogAdmin.php @@ -1,9 +1,7 @@ findManagers(); $this->design->assign('managers', $managersList); if ($this->request->method('POST')) { - if ($this->request->post('save')) { $backendSettingsCatalogHelper->updateSettings(); } @@ -31,19 +28,14 @@ public function fetch( if (empty($error = $backendValidateHelper->getTruncateTableValidateError())) { if ($settingsCatalogRequest->postTruncateTableConfirm() === "1") { $backendSettingsCatalogHelper->clearCatalog(); - } elseif ($settingsCatalogRequest->postTruncateTableConfirmEntity() === "category") { $backendSettingsCatalogHelper->clearCategorys(); - } elseif ($settingsCatalogRequest->postTruncateTableConfirmEntity() === "product") { $backendSettingsCatalogHelper->clearProducts(); - } elseif ($settingsCatalogRequest->postTruncateTableConfirmEntity() === "brand") { $backendSettingsCatalogHelper->clearBrands(); - } elseif ($settingsCatalogRequest->postTruncateTableConfirmEntity() === "feature") { $backendSettingsCatalogHelper->clearFeatures(); - } elseif ($settingsCatalogRequest->postTruncateTableConfirmEntity() === "blog") { $backendSettingsCatalogHelper->clearBlogs(); } diff --git a/backend/Controllers/SettingsCounterAdmin.php b/backend/Controllers/SettingsCounterAdmin.php index b5fb161f7..90e2202ab 100644 --- a/backend/Controllers/SettingsCounterAdmin.php +++ b/backend/Controllers/SettingsCounterAdmin.php @@ -1,9 +1,7 @@ request->method('POST')) { $counters = $settingsRequest->postCounters(); $backendSettingsHelper->updateCounters($counters); diff --git a/backend/Controllers/SettingsNotifyAdmin.php b/backend/Controllers/SettingsNotifyAdmin.php index 14850b7f5..406debe3d 100644 --- a/backend/Controllers/SettingsNotifyAdmin.php +++ b/backend/Controllers/SettingsNotifyAdmin.php @@ -1,16 +1,13 @@ getLangList() as $label=>$l) { - if (file_exists("backend/lang/".$label.".php")) { + foreach ($languages->getLangList() as $label => $l) { + if (file_exists("backend/lang/" . $label . ".php")) { $btrLanguages[$l->name] = $l->label; } } @@ -40,7 +37,7 @@ public function testSMTP(Notify $notify) $this->settings->set('smtp_pass', $this->request->post('smtp_pass')); $this->settings->set('disable_validate_smtp_certificate', $this->request->post('disable_validate_smtp_certificate', 'int')); - $from = ($this->settings->get('notify_from_name') ? $this->settings->get('notify_from_name')." <".$this->settings->get('notify_from_email').">" : $this->settings->get('notify_from_email')); + $from = ($this->settings->get('notify_from_name') ? $this->settings->get('notify_from_name') . " <" . $this->settings->get('notify_from_email') . ">" : $this->settings->get('notify_from_email')); $trace = $notify->SMTP( $this->manager->email, @@ -59,8 +56,7 @@ public function testSMTP(Notify $notify) $result['trace'] = $trace; $result['message'] = 'Connect failed'; } - + $this->response->setContent(json_encode($result), RESPONSE_JSON); } - } diff --git a/backend/Controllers/SettingsOpenAiAdmin.php b/backend/Controllers/SettingsOpenAiAdmin.php index 89b55798c..3a0367078 100644 --- a/backend/Controllers/SettingsOpenAiAdmin.php +++ b/backend/Controllers/SettingsOpenAiAdmin.php @@ -1,16 +1,14 @@ request->method('post')) { @@ -20,4 +18,4 @@ public function fetch( $this->design->assign('open_ai_models', $openAiHelper->getTextModels()); $this->response->setContent($this->design->fetch('settings_open_ai.tpl')); } -} \ No newline at end of file +} diff --git a/backend/Controllers/SettingsRouterAdmin.php b/backend/Controllers/SettingsRouterAdmin.php index 5db7ef7a7..22d9c6ce9 100644 --- a/backend/Controllers/SettingsRouterAdmin.php +++ b/backend/Controllers/SettingsRouterAdmin.php @@ -1,21 +1,17 @@ request->method('POST')) { - // Если сменили стратегию генерации урла, очистим кеш для прежней стратегии if ($this->settings->get('product_routes_template') != $this->request->post('product_routes_template')) { $cacheEntity->deleteProductsCache(); @@ -29,11 +25,11 @@ public function fetch( if ($this->settings->get('post_routes_template') != $this->request->post('post_routes_template')) { $cacheEntity->deleteBlogCache(); } - + $backendSettingsHelper->updateRouterSettings(); $this->design->assign('message_success', 'saved'); } $this->response->setContent($this->design->fetch('settings_router.tpl')); } -} \ No newline at end of file +} diff --git a/backend/Controllers/StatsAdmin.php b/backend/Controllers/StatsAdmin.php index aa7f6164e..521a94495 100644 --- a/backend/Controllers/StatsAdmin.php +++ b/backend/Controllers/StatsAdmin.php @@ -1,16 +1,12 @@ response->setContent($this->design->fetch('stats.tpl')); } - } diff --git a/backend/Controllers/StylesAdmin.php b/backend/Controllers/StylesAdmin.php index cf9a144b1..d099dcec3 100644 --- a/backend/Controllers/StylesAdmin.php +++ b/backend/Controllers/StylesAdmin.php @@ -60,6 +60,11 @@ public function fetch(FrontTemplateConfig $frontTemplateConfig) $this->design->assign('theme', $currentTheme); $this->design->assign('styles', $styles); + $this->design->assign( + 'theme_editor_enabled', + (bool) $this->config->get('allow_theme_file_editor') + && $this->managers->access('style_templates', $this->manager) + ); $this->response->setContent($this->design->fetch('styles.tpl')); } } diff --git a/backend/Controllers/SystemAdmin.php b/backend/Controllers/SystemAdmin.php index e64d2d760..1b8d15b4f 100644 --- a/backend/Controllers/SystemAdmin.php +++ b/backend/Controllers/SystemAdmin.php @@ -1,12 +1,9 @@ design->assign('sql_info', $sqlInfo); $this->design->assign('php_version', $phpVersion); $this->design->assign('all_extensions', $allExtensions); - $this->design->assign('ini_params',$iniParams); + $this->design->assign('ini_params', $iniParams); $this->design->assign('server_ip', $serverIp); $this->response->setContent($this->design->fetch('settings_system.tpl')); } -} \ No newline at end of file +} diff --git a/backend/Controllers/TemplatesAdmin.php b/backend/Controllers/TemplatesAdmin.php index cc9626133..584b4a6f7 100644 --- a/backend/Controllers/TemplatesAdmin.php +++ b/backend/Controllers/TemplatesAdmin.php @@ -1,32 +1,29 @@ getTheme(); - if ($this->request->get("email")){ - $templatesDir = 'design/'.$currentTheme.'/html/email/'; + if ($this->request->get("email")) { + $templatesDir = 'design/' . $currentTheme . '/html/email/'; $this->design->assign('current_dir', 'email'); } else { - $templatesDir = 'design/'.$currentTheme.'/html/'; + $templatesDir = 'design/' . $currentTheme . '/html/'; $this->design->assign('current_dir', 'html'); } $templates = []; // Читаем все tpl-файлы - if($handle = opendir($templatesDir)) { - while(false !== ($file = readdir($handle))) { - if(is_file($templatesDir.$file) && $file[0] != '.' && pathinfo($file, PATHINFO_EXTENSION) == 'tpl') { + if ($handle = opendir($templatesDir)) { + while (false !== ($file = readdir($handle))) { + if (is_file($templatesDir . $file) && $file[0] != '.' && pathinfo($file, PATHINFO_EXTENSION) == 'tpl') { $templates[] = $file; } } @@ -36,28 +33,28 @@ public function fetch(FrontTemplateConfig $frontTemplateConfig) // Текущий шаблон $templateFile = $this->request->get('file'); - + if (!empty($templateFile) && pathinfo($templateFile, PATHINFO_EXTENSION) != 'tpl') { exit(); } - - if (!isset($templateFile)){ + + if (!isset($templateFile)) { $templateFile = reset($templates); } // Передаем имя шаблона в дизайн $this->design->assign('template_file', $templateFile); - + // Если можем прочитать файл - передаем содержимое в дизайн - if (is_readable($templatesDir.$templateFile)) { - $template_content = file_get_contents($templatesDir.$templateFile); + if (is_readable($templatesDir . $templateFile)) { + $template_content = file_get_contents($templatesDir . $templateFile); $this->design->assign('template_content', $template_content); } - + // Если нет прав на запись - передаем в дизайн предупреждение - if (!empty($templateFile) && !is_writable($templatesDir.$templateFile) && !is_file($templatesDir.'../locked')) { + if (!empty($templateFile) && !is_writable($templatesDir . $templateFile) && !is_file($templatesDir . '../locked')) { $this->design->assign('message_error', 'permissions'); - } elseif (is_file($templatesDir.'../locked')) { + } elseif (is_file($templatesDir . '../locked')) { $this->design->assign('message_error', 'theme_locked'); } else { // Запоминаем в сессии имя редактируемого шаблона @@ -66,8 +63,12 @@ public function fetch(FrontTemplateConfig $frontTemplateConfig) $this->design->assign('theme', $currentTheme); $this->design->assign('templates', $templates); - + $this->design->assign( + 'theme_editor_enabled', + (bool) $this->config->get('allow_theme_file_editor') + && $this->managers->access('file_templates', $this->manager) + ); + $this->response->setContent($this->design->fetch('templates.tpl')); } - } diff --git a/backend/Controllers/TranslationAdmin.php b/backend/Controllers/TranslationAdmin.php index cbd1bf90a..4661198da 100644 --- a/backend/Controllers/TranslationAdmin.php +++ b/backend/Controllers/TranslationAdmin.php @@ -1,51 +1,48 @@ find(); - + $locked_theme = is_file('design/' . $frontTemplateConfig->getTheme() . '/locked'); $this->design->assign('locked_theme', $locked_theme); $translation = new \stdClass(); - if(!$locked_theme && $this->request->method('post')) { + if (!$locked_theme && $this->request->method('post')) { // id - предыдущий label $translation->id = $this->request->post('id'); $translation->label = trim($this->request->post('label')); $translation->label = str_replace(" ", '_', $translation->label); $translation->label = preg_replace("/[^a-z0-9\-_]/i", "", $translation->label); - - if ($languages){ - foreach($languages as $lang) { - $field = 'lang_'.$lang->label; + + if ($languages) { + foreach ($languages as $lang) { + $field = 'lang_' . $lang->label; $translation->$field = $this->request->post($field); $translation->values[$lang->id] = $translation->$field; } } $exist = $translationsEntity->templateOnly(true)->get($translation->label); - - if(!$translation->label) { + + if (!$translation->label) { $this->design->assign('message_error', 'label_empty'); - } elseif($exist && $exist->id!=$translation->id) { + } elseif ($exist && $exist->id != $translation->id) { $this->design->assign('message_error', 'label_exists'); - } elseif(false) { // todo возможно сделать проверку, чтобы переменная не была класом + } elseif (false) { // todo возможно сделать проверку, чтобы переменная не была класом $this->design->assign('message_error', 'label_is_class'); } else { /*Добавление/Удаление перевода*/ - if(empty($translation->id)) { + if (empty($translation->id)) { $this->design->assign('message_success', 'added'); } else { $this->design->assign('message_success', 'updated'); @@ -56,14 +53,13 @@ public function fetch(TranslationsEntity $translationsEntity, FrontTemplateConfi $translation->id = $this->request->get('id'); } - if(!empty($translation->id)) { + if (!empty($translation->id)) { $translation = $translationsEntity->get($translation->id); } - + $this->design->assign('languages', $languages); - + $this->design->assign('translation', $translation); $this->response->setContent($this->design->fetch('translation.tpl')); } - } diff --git a/backend/Controllers/UserAdmin.php b/backend/Controllers/UserAdmin.php index 9caf51905..fa11b5ceb 100644 --- a/backend/Controllers/UserAdmin.php +++ b/backend/Controllers/UserAdmin.php @@ -1,26 +1,23 @@ request->method('post')) { $user = $backendUsersRequest->postUser(); - + /*Не допустить одинаковые email пользователей*/ if ($error = $backendValidateHelper->getUsersValidateError($user)) { $this->design->assign('message_error', $error); @@ -37,9 +34,8 @@ public function fetch( $groups = $backendUsersHelper->getAllGroups(); $this->design->assign('groups', $groups); - $this->design->assign('user', $user); + $this->design->assign('user', $user); $this->response->setContent($this->design->fetch('user.tpl')); } - } diff --git a/backend/Controllers/UserGroupAdmin.php b/backend/Controllers/UserGroupAdmin.php index fe37b0378..e4227bd54 100644 --- a/backend/Controllers/UserGroupAdmin.php +++ b/backend/Controllers/UserGroupAdmin.php @@ -1,9 +1,7 @@ request->method('post')) { $group = $backendUserGroupsRequest->postGroup(); if ($error = $backendValidateHelper->getUserGroupsValidateError($group)) { - } else { /*Добавление/Обновление групы пользователей*/ if (empty($group->id)) { @@ -49,5 +45,4 @@ public function fetch( $this->design->assign('group', $group); $this->response->setContent($this->design->fetch('user_group.tpl')); } - } diff --git a/backend/Helpers/BackendAuthorsHelper.php b/backend/Helpers/BackendAuthorsHelper.php index aadd96ca5..f2b790a73 100644 --- a/backend/Helpers/BackendAuthorsHelper.php +++ b/backend/Helpers/BackendAuthorsHelper.php @@ -1,9 +1,7 @@ authorsEntity = $entityFactory->get(AuthorsEntity::class); $this->config = $config; @@ -126,7 +124,7 @@ public function uploadImage($image, $author) $this->config->get('resized_authors_dir') ); - $this->authorsEntity->update($author->id, ['image'=>$filename]); + $this->authorsEntity->update($author->id, ['image' => $filename]); } return ExtenderFacade::execute(__METHOD__, null, func_get_args()); @@ -155,8 +153,8 @@ public function sortPositions($positions) $ids = array_keys($positions); sort($positions); - foreach ($positions as $i=>$position) { - $this->authorsEntity->update($ids[$i], ['position'=>$position]); + foreach ($positions as $i => $position) { + $this->authorsEntity->update($ids[$i], ['position' => $position]); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); @@ -192,8 +190,8 @@ public function makePagination($authorsCount, $filter) $filter['limit'] = $authorsCount; } - if ($filter['limit']>0) { - $pagesCount = ceil($authorsCount/$filter['limit']); + if ($filter['limit'] > 0) { + $pagesCount = ceil($authorsCount / $filter['limit']); } else { $pagesCount = 0; } @@ -205,9 +203,9 @@ public function makePagination($authorsCount, $filter) public function duplicate($ids) { - foreach($ids as $id) { + foreach ($ids as $id) { $this->authorsEntity->duplicate((int)$id); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendBlogCategoriesHelper.php b/backend/Helpers/BackendBlogCategoriesHelper.php index ab18b8929..05e40e168 100644 --- a/backend/Helpers/BackendBlogCategoriesHelper.php +++ b/backend/Helpers/BackendBlogCategoriesHelper.php @@ -1,9 +1,7 @@ config = $config; $this->imageCore = $imageCore; @@ -50,7 +47,7 @@ public function sortPositions($positions) public function updatePositions($ids, $positions) { - foreach($positions as $i=>$position) { + foreach ($positions as $i => $position) { $this->categoriesEntity->update($ids[$i], array('position' => (int) $position)); } @@ -124,7 +121,6 @@ public function prepareUploadCategoryImage($category, $image) public function uploadCategoryImage($category, $image) { if (!empty($image['name']) && ($filename = $this->imageCore->uploadImage($image['tmp_name'], $image['name'], $this->config->get('original_blog_categories_dir')))) { - $this->imageCore->deleteImage( $category->id, 'image', @@ -133,7 +129,7 @@ public function uploadCategoryImage($category, $image) $this->config->get('resized_blog_categories_dir') ); - $this->categoriesEntity->update($category->id, ['image'=>$filename]); + $this->categoriesEntity->update($category->id, ['image' => $filename]); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); @@ -147,11 +143,11 @@ public function findCategories($filter = []) public function duplicateCategories($ids) { - foreach($ids as $id) { + foreach ($ids as $id) { $category = $this->categoriesEntity->get((int)$id); $this->categoriesEntity->duplicate((int)$id, $category->parent_id); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendBrandsHelper.php b/backend/Helpers/BackendBrandsHelper.php index 3fdebfbfe..c87ee0441 100644 --- a/backend/Helpers/BackendBrandsHelper.php +++ b/backend/Helpers/BackendBrandsHelper.php @@ -1,9 +1,7 @@ brandsEntity = $entityFactory->get(BrandsEntity::class); $this->categoriesEntity = $entityFactory->get(CategoriesEntity::class); $this->config = $config; @@ -147,7 +145,7 @@ public function uploadImage($image, $brand) $this->config->resized_brands_dir ); - $this->brandsEntity->update($brand->id, ['image'=>$filename]); + $this->brandsEntity->update($brand->id, ['image' => $filename]); } return ExtenderFacade::execute(__METHOD__, null, func_get_args()); @@ -177,15 +175,15 @@ public function moveToPage($ids, $targetPage, $filter) $filter['page'] = $targetPage; // До какого бренда перемещать - $limit = $filter['limit']*($targetPage-1); + $limit = $filter['limit'] * ($targetPage - 1); if ($targetPage > $this->request->get('page', 'integer')) { - $limit += count($ids)-1; + $limit += count($ids) - 1; } else { $ids = array_reverse($ids, true); } $tempFilter = $filter; - $tempFilter['page'] = $limit+1; + $tempFilter['page'] = $limit + 1; $tempFilter['limit'] = 1; $tmp = $this->brandsEntity->find($tempFilter); $targetBrand = array_pop($tmp); @@ -193,7 +191,6 @@ public function moveToPage($ids, $targetPage, $filter) // Если вылезли за последний бренд - берем позицию последнего бренда в качестве цели перемещения if ($targetPage > $this->request->get('page', 'integer') && !$targetPosition) { - $select = $this->queryFactory->newSelect(); $select->from('__brands') ->cols(['distinct position AS target']) @@ -240,8 +237,8 @@ public function sortPositions($positions) $ids = array_keys($positions); sort($positions); - foreach ($positions as $i=>$position) { - $this->brandsEntity->update($ids[$i], ['position'=>$position]); + foreach ($positions as $i => $position) { + $this->brandsEntity->update($ids[$i], ['position' => $position]); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); @@ -282,8 +279,8 @@ public function makePagination($brandsCount, $filter) $filter['limit'] = $brandsCount; } - if ($filter['limit']>0) { - $pagesCount = ceil($brandsCount/$filter['limit']); + if ($filter['limit'] > 0) { + $pagesCount = ceil($brandsCount / $filter['limit']); } else { $pagesCount = 0; } @@ -301,7 +298,7 @@ public function findBrandsByCategory($category) public function duplicate($ids) { - foreach($ids as $id) { + foreach ($ids as $id) { $this->brandsEntity->duplicate((int)$id); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); @@ -310,7 +307,7 @@ public function duplicate($ids) public function sortBrandsPositionsAlphabet() { $brands = []; - foreach($this->brandsEntity->cols(['id', 'name'])->find() as $b) { + foreach ($this->brandsEntity->cols(['id', 'name'])->find() as $b) { $brands[$b->id] = $b->name; } @@ -319,10 +316,10 @@ public function sortBrandsPositionsAlphabet() $i = 0; $brandIds = array_keys($brands); - foreach($brandIds as $brandId) { - $this->brandsEntity->update($brandId, ['position'=>$i++]); + foreach ($brandIds as $brandId) { + $this->brandsEntity->update($brandId, ['position' => $i++]); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendCallbacksHelper.php b/backend/Helpers/BackendCallbacksHelper.php index c8a947792..41ed1259c 100644 --- a/backend/Helpers/BackendCallbacksHelper.php +++ b/backend/Helpers/BackendCallbacksHelper.php @@ -1,9 +1,7 @@ callbacksEntity = $entityFactory->get(CallbacksEntity::class); $this->request = $request; @@ -37,13 +35,13 @@ public function delete($ids) public function processed($ids) { - $this->callbacksEntity->update($ids, ['processed'=>1]); + $this->callbacksEntity->update($ids, ['processed' => 1]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } public function unprocessed($ids) { - $this->callbacksEntity->update($ids, ['unprocessed'=>1]); + $this->callbacksEntity->update($ids, ['unprocessed' => 1]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } @@ -78,7 +76,7 @@ public function buildFilter() $callbacksCount = $this->callbacksEntity->count($filter); // Показать все страницы сразу - if($this->request->get('page') == 'all') { + if ($this->request->get('page') == 'all') { $filter['limit'] = $callbacksCount; } @@ -98,4 +96,4 @@ public function countCallbacks($filter) $callbacksCount = $this->callbacksEntity->count($filter); return ExtenderFacade::execute(__METHOD__, $callbacksCount, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendCategoriesHelper.php b/backend/Helpers/BackendCategoriesHelper.php index f7753a1a5..205f3e99f 100644 --- a/backend/Helpers/BackendCategoriesHelper.php +++ b/backend/Helpers/BackendCategoriesHelper.php @@ -1,9 +1,7 @@ config = $config; $this->imageCore = $imageCore; @@ -50,7 +47,7 @@ public function sortPositions($positions) public function updatePositions($ids, $positions) { - foreach($positions as $i=>$position) { + foreach ($positions as $i => $position) { $this->categoriesEntity->update($ids[$i], array('position' => (int) $position)); } @@ -124,7 +121,6 @@ public function prepareUploadCategoryImage($category, $image) public function uploadCategoryImage($category, $image) { if (!empty($image['name']) && ($filename = $this->imageCore->uploadImage($image['tmp_name'], $image['name'], $this->config->original_categories_dir))) { - $this->imageCore->deleteImage( $category->id, 'image', @@ -133,7 +129,7 @@ public function uploadCategoryImage($category, $image) $this->config->resized_categories_dir ); - $this->categoriesEntity->update($category->id, ['image'=>$filename]); + $this->categoriesEntity->update($category->id, ['image' => $filename]); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); @@ -147,11 +143,11 @@ public function findCategories($filter = []) public function duplicateCategories($ids) { - foreach($ids as $id) { + foreach ($ids as $id) { $category = $this->categoriesEntity->get((int)$id); $this->categoriesEntity->duplicate((int)$id, $category->parent_id); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendCategoryStatsHelper.php b/backend/Helpers/BackendCategoryStatsHelper.php index abe810e9d..da072b7c0 100644 --- a/backend/Helpers/BackendCategoryStatsHelper.php +++ b/backend/Helpers/BackendCategoryStatsHelper.php @@ -1,9 +1,7 @@ request = $request; $this->commentsEntity = $entityFactory->get(CommentsEntity::class); $this->productsEntity = $entityFactory->get(ProductsEntity::class); @@ -147,7 +145,7 @@ public function delete($ids) public function approve($ids) { - $this->commentsEntity->update($ids, array('approved'=>1)); + $this->commentsEntity->update($ids, array('approved' => 1)); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } @@ -235,4 +233,4 @@ public function matchStatus($filter) return ExtenderFacade::execute(__METHOD__, $status, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendCouponsHelper.php b/backend/Helpers/BackendCouponsHelper.php index d83186f30..45724018e 100644 --- a/backend/Helpers/BackendCouponsHelper.php +++ b/backend/Helpers/BackendCouponsHelper.php @@ -1,9 +1,7 @@ couponsEntity->add($coupon); return ExtenderFacade::execute(__METHOD__, $insertId, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendCurrenciesHelper.php b/backend/Helpers/BackendCurrenciesHelper.php index 0fc6f191d..dc38975ed 100644 --- a/backend/Helpers/BackendCurrenciesHelper.php +++ b/backend/Helpers/BackendCurrenciesHelper.php @@ -1,9 +1,7 @@ currenciesEntity = $entityFactory->get(CurrenciesEntity::class); $this->queryFactory = $queryFactory; $this->db = $db; @@ -108,64 +106,64 @@ public function recalculateCurrencies($currencies) $oldCurrency = $this->currenciesEntity->getMainCurrency(); $newCurrency = reset($currencies); if (!empty($oldCurrency) && $oldCurrency->id != $newCurrency->id) { - $coef = $newCurrency->rate_from/$newCurrency->rate_to; + $coef = $newCurrency->rate_from / $newCurrency->rate_to; /*Пересчет цен по курсу валюты*/ if ($this->request->post('recalculate') == 1) { $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".VariantsEntity::getTable()." SET price=price*{$coef}, compare_price=compare_price*{$coef} where currency_id=0"); + $sql->setStatement("UPDATE " . VariantsEntity::getTable() . " SET price=price*{$coef}, compare_price=compare_price*{$coef} where currency_id=0"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".DeliveriesEntity::getTable()." SET price=price*{$coef}, free_from=free_from*{$coef}"); + $sql->setStatement("UPDATE " . DeliveriesEntity::getTable() . " SET price=price*{$coef}, free_from=free_from*{$coef}"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".OrdersEntity::getTable()." SET delivery_price=delivery_price*{$coef}"); + $sql->setStatement("UPDATE " . OrdersEntity::getTable() . " SET delivery_price=delivery_price*{$coef}"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".OrdersEntity::getTable()." SET undiscounted_total_price=undiscounted_total_price*{$coef}"); + $sql->setStatement("UPDATE " . OrdersEntity::getTable() . " SET undiscounted_total_price=undiscounted_total_price*{$coef}"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".OrdersEntity::getTable()." SET total_price=total_price*{$coef}"); + $sql->setStatement("UPDATE " . OrdersEntity::getTable() . " SET total_price=total_price*{$coef}"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".PurchasesEntity::getTable()." SET undiscounted_price=undiscounted_price*{$coef}"); + $sql->setStatement("UPDATE " . PurchasesEntity::getTable() . " SET undiscounted_price=undiscounted_price*{$coef}"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".PurchasesEntity::getTable()." SET price=price*{$coef}"); + $sql->setStatement("UPDATE " . PurchasesEntity::getTable() . " SET price=price*{$coef}"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".CouponsEntity::getTable()." SET value=value*{$coef} WHERE type='absolute'"); + $sql->setStatement("UPDATE " . CouponsEntity::getTable() . " SET value=value*{$coef} WHERE type='absolute'"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".CouponsEntity::getTable()." SET min_order_price=min_order_price*{$coef}"); + $sql->setStatement("UPDATE " . CouponsEntity::getTable() . " SET min_order_price=min_order_price*{$coef}"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".DiscountsEntity::getTable()." SET value=value*{$coef} WHERE type='absolute'"); + $sql->setStatement("UPDATE " . DiscountsEntity::getTable() . " SET value=value*{$coef} WHERE type='absolute'"); $this->db->query($sql); } $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".CurrenciesEntity::getTable()." SET rate_from=1.0*rate_from*$newCurrency->rate_to/$oldCurrency->rate_to"); + $sql->setStatement("UPDATE " . CurrenciesEntity::getTable() . " SET rate_from=1.0*rate_from*$newCurrency->rate_to/$oldCurrency->rate_to"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".CurrenciesEntity::getTable()." SET rate_to=1.0*rate_to*$newCurrency->rate_from/$oldCurrency->rate_from"); + $sql->setStatement("UPDATE " . CurrenciesEntity::getTable() . " SET rate_to=1.0*rate_to*$newCurrency->rate_from/$oldCurrency->rate_from"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".CurrenciesEntity::getTable()." SET rate_to = rate_from WHERE id={$newCurrency->id}"); + $sql->setStatement("UPDATE " . CurrenciesEntity::getTable() . " SET rate_to = rate_from WHERE id={$newCurrency->id}"); $this->db->query($sql); $sql = $this->queryFactory->newSqlQuery(); - $sql->setStatement("UPDATE ".CurrenciesEntity::getTable()." SET rate_to = 1, rate_from = 1 WHERE (rate_to=0 OR rate_from=0) AND id={$newCurrency->id}"); + $sql->setStatement("UPDATE " . CurrenciesEntity::getTable() . " SET rate_to = 1, rate_from = 1 WHERE (rate_to=0 OR rate_from=0) AND id={$newCurrency->id}"); $this->db->query($sql); } @@ -194,7 +192,7 @@ public function sortCurrencies($currencies) asort($currenciesIds); $i = 0; foreach ($currenciesIds as $currencyId) { - $this->currenciesEntity->update($currenciesIds[$i], ['position'=>$currencyId]); + $this->currenciesEntity->update($currenciesIds[$i], ['position' => $currencyId]); $i++; } @@ -203,25 +201,25 @@ public function sortCurrencies($currencies) public function disable($id) { - $this->currenciesEntity->update($id, ['enabled'=>0]); + $this->currenciesEntity->update($id, ['enabled' => 0]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } public function enable($id) { - $this->currenciesEntity->update($id, ['enabled'=>1]); + $this->currenciesEntity->update($id, ['enabled' => 1]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } public function showCents($id) { - $this->currenciesEntity->update($id, ['cents'=>2]); + $this->currenciesEntity->update($id, ['cents' => 2]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } public function hideCents($id) { - $this->currenciesEntity->update($id, ['cents'=>0]); + $this->currenciesEntity->update($id, ['cents' => 0]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } @@ -236,4 +234,4 @@ public function getMainCurrency() $currency = $this->currenciesEntity->getMainCurrency(); return ExtenderFacade::execute(__METHOD__, $currency, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendFeedbacksHelper.php b/backend/Helpers/BackendFeedbacksHelper.php index 5dd9825a0..6d1133c1e 100644 --- a/backend/Helpers/BackendFeedbacksHelper.php +++ b/backend/Helpers/BackendFeedbacksHelper.php @@ -1,9 +1,7 @@ feedbacksEntity = $entityFactory->get(FeedbacksEntity::class); $this->request = $request; $this->settings = $settings; @@ -73,7 +71,7 @@ public function buildFilter() $feedbacksCount = $this->feedbacksEntity->count($filter); // Показать все страницы сразу - if($this->request->get('page') == 'all') { + if ($this->request->get('page') == 'all') { $filter['limit'] = $feedbacksCount; } @@ -155,4 +153,4 @@ public function addAnswer($feedback) $insertId = $this->feedbacksEntity->add($feedback); return ExtenderFacade::execute(__METHOD__, $insertId, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendManagersHelper.php b/backend/Helpers/BackendManagersHelper.php index 5589a814b..fbdd09d41 100644 --- a/backend/Helpers/BackendManagersHelper.php +++ b/backend/Helpers/BackendManagersHelper.php @@ -1,9 +1,7 @@ managersEntity->find($filter); return ExtenderFacade::execute(__METHOD__, $managers, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendModulesHelper.php b/backend/Helpers/BackendModulesHelper.php index e3a31b274..cd9639639 100644 --- a/backend/Helpers/BackendModulesHelper.php +++ b/backend/Helpers/BackendModulesHelper.php @@ -124,6 +124,16 @@ public function downloadModule(string $downloadUrl) $zip = new \ZipArchive(); if ($zip->open($tempFileToSave) === true) { + for ($i = 0; $i < $zip->numFiles; $i++) { + $entryName = $zip->getNameIndex($i); + if (!is_string($entryName) || !$this->isSafeZipEntryName($entryName)) { + $zip->close(); + unlink($tempFileToSave); + $this->rRmdir($tempDir); + return false; + } + } + $zip->extractTo($tempDir); $zip->close(); unlink($tempFileToSave); @@ -285,6 +295,23 @@ private function rRmdir($dir, $level = 0) } } + private function isSafeZipEntryName(string $name): bool + { + if ($name === '') { + return false; + } + + if (str_starts_with($name, '/') || str_starts_with($name, '\\')) { + return false; + } + + if (str_contains($name, '..')) { + return false; + } + + return true; + } + private function tempDir($subDir = null) { if ($subDir === null) { diff --git a/backend/Helpers/BackendNotifyHelper.php b/backend/Helpers/BackendNotifyHelper.php index 62c23c5fc..9d0c7c0be 100644 --- a/backend/Helpers/BackendNotifyHelper.php +++ b/backend/Helpers/BackendNotifyHelper.php @@ -1,9 +1,7 @@ &foo=bar * Этот метод нужно расширить ChainExtender-ом и сделать проверку если $debugEmail == то вернуть * html вашего шаблона - * + * * @param string $debugEmail строка, переданная в параметре debug * @return false|mixed|void|null */ @@ -43,4 +41,4 @@ public function debugTemplate($debugEmail) { return ExtenderFacade::execute(__METHOD__, null, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendProductsHelper.php b/backend/Helpers/BackendProductsHelper.php index 696546f1d..030032018 100644 --- a/backend/Helpers/BackendProductsHelper.php +++ b/backend/Helpers/BackendProductsHelper.php @@ -1,9 +1,7 @@ db = $db; $this->config = $config; @@ -78,7 +76,7 @@ public function __construct( $this->categoriesEntity = $entityFactory->get(CategoriesEntity::class); $this->specialImagesEntity = $entityFactory->get(SpecialImagesEntity::class); $this->routerCacheEntity = $entityFactory->get(RouterCacheEntity::class); - $this->featuresValuesEntity= $entityFactory->get(FeaturesValuesEntity::class); + $this->featuresValuesEntity = $entityFactory->get(FeaturesValuesEntity::class); $this->featuresEntity = $entityFactory->get(FeaturesEntity::class); } @@ -91,7 +89,7 @@ public function getProduct($id) $product = new \stdClass(); $product->visible = 1; } - + return ExtenderFacade::execute(__METHOD__, $product, func_get_args()); } @@ -103,7 +101,7 @@ public function prepareAdd($product) public function add($product) { if (!empty($product->brand_id)) { - $this->brandsEntity->update($product->brand_id, ['last_modify'=>'now()']); + $this->brandsEntity->update($product->brand_id, ['last_modify' => 'now()']); } $insertId = $this->productsEntity->add($product); @@ -120,8 +118,8 @@ public function update($product) $oldBrandId = $this->productsEntity->cols(['brand_id'])->get((int)$product->id)->brand_id; if (!empty($product->brand_id) && $oldBrandId != $product->brand_id) { - $this->brandsEntity->update($oldBrandId, ['last_modify'=>'now()']); - $this->brandsEntity->update($product->brand_id, ['last_modify'=>'now()']); + $this->brandsEntity->update($oldBrandId, ['last_modify' => 'now()']); + $this->brandsEntity->update($product->brand_id, ['last_modify' => 'now()']); } $this->productsEntity->update($product->id, $product); @@ -157,7 +155,7 @@ public function updateProductsCategories($product, $productCategories) $this->db->query($delete); if (is_array($productCategories)) { $i = 0; - foreach($productCategories as $category) { + foreach ($productCategories as $category) { $this->categoriesEntity->addProductCategory($product->id, $category->id, $i); $i++; } @@ -165,7 +163,7 @@ public function updateProductsCategories($product, $productCategories) } $mainCategory = reset($productCategories); - $this->productsEntity->update($product->id, ['main_category_id'=>$mainCategory->id]); + $this->productsEntity->update($product->id, ['main_category_id' => $mainCategory->id]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } @@ -180,7 +178,7 @@ public function updateRelatedProducts($product, $relatedProducts) $this->productsEntity->deleteRelatedProduct($product->id); if (is_array($relatedProducts)) { $pos = 0; - foreach($relatedProducts as $i=>$relatedProduct) { + foreach ($relatedProducts as $i => $relatedProduct) { $this->productsEntity->addRelatedProduct($product->id, $relatedProduct->related_id, $pos++); } } @@ -191,7 +189,7 @@ public function updateRelatedProducts($product, $relatedProducts) public function updateImages($product, $images, $droppedImages) { // Удаление изображений - $currentImages = $this->imagesEntity->find(['product_id'=>$product->id]); + $currentImages = $this->imagesEntity->find(['product_id' => $product->id]); foreach ($currentImages as $image) { if (!in_array($image->id, $images)) { $this->imagesEntity->delete($image->id); @@ -200,9 +198,9 @@ public function updateImages($product, $images, $droppedImages) // Порядок изображений if ($images) { - $i=0; + $i = 0; foreach ($images as $id) { - $this->imagesEntity->update($id, ['position'=>$i]); + $this->imagesEntity->update($id, ['position' => $i]); $i++; } } @@ -238,7 +236,7 @@ public function updateSpecialImages($product, $specialImages, $specDroppedImages } } } - + // Загрузка изображений из интернета и drag-n-drop файлов if (!empty($specDroppedImages)) { foreach (array_keys($specDroppedImages['name']) as $key) { @@ -258,9 +256,9 @@ public function updateSpecialImages($product, $specialImages, $specDroppedImages // Порядок промо изображений if (!empty($specialImages)) { - $i=0; + $i = 0; foreach ($specialImages as $id) { - $this->specialImagesEntity->update($id, ['position'=>$i]); + $this->specialImagesEntity->update($id, ['position' => $i]); $i++; } } @@ -335,7 +333,7 @@ public function getProductsSortName() $sort = $this->request->get('sort', 'string', null); return ExtenderFacade::execute(__METHOD__, $sort, func_get_args()); } - + public function buildFilter() { // Пагинация @@ -355,7 +353,7 @@ public function buildFilter() // Категории $categoryId = $this->request->get('category_id', 'integer'); $category = $this->categoriesEntity->get($categoryId); - if(!empty($categoryId) && !empty($category)) { + if (!empty($categoryId) && !empty($category)) { $filter['category_id'] = $category->children; } elseif ($categoryId == -1) { $filter['without_category'] = 1; @@ -363,7 +361,7 @@ public function buildFilter() // Бренды $brandId = $this->request->get('brand_id', 'integer'); - if($brandId && $brand = $this->brandsEntity->get($brandId)) { + if ($brandId && $brand = $this->brandsEntity->get($brandId)) { $filter['brand_id'] = $brand->id; } elseif ($brandId == -1) { $filter['brand_id'] = array(0); @@ -374,20 +372,20 @@ public function buildFilter() } // Фильтр по товарам - if($f = $this->request->get('filter', 'string')) { - if($f == 'featured') { + if ($f = $this->request->get('filter', 'string')) { + if ($f == 'featured') { $filter['featured'] = 1; - } elseif($f == 'discounted') { + } elseif ($f == 'discounted') { $filter['discounted'] = 1; - } elseif($f == 'visible') { + } elseif ($f == 'visible') { $filter['visible'] = 1; - } elseif($f == 'hidden') { + } elseif ($f == 'hidden') { $filter['visible'] = 0; - } elseif($f == 'outofstock') { + } elseif ($f == 'outofstock') { $filter['not_in_stock'] = 1; - } elseif($f == 'instock') { + } elseif ($f == 'instock') { $filter['in_stock'] = 1; - } elseif($f == 'without_images') { + } elseif ($f == 'without_images') { $filter['has_no_images'] = 1; } @@ -414,8 +412,7 @@ public function buildFilter() $productIds = []; $pid = []; if (!empty($values)) { - foreach ($values as $i=>$value) { - + foreach ($values as $i => $value) { $select = $this->queryFactory->newSelect(); $select->from('__products_features_values') ->cols(['product_id']) @@ -447,15 +444,15 @@ public function moveToPage($ids, $filter) $filter['page'] = $targetPage; // До какого товара перемещать - $limit = $filter['limit']*($targetPage-1); - if($targetPage > $this->request->get('page', 'integer')) { - $limit += count($ids)-1; + $limit = $filter['limit'] * ($targetPage - 1); + if ($targetPage > $this->request->get('page', 'integer')) { + $limit += count($ids) - 1; } else { $ids = array_reverse($ids, true); } $tempFilter = $filter; - $tempFilter['page'] = $limit+1; + $tempFilter['page'] = $limit + 1; $tempFilter['limit'] = 1; $tmp = $this->productsEntity->find($tempFilter); $targetProduct = array_pop($tmp); @@ -463,7 +460,6 @@ public function moveToPage($ids, $filter) // Если вылезли за последний товар - берем позицию последнего товара в качестве цели перемещения if ($targetPage > $this->request->get('page', 'integer') && !$targetPosition) { - $select = $this->queryFactory->newSelect(); $select->cols(['distinct p.position AS target']) ->from('__products AS p') @@ -510,7 +506,7 @@ public function actionAddSecondCategories($ids) { /* Добавить категорию */ $categoryId = $this->request->post('target_second_category'); - + // Подсчитываем кол-во категорий каждого товара, чтобы знать какой position ставить новым категориям $productsCategoriesNum = []; foreach ($this->categoriesEntity->getProductCategories($ids) as $pc) { @@ -525,7 +521,7 @@ public function actionAddSecondCategories($ids) } $this->categoriesEntity->addProductCategory($id, $categoryId, $productsCategoriesNum[$id]++); } - + ExtenderFacade::execute(__METHOD__, null, func_get_args()); } @@ -537,7 +533,7 @@ public function actionMoveToCategory($ids) $category = $this->categoriesEntity->get($categoryId); $filter['category_id'] = $category->children; - foreach($ids as $id) { + foreach ($ids as $id) { $delete = $this->queryFactory->newDelete(); $delete->from('__products_categories') ->where('category_id=:category_id') @@ -568,7 +564,6 @@ public function actionMoveToCategory($ids) ->set('product_id', $id); $this->db->query($insert); } - } if (!empty($ids)) { @@ -582,7 +577,7 @@ public function actionMoveToCategory($ids) public function duplicateProducts($ids) { - foreach($ids as $id) { + foreach ($ids as $id) { $this->productsEntity->duplicate((int)$id); } @@ -609,7 +604,7 @@ public function sortPositions($positions) public function updatePositions($ids, $positions) { - foreach($positions as $i=>$position) { + foreach ($positions as $i => $position) { $this->productsEntity->update($ids[$i], array('position' => (int) $position)); } @@ -618,9 +613,9 @@ public function updatePositions($ids, $positions) public function findProductsForProductsAdmin($filter, $sortName) { - + $this->productsEntity->order($sortName); - + $products = $this->productsEntity->mappedBy('id')->find($filter); $imagesIds = []; @@ -631,7 +626,7 @@ public function findProductsForProductsAdmin($filter, $sortName) if (!empty($products)) { // Товары $productsIds = array_keys($products); - foreach($products as $product) { + foreach ($products as $product) { $product->variants = array(); $product->properties = array(); } @@ -655,13 +650,13 @@ public function findProductsForProductsAdmin($filter, $sortName) public function disable($ids) { - $this->productsEntity->update($ids, ['visible'=>0]); + $this->productsEntity->update($ids, ['visible' => 0]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } public function enable($ids) { - $this->productsEntity->update($ids, ['visible'=>1]); + $this->productsEntity->update($ids, ['visible' => 1]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } @@ -673,7 +668,7 @@ public function setFeatured($ids) public function unsetFeatured($ids) { - $this->productsEntity->update($ids, ['featured'=>0]); + $this->productsEntity->update($ids, ['featured' => 0]); ExtenderFacade::execute(__METHOD__, null, func_get_args()); } @@ -682,5 +677,4 @@ public function delete($ids) ExtenderFacade::execute(__METHOD__, null, func_get_args()); $this->productsEntity->delete($ids); } - -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendSpecialImagesHelper.php b/backend/Helpers/BackendSpecialImagesHelper.php index dc8227031..6368272aa 100644 --- a/backend/Helpers/BackendSpecialImagesHelper.php +++ b/backend/Helpers/BackendSpecialImagesHelper.php @@ -1,9 +1,7 @@ specialImagesEntity->find(); return ExtenderFacade::execute(__METHOD__, $specialImages, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendUserGroupsHelper.php b/backend/Helpers/BackendUserGroupsHelper.php index 13289f86f..b0e2de551 100644 --- a/backend/Helpers/BackendUserGroupsHelper.php +++ b/backend/Helpers/BackendUserGroupsHelper.php @@ -1,9 +1,7 @@ entityFactory = $entityFactory; $this->userGroupsEntity = $entityFactory->get(UserGroupsEntity::class); @@ -41,9 +39,9 @@ public function findGroups($filter) $usersEntity = $this->entityFactory->get(UsersEntity::class); $groups = $this->userGroupsEntity->mappedBy('id')->find($filter); foreach ($groups as $group) { - $group->cnt_users = $usersEntity->count(["group_id"=>$group->id]); + $group->cnt_users = $usersEntity->count(["group_id" => $group->id]); } - + return ExtenderFacade::execute(__METHOD__, $groups, func_get_args()); } @@ -57,7 +55,7 @@ public function add($group) $insertId = $this->userGroupsEntity->add($group); return ExtenderFacade::execute(__METHOD__, $insertId, func_get_args()); } - + public function prepareUpdate($group) { return ExtenderFacade::execute(__METHOD__, $group, func_get_args()); @@ -86,5 +84,4 @@ public function buildFilter() $filter = []; return ExtenderFacade::execute(__METHOD__, $filter, func_get_args()); } - -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendUsersHelper.php b/backend/Helpers/BackendUsersHelper.php index 71beac3e6..2ccf9ee88 100644 --- a/backend/Helpers/BackendUsersHelper.php +++ b/backend/Helpers/BackendUsersHelper.php @@ -1,9 +1,7 @@ entityFactory = $entityFactory; $this->usersEntity = $entityFactory->get(UsersEntity::class); $this->request = $request; @@ -40,16 +38,16 @@ public function __construct( public function findUsers($filter, $usersSort = null) { $ordersEntity = $this->entityFactory->get(OrdersEntity::class); - + if (!empty($usersSort)) { $this->usersEntity->order($usersSort); } $users = $this->usersEntity->mappedBy('id')->find($filter); foreach ($users as $user) { - $user->orders = $ordersEntity->find(['user_id'=>$user->id]); + $user->orders = $ordersEntity->find(['user_id' => $user->id]); } - + return ExtenderFacade::execute(__METHOD__, $users, func_get_args()); } @@ -60,7 +58,7 @@ public function getAllGroups() $groups = $userGroupsEntity->mappedBy('id')->find(); return ExtenderFacade::execute(__METHOD__, $groups, func_get_args()); } - + public function getUsersSort() { $usersSort = null; @@ -91,10 +89,10 @@ public function getUser($id) { $ordersEntity = $this->entityFactory->get(OrdersEntity::class); $user = $this->usersEntity->get($id); - $user->orders = $ordersEntity->find(['user_id'=>$user->id]); + $user->orders = $ordersEntity->find(['user_id' => $user->id]); return ExtenderFacade::execute(__METHOD__, $user, func_get_args()); } - + public function delete($ids) { @@ -131,7 +129,7 @@ public function buildFilter() if ($this->request->get('page') == 'all') { $filter['limit'] = $this->usersEntity->count($filter); } - + return ExtenderFacade::execute(__METHOD__, $filter, func_get_args()); } @@ -140,5 +138,4 @@ public function countUsers($filter) $count = $this->usersEntity->count($filter); return ExtenderFacade::execute(__METHOD__, $count, func_get_args()); } - -} \ No newline at end of file +} diff --git a/backend/Helpers/BackendVariantsHelper.php b/backend/Helpers/BackendVariantsHelper.php index e12e7dad4..979690409 100644 --- a/backend/Helpers/BackendVariantsHelper.php +++ b/backend/Helpers/BackendVariantsHelper.php @@ -1,9 +1,7 @@ &$variant) { + foreach ($productVariants as $index => &$variant) { if (!empty($variant->id)) { $this->variantsEntity->update($variant->id, $variant); } else { @@ -53,7 +51,7 @@ public function updateVariants($product, $productVariants) } // Удалить непереданные варианты - $current_variants = $this->variantsEntity->find(['product_id'=>$product->id]); + $current_variants = $this->variantsEntity->find(['product_id' => $product->id]); foreach ($current_variants as $current_variant) { if (!in_array($current_variant->id, $variantsIds)) { $this->variantsEntity->delete($current_variant->id); @@ -64,7 +62,7 @@ public function updateVariants($product, $productVariants) asort($variantsIds); $i = 0; foreach ($variantsIds as $variant_id) { - $this->variantsEntity->update($variantsIds[$i], ['position'=>$variant_id]); + $this->variantsEntity->update($variantsIds[$i], ['position' => $variant_id]); $i++; } @@ -77,7 +75,7 @@ public function findProductVariants($product) if (!empty($product->id)) { $productVariants = $this->variantsEntity->find(['product_id' => $product->id]); } - + if (empty($productVariants)) { $productVariants = [1]; } @@ -87,14 +85,14 @@ public function findProductVariants($product) public function updateStocksAndPrices($stocks, $prices) { - foreach($prices as $id=>$price) { + foreach ($prices as $id => $price) { $stock = $stocks[$id]; - if($stock == '∞' || $stock == '') { + if ($stock == '∞' || $stock == '') { $stock = null; } - $this->variantsEntity->update($id, ['price'=>str_replace(',', '.', $price), 'stock'=>$stock]); + $this->variantsEntity->update($id, ['price' => str_replace(',', '.', $price), 'stock' => $stock]); } ExtenderFacade::execute(__METHOD__, null, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Requests/BackendAuthorsRequest.php b/backend/Requests/BackendAuthorsRequest.php index 9b193def0..dab7cff8c 100644 --- a/backend/Requests/BackendAuthorsRequest.php +++ b/backend/Requests/BackendAuthorsRequest.php @@ -1,9 +1,7 @@ socials = json_encode($socials); return ExtenderFacade::execute(__METHOD__, $author, func_get_args()); } @@ -81,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/backend/Requests/BackendBrandsRequest.php b/backend/Requests/BackendBrandsRequest.php index 3d9ca1812..9eb07dddb 100644 --- a/backend/Requests/BackendBrandsRequest.php +++ b/backend/Requests/BackendBrandsRequest.php @@ -1,9 +1,7 @@ request->post('positions'); return ExtenderFacade::execute(__METHOD__, $positions, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Requests/BackendCallbacksRequest.php b/backend/Requests/BackendCallbacksRequest.php index 3a2a27862..263b235bb 100644 --- a/backend/Requests/BackendCallbacksRequest.php +++ b/backend/Requests/BackendCallbacksRequest.php @@ -1,9 +1,7 @@ request->post('action'); return ExtenderFacade::execute(__METHOD__, $action, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Requests/BackendCommentsRequest.php b/backend/Requests/BackendCommentsRequest.php index c7d2b0884..c013fa1ec 100644 --- a/backend/Requests/BackendCommentsRequest.php +++ b/backend/Requests/BackendCommentsRequest.php @@ -1,9 +1,7 @@ request->post('check'); return ExtenderFacade::execute(__METHOD__, $check, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Requests/BackendCouponsRequest.php b/backend/Requests/BackendCouponsRequest.php index 039b8c6f4..a187d9a9a 100644 --- a/backend/Requests/BackendCouponsRequest.php +++ b/backend/Requests/BackendCouponsRequest.php @@ -1,9 +1,7 @@ request = $request; } - + public function postCoupon() { $coupon = new \stdClass(); @@ -56,4 +54,4 @@ public function postAction() $check = $this->request->post('action'); return ExtenderFacade::execute(__METHOD__, $check, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Requests/BackendDeliveriesRequest.php b/backend/Requests/BackendDeliveriesRequest.php index 284dbae4e..423f2ac2c 100644 --- a/backend/Requests/BackendDeliveriesRequest.php +++ b/backend/Requests/BackendDeliveriesRequest.php @@ -1,9 +1,7 @@ price = $this->request->post('price'); $delivery->free_from = $this->request->post('free_from'); $delivery->paid = $this->request->post('delivery_type') === 'paid' ? 1 : 0; - $delivery->separate_payment = $this->request->post('separate_payment','boolean'); + $delivery->separate_payment = $this->request->post('separate_payment', 'boolean'); $delivery->module_id = $this->request->post('module_id', 'integer'); $delivery->hide_front_delivery_price = $this->request->post('hide_front_delivery_price', 'integer'); @@ -77,4 +75,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/backend/Requests/BackendFeaturesValuesRequest.php b/backend/Requests/BackendFeaturesValuesRequest.php index daf52588d..38280edd0 100644 --- a/backend/Requests/BackendFeaturesValuesRequest.php +++ b/backend/Requests/BackendFeaturesValuesRequest.php @@ -1,9 +1,7 @@ request = $request; $this->translit = $translit; } @@ -51,4 +49,4 @@ public function postToIndexAllValues() $toIndexAllValues = isset($_POST['to_index_all_values']) ? $_POST['to_index_all_values'] : null; return ExtenderFacade::execute(__METHOD__, $toIndexAllValues, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Requests/BackendFeedbacksRequest.php b/backend/Requests/BackendFeedbacksRequest.php index c1ab71cd4..2b44499e4 100644 --- a/backend/Requests/BackendFeedbacksRequest.php +++ b/backend/Requests/BackendFeedbacksRequest.php @@ -1,4 +1,5 @@ request->post('feedback_answer'); return ExtenderFacade::execute(__METHOD__, $feedbackAnswer, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Requests/BackendOrderSettingsRequest.php b/backend/Requests/BackendOrderSettingsRequest.php index c4bd995b2..058591999 100644 --- a/backend/Requests/BackendOrderSettingsRequest.php +++ b/backend/Requests/BackendOrderSettingsRequest.php @@ -1,9 +1,7 @@ $va) { - foreach ($va as $i=>$v) { + foreach ($postFields as $n => $va) { + foreach ($va as $i => $v) { if (empty($statuses[$i])) { $statuses[$i] = new \stdClass(); } @@ -59,7 +57,7 @@ public function postStatuses() $statuses[$i]->$n = $v; } } - + return ExtenderFacade::execute(__METHOD__, $statuses, func_get_args()); } @@ -88,10 +86,10 @@ public function postLabels() if (empty($postFields)) { return ExtenderFacade::execute(__METHOD__, false, func_get_args()); } - + $labels = []; - foreach ($postFields as $n=>$va) { - foreach ($va as $i=>$v) { + foreach ($postFields as $n => $va) { + foreach ($va as $i => $v) { if (empty($labels[$i])) { $labels[$i] = new \stdClass(); } @@ -101,7 +99,7 @@ public function postLabels() $labels[$i]->$n = $v; } } - + return ExtenderFacade::execute(__METHOD__, $labels, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/Requests/BackendPaymentsRequest.php b/backend/Requests/BackendPaymentsRequest.php index 2c19951df..03f655fd3 100644 --- a/backend/Requests/BackendPaymentsRequest.php +++ b/backend/Requests/BackendPaymentsRequest.php @@ -1,9 +1,7 @@ request->post('positions'); return ExtenderFacade::execute(__METHOD__, $positions, func_get_args()); } -} \ No newline at end of file +} diff --git a/backend/ajax/configure.php b/backend/ajax/configure.php index 9bb0aa8a0..5efc3ecc2 100644 --- a/backend/ajax/configure.php +++ b/backend/ajax/configure.php @@ -5,7 +5,9 @@ use Okay\Core\Settings; use Okay\Core\Config; use Okay\Core\Managers; +use Okay\Core\Security\AdminCsrfToken; use Okay\Core\Security\AdminSession; +use Okay\Core\Security\TrustedRequestContext; use Okay\Entities\ManagersEntity; use Okay\Core\EntityFactory; use Okay\Core\Modules\Modules; @@ -17,14 +19,23 @@ chdir(dirname(dirname(__DIR__))); require_once('vendor/autoload.php'); -session_name(AdminSession::SESSION_NAME); -AdminSession::configureCookieParams($_SERVER); -session_start(); - +/** @var \Okay\Core\OkayContainer\OkayContainer $DI */ $DI = include 'Okay/Core/config/container.php'; /** @var Config $config */ $config = $DI->get(Config::class); +$trustedProxyIps = $config->get('trusted_proxy_ips'); +$trustedClientIpHeader = $config->get('trusted_proxy_client_ip_header'); +Request::setTrustedRequestContext(TrustedRequestContext::fromServer( + $_SERVER, + is_string($trustedProxyIps) ? $trustedProxyIps : null, + is_string($trustedClientIpHeader) ? $trustedClientIpHeader : 'X-Forwarded-For' +)); + +session_name(AdminSession::SESSION_NAME); +AdminSession::configureCookieParams($_SERVER); +session_start(); +$_SESSION['id'] = AdminCsrfToken::get(); $smartyPlugins = include_once 'Okay/Core/SmartyPlugins/SmartyPlugins.php'; diff --git a/backend/ajax/get_features.php b/backend/ajax/get_features.php index 8a9f258fe..ac9042775 100644 --- a/backend/ajax/get_features.php +++ b/backend/ajax/get_features.php @@ -20,7 +20,7 @@ $featuresValuesEntity = $entityFactory->get(FeaturesValuesEntity::class); if (!empty($categoryId)) { - $features = $featuresEntity->find(['category_id'=>$categoryId]); + $features = $featuresEntity->find(['category_id' => $categoryId]); } else { $features = $featuresEntity->find(); } @@ -28,7 +28,7 @@ /*Выборка значений свойств*/ $featuresValues = []; if (!empty($productId)) { - foreach ($featuresValuesEntity->find(['product_id'=>$productId]) as $fv) { + foreach ($featuresValuesEntity->find(['product_id' => $productId]) as $fv) { $featuresValues[$fv->feature_id][] = $fv; } } @@ -38,7 +38,7 @@ $f->values = $featuresValues[$f->id]; } else { $f->values = [ - ['value'=>'', 'id'=>''] + ['value' => '', 'id' => ''] ]; } } diff --git a/backend/ajax/lesson_done.php b/backend/ajax/lesson_done.php index 28d4236f7..259b14e50 100644 --- a/backend/ajax/lesson_done.php +++ b/backend/ajax/lesson_done.php @@ -4,15 +4,58 @@ use Okay\Entities\LessonsEntity; +/** @var \Okay\Core\EntityFactory $entityFactory */ +/** @var \Okay\Core\BackendTranslations $backendTranslations */ +/** @var object $manager */ +/** @var \Okay\Core\Managers $managers */ +/** @var \Okay\Core\Request $request */ +/** @var \Okay\Core\Response $response */ + if (!$managers->access('learning', $manager)) { - exit(); + $response->setStatusCode(403); + $response->setContent(json_encode([ + 'success' => false, + 'error' => ['code' => 'lesson_forbidden', 'message' => $backendTranslations->getTranslation('lesson_forbidden')], + ]), RESPONSE_JSON); + $response->sendContent(); + exit; +} + +if (!$request->method('post')) { + $response->setStatusCode(405); + $response->setContent(json_encode([ + 'success' => false, + 'error' => ['code' => 'lesson_method_not_allowed', 'message' => $backendTranslations->getTranslation('lesson_method_not_allowed')], + ]), RESPONSE_JSON); + $response->sendContent(); + exit; } -if (($_SERVER['HTTP_X_OKAY_SESSION_ID'] ?? null) !== session_id()) { - exit(); +if (!$request->checkSession()) { + $response->setStatusCode(403); + $response->setContent(json_encode([ + 'success' => false, + 'error' => ['code' => 'lesson_csrf_invalid', 'message' => $backendTranslations->getTranslation('lesson_csrf_invalid')], + ]), RESPONSE_JSON); + $response->sendContent(); + exit; +} + +$lessonId = $request->post('lesson', 'integer'); +if ($lessonId < 1) { + $response->setStatusCode(422); + $response->setContent(json_encode([ + 'success' => false, + 'error' => ['code' => 'lesson_invalid', 'message' => $backendTranslations->getTranslation('lesson_invalid')], + ]), RESPONSE_JSON); + $response->sendContent(); + exit; } $lessonsEntity = $entityFactory->get(LessonsEntity::class); -$lessonsEntity->update($request->get('lesson'), [ +$lessonsEntity->update($lessonId, [ 'done' => 1 ]); + +$response->setContent(json_encode(['success' => true]), RESPONSE_JSON); +$response->sendContent(); diff --git a/backend/ajax/save_script.php b/backend/ajax/save_script.php index 812129bf0..ddc9e5ba6 100644 --- a/backend/ajax/save_script.php +++ b/backend/ajax/save_script.php @@ -2,30 +2,77 @@ require_once 'configure.php'; +use Okay\Core\Security\ThemeEditorResponse; + +/** @var \Okay\Core\BackendTranslations $backendTranslations */ +/** @var \Okay\Core\Config $config */ +/** @var object $manager */ +/** @var \Okay\Core\Managers $managers */ +/** @var \Okay\Core\Request $request */ +/** @var \Okay\Core\Response $response */ + if (!$managers->access('scripts', $manager)) { - exit(); + ThemeEditorResponse::send($response, false, 'editor_unauthorized', $backendTranslations->getTranslation('theme_editor_unauthorized')); + exit; +} + +if (!$config->get('allow_theme_file_editor')) { + ThemeEditorResponse::send($response, false, 'editor_disabled', $backendTranslations->getTranslation('theme_editor_disabled')); + exit; +} + +if (!$request->method('post')) { + ThemeEditorResponse::send($response, false, 'method_not_allowed', $backendTranslations->getTranslation('theme_editor_method_not_allowed')); + exit; } -// Проверка сессии для защиты от xss if (!$request->checkSession()) { - trigger_error('Session expired', E_USER_WARNING); - exit(); + ThemeEditorResponse::send($response, false, 'csrf_invalid', $backendTranslations->getTranslation('theme_editor_csrf_invalid')); + exit; } + $content = $request->post('content'); $script = $request->post('script'); $theme = $request->post('theme', 'string'); +if (!is_string($content) || !is_string($script) || $script === '' || basename($script) !== $script) { + ThemeEditorResponse::send($response, false, 'invalid_path', $backendTranslations->getTranslation('theme_editor_invalid_path')); + exit; +} + +$rootDir = $config->get('root_dir'); +if (!is_string($rootDir)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; +} + /*Сохранение скриптов из админки*/ -$file = $config->root_dir.'design/'.$theme.'/js/'.$script; +$file = $rootDir . 'design/' . $theme . '/js/' . $script; + +if (pathinfo($script, PATHINFO_EXTENSION) != 'js') { + ThemeEditorResponse::send($response, false, 'invalid_extension', $backendTranslations->getTranslation('theme_editor_invalid_extension')); + exit; +} + +if ($file != realpath($file)) { + ThemeEditorResponse::send($response, false, 'invalid_path', $backendTranslations->getTranslation('theme_editor_invalid_path')); + exit; +} + +if (!is_file($file) || !is_writable($file)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; +} -if (pathinfo($script, PATHINFO_EXTENSION) != 'js' || $file != realpath($file)) { - exit(); +if (is_file($rootDir . 'design/' . $theme . '/locked')) { + ThemeEditorResponse::send($response, false, 'theme_locked', $backendTranslations->getTranslation('theme_editor_locked')); + exit; } -if(is_file($file) && is_writable($file) && !is_file($config->root_dir.'design/'.$theme.'/locked')) { - file_put_contents($file, $content); +$writtenBytes = file_put_contents($file, $content, LOCK_EX); +if ($writtenBytes === false || $writtenBytes !== strlen($content)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; } -$result = true; -$response->setContent(json_encode($result), RESPONSE_JSON); -$response->sendContent(); +ThemeEditorResponse::send($response, true, 'saved', $backendTranslations->getTranslation('theme_editor_saved')); diff --git a/backend/ajax/save_style.php b/backend/ajax/save_style.php index 49d814fed..0e7b8bbcc 100644 --- a/backend/ajax/save_style.php +++ b/backend/ajax/save_style.php @@ -2,30 +2,77 @@ require_once 'configure.php'; +use Okay\Core\Security\ThemeEditorResponse; + +/** @var \Okay\Core\BackendTranslations $backendTranslations */ +/** @var \Okay\Core\Config $config */ +/** @var object $manager */ +/** @var \Okay\Core\Managers $managers */ +/** @var \Okay\Core\Request $request */ +/** @var \Okay\Core\Response $response */ + if (!$managers->access('style_templates', $manager)) { - exit(); + ThemeEditorResponse::send($response, false, 'editor_unauthorized', $backendTranslations->getTranslation('theme_editor_unauthorized')); + exit; +} + +if (!$config->get('allow_theme_file_editor')) { + ThemeEditorResponse::send($response, false, 'editor_disabled', $backendTranslations->getTranslation('theme_editor_disabled')); + exit; +} + +if (!$request->method('post')) { + ThemeEditorResponse::send($response, false, 'method_not_allowed', $backendTranslations->getTranslation('theme_editor_method_not_allowed')); + exit; } -// Проверка сессии для защиты от xss if (!$request->checkSession()) { - trigger_error('Session expired', E_USER_WARNING); - exit(); + ThemeEditorResponse::send($response, false, 'csrf_invalid', $backendTranslations->getTranslation('theme_editor_csrf_invalid')); + exit; } + $content = $request->post('content'); $style = $request->post('style'); $theme = $request->post('theme', 'string'); +if (!is_string($content) || !is_string($style) || $style === '' || basename($style) !== $style) { + ThemeEditorResponse::send($response, false, 'invalid_path', $backendTranslations->getTranslation('theme_editor_invalid_path')); + exit; +} + +$rootDir = $config->get('root_dir'); +if (!is_string($rootDir)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; +} + /*Сохранение стилей из админки*/ -$file = $config->root_dir.'design/'.$theme.'/css/'.$style; +$file = $rootDir . 'design/' . $theme . '/css/' . $style; + +if (pathinfo($style, PATHINFO_EXTENSION) != 'css') { + ThemeEditorResponse::send($response, false, 'invalid_extension', $backendTranslations->getTranslation('theme_editor_invalid_extension')); + exit; +} + +if ($file != realpath($file)) { + ThemeEditorResponse::send($response, false, 'invalid_path', $backendTranslations->getTranslation('theme_editor_invalid_path')); + exit; +} + +if (!is_file($file) || !is_writable($file)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; +} -if (pathinfo($style, PATHINFO_EXTENSION) != 'css' || $file != realpath($file)) { - exit(); +if (is_file($rootDir . 'design/' . $theme . '/locked')) { + ThemeEditorResponse::send($response, false, 'theme_locked', $backendTranslations->getTranslation('theme_editor_locked')); + exit; } -if (is_file($file) && is_writable($file) && !is_file($config->root_dir.'design/'.$theme.'/locked')) { - file_put_contents($file, $content); +$writtenBytes = file_put_contents($file, $content, LOCK_EX); +if ($writtenBytes === false || $writtenBytes !== strlen($content)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; } -$result = true; -$response->setContent(json_encode($result), RESPONSE_JSON); -$response->sendContent(); +ThemeEditorResponse::send($response, true, 'saved', $backendTranslations->getTranslation('theme_editor_saved')); diff --git a/backend/ajax/save_template.php b/backend/ajax/save_template.php index f346cf54f..f5cc8cd29 100644 --- a/backend/ajax/save_template.php +++ b/backend/ajax/save_template.php @@ -2,34 +2,81 @@ require_once 'configure.php'; +use Okay\Core\Security\ThemeEditorResponse; + +/** @var \Okay\Core\BackendTranslations $backendTranslations */ +/** @var \Okay\Core\Config $config */ +/** @var object $manager */ +/** @var \Okay\Core\Managers $managers */ +/** @var \Okay\Core\Request $request */ +/** @var \Okay\Core\Response $response */ + if (!$managers->access('file_templates', $manager)) { - exit(); + ThemeEditorResponse::send($response, false, 'editor_unauthorized', $backendTranslations->getTranslation('theme_editor_unauthorized')); + exit; +} + +if (!$config->get('allow_theme_file_editor')) { + ThemeEditorResponse::send($response, false, 'editor_disabled', $backendTranslations->getTranslation('theme_editor_disabled')); + exit; +} + +if (!$request->method('post')) { + ThemeEditorResponse::send($response, false, 'method_not_allowed', $backendTranslations->getTranslation('theme_editor_method_not_allowed')); + exit; } -// Проверка сессии для защиты от xss if (!$request->checkSession()) { - trigger_error('Session expired', E_USER_WARNING); - exit(); + ThemeEditorResponse::send($response, false, 'csrf_invalid', $backendTranslations->getTranslation('theme_editor_csrf_invalid')); + exit; } + $content = $request->post('content'); $template = $request->post('template'); $theme = $request->post('theme', 'string'); +if (!is_string($content) || !is_string($template) || $template === '' || basename($template) !== $template) { + ThemeEditorResponse::send($response, false, 'invalid_path', $backendTranslations->getTranslation('theme_editor_invalid_path')); + exit; +} + +$rootDir = $config->get('root_dir'); +if (!is_string($rootDir)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; +} + if ($request->post("email")) { $template = "email/{$template}"; } /*Сохранение файлов шаблона из админки*/ -$file = $config->root_dir.'design/'.$theme.'/html/'.$template; +$file = $rootDir . 'design/' . $theme . '/html/' . $template; + +if (pathinfo($template, PATHINFO_EXTENSION) != 'tpl') { + ThemeEditorResponse::send($response, false, 'invalid_extension', $backendTranslations->getTranslation('theme_editor_invalid_extension')); + exit; +} + +if ($file != realpath($file)) { + ThemeEditorResponse::send($response, false, 'invalid_path', $backendTranslations->getTranslation('theme_editor_invalid_path')); + exit; +} + +if (!is_file($file) || !is_writable($file)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; +} -if (pathinfo($template, PATHINFO_EXTENSION) != 'tpl' || $file != realpath($file)) { - exit(); +if (is_file($rootDir . 'design/' . $theme . '/locked')) { + ThemeEditorResponse::send($response, false, 'theme_locked', $backendTranslations->getTranslation('theme_editor_locked')); + exit; } -if (is_file($file) && is_writable($file) && !is_file($config->root_dir.'design/'.$theme.'/locked')) { - file_put_contents($file, $content); +$writtenBytes = file_put_contents($file, $content, LOCK_EX); +if ($writtenBytes === false || $writtenBytes !== strlen($content)) { + ThemeEditorResponse::send($response, false, 'write_failed', $backendTranslations->getTranslation('theme_editor_write_failed')); + exit; } -$result = true; -$response->setContent(json_encode($result), RESPONSE_JSON); -$response->sendContent(); +ThemeEditorResponse::send($response, true, 'saved', $backendTranslations->getTranslation('theme_editor_saved')); diff --git a/backend/ajax/stat.php b/backend/ajax/stat.php index 3541b216f..1ffda94f4 100644 --- a/backend/ajax/stat.php +++ b/backend/ajax/stat.php @@ -33,7 +33,7 @@ $data = $db->results(); $results = []; -foreach($data as $d) { +foreach ($data as $d) { $result['day'] = $d->day; $result['month'] = $d->month; $result['year'] = $d->year; diff --git a/backend/design/css.php b/backend/design/css.php index c14cf346f..12c6e9588 100644 --- a/backend/design/css.php +++ b/backend/design/css.php @@ -1,4 +1,5 @@ setDir('backend/design/js/jquery'), - (new Css('jquery.fancybox.min.css')), + (new Css('jquery.fancybox.min.css')), (new Css('grid.css')), (new Css('reboot.css')), (new Css('font-awesome.min.css')), @@ -25,4 +26,3 @@ (new Css('media.css')), (new Css('introjs.css'))->setDir('backend/design/js/intro_js'), ]; - diff --git a/backend/design/fonts/fontawesome-webfont.svg b/backend/design/fonts/fontawesome-webfont.svg index 8b66187fe..855c845e5 100644 --- a/backend/design/fonts/fontawesome-webfont.svg +++ b/backend/design/fonts/fontawesome-webfont.svg @@ -1,685 +1,2671 @@ - - + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/design/html/learning_hints.tpl b/backend/design/html/learning_hints.tpl index be78d3f5b..1d4784fc0 100644 --- a/backend/design/html/learning_hints.tpl +++ b/backend/design/html/learning_hints.tpl @@ -1,5 +1,72 @@ + - diff --git a/backend/design/html/scripts.tpl b/backend/design/html/scripts.tpl index 73905715f..c344c3402 100644 --- a/backend/design/html/scripts.tpl +++ b/backend/design/html/scripts.tpl @@ -46,26 +46,90 @@ {/literal} @@ -131,7 +195,29 @@
-
{$btr->scripts_scripts|escape} {$script_file}
+
{$btr->scripts_scripts|escape} {$script_file|escape}
+ + {if !$theme_editor_enabled} +
+
{$btr->theme_editor_disabled|escape}
+
+ {/if} + +
+
+ +
diff --git a/backend/design/html/styles.tpl b/backend/design/html/styles.tpl index d222fff17..0e5f07a3d 100644 --- a/backend/design/html/styles.tpl +++ b/backend/design/html/styles.tpl @@ -31,26 +31,90 @@ {/literal} @@ -60,7 +124,7 @@ $(function() {
- {$btr->general_theme|escape} {$theme} {$btr->styles_style|escape} {$style_file} + {$btr->general_theme|escape} {$theme|escape} {$btr->styles_style|escape} {$style_file|escape}
@@ -118,6 +182,28 @@ $(function() {
{$btr->styles_styles|escape} {$style_file|escape}
+ + {if !$theme_editor_enabled} +
+
{$btr->theme_editor_disabled|escape}
+
+ {/if} + +
+
+ +
diff --git a/backend/design/html/templates.tpl b/backend/design/html/templates.tpl index 80a28a9bf..31ccda4f3 100644 --- a/backend/design/html/templates.tpl +++ b/backend/design/html/templates.tpl @@ -7,7 +7,7 @@
- {$btr->general_theme|escape} {$theme} {$btr->general_template|escape} {$template_file} + {$btr->general_theme|escape} {$theme|escape} {$btr->general_template|escape} {$template_file|escape}
@@ -67,7 +67,30 @@
-
{$btr->general_template|escape} {$template_file}
+
{$btr->general_template|escape} {$template_file|escape}
+ + {if !$theme_editor_enabled} +
+
{$btr->theme_editor_disabled|escape}
+
+ {/if} + +
+
+ +
@@ -131,32 +154,95 @@ {/literal} @@ -176,4 +262,3 @@ }); {/literal} - diff --git a/backend/design/js.php b/backend/design/js.php index 5be4d42ca..25012ecc1 100644 --- a/backend/design/js.php +++ b/backend/design/js.php @@ -1,4 +1,5 @@ has(Config::class)) { + $DI->bindServices(require $okayRootDir . '/Okay/Core/config/services.php'); + $DI->bindParameters(require $okayRootDir . '/Okay/Core/config/parameters.php'); +} +$okayConfig = $DI->get(Config::class); +Request::setTrustedRequestContext(TrustedRequestContext::fromServer( + $_SERVER, + is_string($okayConfig->get('trusted_proxy_ips')) ? $okayConfig->get('trusted_proxy_ips') : null, + is_string($okayConfig->get('trusted_proxy_client_ip_header')) ? $okayConfig->get('trusted_proxy_client_ip_header') : 'X-Forwarded-For' +)); + $version = "9.14.0"; if (session_status() === PHP_SESSION_NONE) { session_name('okay_admin_sid'); + AdminSession::configureCookieParams($_SERVER); session_start(); } mb_internal_encoding('UTF-8'); @@ -71,7 +90,7 @@ | without final / (DON'T TOUCH) | */ - 'base_url' => ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == "on") ? "https" : "http") . "://" . @$_SERVER['HTTP_HOST'], + 'base_url' => Request::getProtocol() . "://" . @$_SERVER['HTTP_HOST'], /* |-------------------------------------------------------------------------- | path from base_url to base of upload folder diff --git a/backend/design/js/filemanager/index.php b/backend/design/js/filemanager/index.php index c30c28fc3..f0294fdab 100644 --- a/backend/design/js/filemanager/index.php +++ b/backend/design/js/filemanager/index.php @@ -1,6 +1,7 @@ "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/bg_BG.php b/backend/design/js/filemanager/lang/bg_BG.php index 7295eb57c..b0946dd71 100644 --- a/backend/design/js/filemanager/lang/bg_BG.php +++ b/backend/design/js/filemanager/lang/bg_BG.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/cs.php b/backend/design/js/filemanager/lang/cs.php index 01d47853e..8c287e50e 100755 --- a/backend/design/js/filemanager/lang/cs.php +++ b/backend/design/js/filemanager/lang/cs.php @@ -2,146 +2,146 @@ return array( - 'Select' => 'Vybrat', - 'Deselect_All' => 'Zrušit vše', - 'Select_All' => 'Vybrat vše', - 'Erase' => 'Smazat', - 'Open' => 'Otevřít', - 'Confirm_del' => 'Opravdu chcete smazat tento soubor?', - 'All' => 'Vše', - 'Files' => 'Soubory', - 'Images' => 'Obrázky', - 'Archives' => 'Archivy', - 'Error_Upload' => 'Nahrávaný soubor je příliš velký.', - 'Error_extension' => 'Nahrávání souborů s touto příponou není povoleno.', - 'Upload_file' => 'Nahrát soubor', - 'Filters' => 'Filtr', - 'Videos' => 'Videa', - 'Music' => 'Hudba', - 'New_Folder' => 'Nová složka', - 'Folder_Created' => 'Složka vytvořena', - 'Existing_Folder' => 'Existující složka', - 'Confirm_Folder_del' => 'Opravdu chcete smazat tuto složku a její obsah?', - 'Return_Files_List' => 'Zpět k seznamu souborů', - 'Preview' => 'Náhled', - 'Download' => 'Stáhnout', - 'Insert_Folder_Name' => 'Vložte název složky:', - 'Root' => 'root', - 'Rename' => 'Přejmenovat', - 'Back' => 'zpět', - 'View' => 'Zobrazení', - 'View_list' => 'Seznam souborů', - 'View_columns_list' => 'Dvousloupcový seznam souborů', - 'View_boxes' => 'Dlaždicové zobrazení', - 'Toolbar' => 'Panel nástrojů', - 'Actions' => 'Akce', - 'Rename_existing_file' => 'Tento soubor již existuje', - 'Rename_existing_folder' => 'Tato složka již existuje', - 'Empty_name' => 'Zadali jste prázdný název', - 'Text_filter' => 'textový filtr', - 'Swipe_help' => 'Pro zobrazení možností klikněte na název souboru/složky.', - 'Upload_base' => 'Základní nahrávání', - 'Upload_base_help' => "Soubory přetáhněte (pouze moderní prohlížeče) nebo klikněte na horní tlačítko 'Přidat soubor(y)' a poté na tlačítko 'Sputit nahrávání'. Až bude nahrávání dokončeno, klikněte na 'Zpět k seznamu souborů'.", - 'Upload_add_files' => 'Přidat soubor(y)', - 'Upload_start' => 'Sputit nahrávání', - 'Upload_error_messages' =>array( - 1 => 'Nahrávaný soubor má větší velikost, než co povoluje direktiva upload_max_filesize v php.ini', - 2 => 'Nahrávaný soubor má větší velikost, než co povoluje direktiva MAX_FILE_SIZE uvedená v HTML formuláři', - 3 => 'Soubor byl nahrán pouze z části', - 4 => 'Nebyl nahrán žádný soubor', - 6 => 'Chybí dočasná složka', - 7 => 'Při zapisování souboru na disk došlo k chybě', - 8 => 'Nahrávání souborů zastavilo rozšížení PHP', - 'post_max_size' => 'Nahrávaný soubor má větší velikost, než co povoluje direktiva post_max_size v php.ini', - 'max_file_size' => 'Příliš velký soubor', - 'min_file_size' => 'Příliš malý soubor', - 'accept_file_types' => 'Není povolen tento typ souboru (přípona)', - 'max_number_of_files' => 'Překročen maximální počet souborů', - 'max_width' => 'Obrázek přesahuje maximální šířku', - 'min_width' => 'Obrázek vyžaduje minimální šířku', - 'max_height' => 'Obrázek přesahuje maximální výšku', - 'min_height' => 'Obrázek vyžaduje minimální výšku', - 'abort' => 'Nahrávání souboru bylo přerušeno', - 'image_resize' => 'Nepodařilo se změnit velikost obrázku' - ), - 'Upload_url' => 'Z url adresy', - 'Type_dir' => 'složka', - 'Type' => 'Typ', - 'Dimension' => 'Rozměr', - 'Size' => 'Velikost', - 'Date' => 'Datum', - 'Filename' => 'Název', - 'Operations' => 'Operace', - 'Date_type' => 'd.m.Y', - 'OK' => 'OK', - 'Cancel' => 'Zrušit', - 'Sorting' => 'Řazení', - 'Show_url' => 'Zobrazit URL adresu', - 'Extract' => 'Rozbalit zde', - 'File_info' => 'Informace o souboru', - 'Edit_image' => 'Upravit obrázek', - 'Duplicate' => 'Duplikovat', - 'Folders' => 'Složky', - 'Copy' => 'Kopírovat', - 'Cut' => 'Vyjmout', - 'Paste' => 'Vložit', - 'CB' => 'Schránka', // clipboard - 'Paste_Here' => 'Vložit do této složky', - 'Paste_Confirm' => 'Skutečně chcete vložit obsah schránky do této složky? Existující soubory či složky budou přepsány.', - 'Paste_Failed' => 'Vložení selhalo', - 'Clear_Clipboard' => 'Vymazat schránku', - 'Clear_Clipboard_Confirm' => 'Skutečně chcete vymazat obsah schránky?', - 'Files_ON_Clipboard' => 'Ve schránce jsou soubory.', - 'Copy_Cut_Size_Limit' => 'Zvolené soubory/složky jsou příliš velké pro operaci %s. Limit: %d MB/operace', // %s = cut or copy - 'Copy_Cut_Count_Limit' => 'Zvolili jste příliš mnoho souborů/složek pro operaci %s. Limit: %d souborů/operace', // %s = cut or copy - 'Copy_Cut_Not_Allowed' => 'Nemáte oprávnění %s.', // %s(1) = cut or copy, %s(2) = files or folders - 'Image_Editor_No_Save' => 'Obrázek nelze uložit', - 'Image_Editor_Exit' => "Exit", + 'Select' => 'Vybrat', + 'Deselect_All' => 'Zrušit vše', + 'Select_All' => 'Vybrat vše', + 'Erase' => 'Smazat', + 'Open' => 'Otevřít', + 'Confirm_del' => 'Opravdu chcete smazat tento soubor?', + 'All' => 'Vše', + 'Files' => 'Soubory', + 'Images' => 'Obrázky', + 'Archives' => 'Archivy', + 'Error_Upload' => 'Nahrávaný soubor je příliš velký.', + 'Error_extension' => 'Nahrávání souborů s touto příponou není povoleno.', + 'Upload_file' => 'Nahrát soubor', + 'Filters' => 'Filtr', + 'Videos' => 'Videa', + 'Music' => 'Hudba', + 'New_Folder' => 'Nová složka', + 'Folder_Created' => 'Složka vytvořena', + 'Existing_Folder' => 'Existující složka', + 'Confirm_Folder_del' => 'Opravdu chcete smazat tuto složku a její obsah?', + 'Return_Files_List' => 'Zpět k seznamu souborů', + 'Preview' => 'Náhled', + 'Download' => 'Stáhnout', + 'Insert_Folder_Name' => 'Vložte název složky:', + 'Root' => 'root', + 'Rename' => 'Přejmenovat', + 'Back' => 'zpět', + 'View' => 'Zobrazení', + 'View_list' => 'Seznam souborů', + 'View_columns_list' => 'Dvousloupcový seznam souborů', + 'View_boxes' => 'Dlaždicové zobrazení', + 'Toolbar' => 'Panel nástrojů', + 'Actions' => 'Akce', + 'Rename_existing_file' => 'Tento soubor již existuje', + 'Rename_existing_folder' => 'Tato složka již existuje', + 'Empty_name' => 'Zadali jste prázdný název', + 'Text_filter' => 'textový filtr', + 'Swipe_help' => 'Pro zobrazení možností klikněte na název souboru/složky.', + 'Upload_base' => 'Základní nahrávání', + 'Upload_base_help' => "Soubory přetáhněte (pouze moderní prohlížeče) nebo klikněte na horní tlačítko 'Přidat soubor(y)' a poté na tlačítko 'Sputit nahrávání'. Až bude nahrávání dokončeno, klikněte na 'Zpět k seznamu souborů'.", + 'Upload_add_files' => 'Přidat soubor(y)', + 'Upload_start' => 'Sputit nahrávání', + 'Upload_error_messages' => array( + 1 => 'Nahrávaný soubor má větší velikost, než co povoluje direktiva upload_max_filesize v php.ini', + 2 => 'Nahrávaný soubor má větší velikost, než co povoluje direktiva MAX_FILE_SIZE uvedená v HTML formuláři', + 3 => 'Soubor byl nahrán pouze z části', + 4 => 'Nebyl nahrán žádný soubor', + 6 => 'Chybí dočasná složka', + 7 => 'Při zapisování souboru na disk došlo k chybě', + 8 => 'Nahrávání souborů zastavilo rozšížení PHP', + 'post_max_size' => 'Nahrávaný soubor má větší velikost, než co povoluje direktiva post_max_size v php.ini', + 'max_file_size' => 'Příliš velký soubor', + 'min_file_size' => 'Příliš malý soubor', + 'accept_file_types' => 'Není povolen tento typ souboru (přípona)', + 'max_number_of_files' => 'Překročen maximální počet souborů', + 'max_width' => 'Obrázek přesahuje maximální šířku', + 'min_width' => 'Obrázek vyžaduje minimální šířku', + 'max_height' => 'Obrázek přesahuje maximální výšku', + 'min_height' => 'Obrázek vyžaduje minimální výšku', + 'abort' => 'Nahrávání souboru bylo přerušeno', + 'image_resize' => 'Nepodařilo se změnit velikost obrázku' + ), + 'Upload_url' => 'Z url adresy', + 'Type_dir' => 'složka', + 'Type' => 'Typ', + 'Dimension' => 'Rozměr', + 'Size' => 'Velikost', + 'Date' => 'Datum', + 'Filename' => 'Název', + 'Operations' => 'Operace', + 'Date_type' => 'd.m.Y', + 'OK' => 'OK', + 'Cancel' => 'Zrušit', + 'Sorting' => 'Řazení', + 'Show_url' => 'Zobrazit URL adresu', + 'Extract' => 'Rozbalit zde', + 'File_info' => 'Informace o souboru', + 'Edit_image' => 'Upravit obrázek', + 'Duplicate' => 'Duplikovat', + 'Folders' => 'Složky', + 'Copy' => 'Kopírovat', + 'Cut' => 'Vyjmout', + 'Paste' => 'Vložit', + 'CB' => 'Schránka', // clipboard + 'Paste_Here' => 'Vložit do této složky', + 'Paste_Confirm' => 'Skutečně chcete vložit obsah schránky do této složky? Existující soubory či složky budou přepsány.', + 'Paste_Failed' => 'Vložení selhalo', + 'Clear_Clipboard' => 'Vymazat schránku', + 'Clear_Clipboard_Confirm' => 'Skutečně chcete vymazat obsah schránky?', + 'Files_ON_Clipboard' => 'Ve schránce jsou soubory.', + 'Copy_Cut_Size_Limit' => 'Zvolené soubory/složky jsou příliš velké pro operaci %s. Limit: %d MB/operace', // %s = cut or copy + 'Copy_Cut_Count_Limit' => 'Zvolili jste příliš mnoho souborů/složek pro operaci %s. Limit: %d souborů/operace', // %s = cut or copy + 'Copy_Cut_Not_Allowed' => 'Nemáte oprávnění %s.', // %s(1) = cut or copy, %s(2) = files or folders + 'Image_Editor_No_Save' => 'Obrázek nelze uložit', + 'Image_Editor_Exit' => "Exit", 'Image_Editor_Save' => "Save", 'Zip_No_Extract' => 'Archív nelze rozbalit. Soubor může být poškozen.', - 'Zip_Invalid' => 'Přípona není podporována. Povolené: zip, gz, tar.', - 'Dir_No_Write' => 'Vybraná složka není zapisovatelná.', - 'Function_Disabled' => 'Funkce %s byla zamítnuta serverem.', // %s = cut or copy - 'File_Permission' => 'Práva souboru', - 'File_Permission_Not_Allowed' => 'Změna oprávnění pro %s není povolena.', // %s = files or folders - 'File_Permission_Recursive' => 'Použít rekurzivně?', - 'File_Permission_Wrong_Mode' => "Zadaný kód oprávnění není platný.", - 'User' => 'Uživatel', - 'Group' => 'Skupina', - 'Yes' => 'Ano', - 'No' => 'Ne', - 'Lang_Not_Found' => 'Jazyk nebyl nalezen.', - 'Lang_Change' => 'Změnit jazyk', - 'File_Not_Found' => 'Soubor nebyl nalezen.', - 'File_Open_Edit_Not_Allowed' => 'Nemáte oprávnění %s tento soubor.', // %s = open or edit - 'Edit' => 'Upravit', - 'Edit_File' => "Upravit obsah souboru", - 'File_Save_OK' => "Soubor byl úspěšně uložen.", - 'File_Save_Error' => "Došlo k chybě při ukládání souboru.", - 'New_File' => 'Nový soubor', - 'No_Extension' => 'Musíte doplnit příponu souboru.', - 'Valid_Extensions' => 'Povolené přípony: %s', // %s = txt,log etc. - 'Upload_message' => "Pro nahrání přetáhněte soubor(y) sem", + 'Zip_Invalid' => 'Přípona není podporována. Povolené: zip, gz, tar.', + 'Dir_No_Write' => 'Vybraná složka není zapisovatelná.', + 'Function_Disabled' => 'Funkce %s byla zamítnuta serverem.', // %s = cut or copy + 'File_Permission' => 'Práva souboru', + 'File_Permission_Not_Allowed' => 'Změna oprávnění pro %s není povolena.', // %s = files or folders + 'File_Permission_Recursive' => 'Použít rekurzivně?', + 'File_Permission_Wrong_Mode' => "Zadaný kód oprávnění není platný.", + 'User' => 'Uživatel', + 'Group' => 'Skupina', + 'Yes' => 'Ano', + 'No' => 'Ne', + 'Lang_Not_Found' => 'Jazyk nebyl nalezen.', + 'Lang_Change' => 'Změnit jazyk', + 'File_Not_Found' => 'Soubor nebyl nalezen.', + 'File_Open_Edit_Not_Allowed' => 'Nemáte oprávnění %s tento soubor.', // %s = open or edit + 'Edit' => 'Upravit', + 'Edit_File' => "Upravit obsah souboru", + 'File_Save_OK' => "Soubor byl úspěšně uložen.", + 'File_Save_Error' => "Došlo k chybě při ukládání souboru.", + 'New_File' => 'Nový soubor', + 'No_Extension' => 'Musíte doplnit příponu souboru.', + 'Valid_Extensions' => 'Povolené přípony: %s', // %s = txt,log etc. + 'Upload_message' => "Pro nahrání přetáhněte soubor(y) sem", - 'SERVER ERROR' => "CHYBA SERVERU", - 'forbiden' => "Zakázáno", - 'wrong path' => "Neplatná cesta", - 'wrong name' => "Neplatná název", - 'wrong extension' => "Neplatná přípona", - 'wrong option' => "Neplatná volba", - 'wrong data' => "Neplatná data", - 'wrong action' => "Neplatná akce", - 'wrong sub-action' => "Neplatná podakce", - 'no action passed' => "Nebyla předána žádná akce", - 'no path' => "Žádná cesta", - 'no file' => "Žádný soubor", - 'view type number missing' => "Chybí číslo typu pro zobrazení", - 'Not enough Memory' => "Nedostatek paměti", - 'max_size_reached' => "Vaše složka s obrázky dosáhla maximální velikosti %d MB.", //%d = max overall size - 'B' => "B", - 'KB' => "KB", - 'MB' => "MB", - 'GB' => "GB", - 'TB' => "TB", - 'total size' => "Celková velikost", + 'SERVER ERROR' => "CHYBA SERVERU", + 'forbiden' => "Zakázáno", + 'wrong path' => "Neplatná cesta", + 'wrong name' => "Neplatná název", + 'wrong extension' => "Neplatná přípona", + 'wrong option' => "Neplatná volba", + 'wrong data' => "Neplatná data", + 'wrong action' => "Neplatná akce", + 'wrong sub-action' => "Neplatná podakce", + 'no action passed' => "Nebyla předána žádná akce", + 'no path' => "Žádná cesta", + 'no file' => "Žádný soubor", + 'view type number missing' => "Chybí číslo typu pro zobrazení", + 'Not enough Memory' => "Nedostatek paměti", + 'max_size_reached' => "Vaše složka s obrázky dosáhla maximální velikosti %d MB.", //%d = max overall size + 'B' => "B", + 'KB' => "KB", + 'MB' => "MB", + 'GB' => "GB", + 'TB' => "TB", + 'total size' => "Celková velikost", ); diff --git a/backend/design/js/filemanager/lang/da.php b/backend/design/js/filemanager/lang/da.php index 078366919..e475fec0c 100644 --- a/backend/design/js/filemanager/lang/da.php +++ b/backend/design/js/filemanager/lang/da.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/el_GR.php b/backend/design/js/filemanager/lang/el_GR.php index 8b59aae7a..f2c562822 100644 --- a/backend/design/js/filemanager/lang/el_GR.php +++ b/backend/design/js/filemanager/lang/el_GR.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/en_EN.php b/backend/design/js/filemanager/lang/en_EN.php index 16ea2a834..8a8ad013e 100755 --- a/backend/design/js/filemanager/lang/en_EN.php +++ b/backend/design/js/filemanager/lang/en_EN.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/es.php b/backend/design/js/filemanager/lang/es.php index b71972140..b59be4bb9 100644 --- a/backend/design/js/filemanager/lang/es.php +++ b/backend/design/js/filemanager/lang/es.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Arrastrar y soltar archivos(Drag & Drop Navegadores modernos) o haz click en el botón superior para Añadir los archivos y click en Empezar subida. Cuando la subida se haya completado, haz click en el botón 'Regresar a la lista de archivos'", 'Upload_add_files' => 'Añadir archivos', 'Upload_start' => 'Empezar subida', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'El archivo subido excede la directiva upload_max_filesize en php.ini', 2 => 'El archivo subido excede la directiva MAX_FILE_SIZE especificada en el formulario HTML', 3 => 'El archivo subido solo fue subido parcialmente', diff --git a/backend/design/js/filemanager/lang/fa.php b/backend/design/js/filemanager/lang/fa.php index 7908ded7a..d05e3fa10 100644 --- a/backend/design/js/filemanager/lang/fa.php +++ b/backend/design/js/filemanager/lang/fa.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/fr_FR.php b/backend/design/js/filemanager/lang/fr_FR.php index ce5870a85..a648cfdcc 100755 --- a/backend/design/js/filemanager/lang/fr_FR.php +++ b/backend/design/js/filemanager/lang/fr_FR.php @@ -44,7 +44,7 @@ 'Upload_base_help' => 'Glisser-Déposer des fichiers (navigateurs récents) ou cliquer sur le bontons en haut pour ajouter des fichiers. Cliquer sur "Envoyer les fichiers". Lorsque le chargement est complet, cliquer sur "Revenir à la liste des fichiers".', 'Upload_add_files' => 'Ajouter des fichiers', 'Upload_start' => 'Envoyer les fichiers', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'La taille du fichier dépasse la limite fixée par le paramètre upload_max_filesize dans php.ini', 2 => 'La taille du fichier dépasse la limite fixée par le paramètre MAX_FILE_SIZE du formulaire', 3 => 'Le fichier n\'a pas été correctement téléchargé', diff --git a/backend/design/js/filemanager/lang/he_IL.php b/backend/design/js/filemanager/lang/he_IL.php index 00d7fe918..6351a0879 100644 --- a/backend/design/js/filemanager/lang/he_IL.php +++ b/backend/design/js/filemanager/lang/he_IL.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/hr.php b/backend/design/js/filemanager/lang/hr.php index ad3cece4b..0934dcc89 100644 --- a/backend/design/js/filemanager/lang/hr.php +++ b/backend/design/js/filemanager/lang/hr.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/hu_HU.php b/backend/design/js/filemanager/lang/hu_HU.php index 7e259d1ee..3c5c32107 100644 --- a/backend/design/js/filemanager/lang/hu_HU.php +++ b/backend/design/js/filemanager/lang/hu_HU.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Húzza ide a feltölteni kívánt fájlokat, vagy kattintson a 'Fájl(ok) hozzáadása gombra. Ha kiválasztotta a fájlokat kattintson a 'Feltöltés indítása' gomba. Miután elkészült a feltöltés kattintson a fenti 'Vissza a fájllistához' gombra.", 'Upload_add_files' => 'Fájl(ok) hozzáadása', 'Upload_start' => 'Feltöltés elindítása', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/id.php b/backend/design/js/filemanager/lang/id.php index 75795e6d6..58143b771 100644 --- a/backend/design/js/filemanager/lang/id.php +++ b/backend/design/js/filemanager/lang/id.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/it.php b/backend/design/js/filemanager/lang/it.php index 52bc49c7d..617e4ec9c 100644 --- a/backend/design/js/filemanager/lang/it.php +++ b/backend/design/js/filemanager/lang/it.php @@ -44,7 +44,7 @@ 'Upload_add_files' => 'Aggiungi file', 'Upload_start' => "Esegui l'upload", 'Upload_base_help' => "Trascina i file nell'area superiore (per i moderni browser) o clicca sul bottone \"Aggiungi file\" e clicca sul bottone \"Esegui l'upload\". Quando il caricamento dei file è terminato clicca sul bottone di ritorno in alto.", - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/ja.php b/backend/design/js/filemanager/lang/ja.php index f2bcb0467..92cf392b2 100755 --- a/backend/design/js/filemanager/lang/ja.php +++ b/backend/design/js/filemanager/lang/ja.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/lt.php b/backend/design/js/filemanager/lang/lt.php index 355e90efe..cd0001fe4 100644 --- a/backend/design/js/filemanager/lang/lt.php +++ b/backend/design/js/filemanager/lang/lt.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/mn_MN.php b/backend/design/js/filemanager/lang/mn_MN.php index eeb9f0719..5f8bb6bf0 100644 --- a/backend/design/js/filemanager/lang/mn_MN.php +++ b/backend/design/js/filemanager/lang/mn_MN.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/nb_NO.php b/backend/design/js/filemanager/lang/nb_NO.php index cd2cf15ff..2604cf54c 100644 --- a/backend/design/js/filemanager/lang/nb_NO.php +++ b/backend/design/js/filemanager/lang/nb_NO.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/nl.php b/backend/design/js/filemanager/lang/nl.php index f0abb10c9..d37026f68 100644 --- a/backend/design/js/filemanager/lang/nl.php +++ b/backend/design/js/filemanager/lang/nl.php @@ -45,7 +45,7 @@ 'Upload_base_help' => "Drag & Drop bestanden (moderne browsers) of klik op de bovenste knop om het bestand (en) toe te voegen en klik op Begin uploaden. Wanneer het uploaden is voltooid, klikt u op de knop 'Terug naar bestanden'.", 'Upload_add_files' => 'Voeg bestanden toe', 'Upload_start' => 'Begin uploaden', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'Bestandsgrootte is te groot.', 2 => 'Bestandsgrootte is te groot.', 3 => 'Bestand is slechts gedeeltelijk geupload.', diff --git a/backend/design/js/filemanager/lang/nn_NO.php b/backend/design/js/filemanager/lang/nn_NO.php index a1535a68b..4c5f07c08 100644 --- a/backend/design/js/filemanager/lang/nn_NO.php +++ b/backend/design/js/filemanager/lang/nn_NO.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Dra og slepp filer (moderne nettlesarar) eller klikk på knappen «Legg til fil(er)» øvst og deretter på «Start opplasting». Når opplastinga er ferdig, klikk knappen «Tilbake til filoversikta».", 'Upload_add_files' => 'Legg til fil(er)', 'Upload_start' => 'Start opplasting', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'Fila enn større enn grensa upload_max_filesize i php.ini', 2 => 'Fila er større enn direktivet MAX_FILE_SIZE i HTML-skjemaet', 3 => 'Fila vart berre delvis opplasta', diff --git a/backend/design/js/filemanager/lang/pl.php b/backend/design/js/filemanager/lang/pl.php index d78408da2..f3ae6a5eb 100644 --- a/backend/design/js/filemanager/lang/pl.php +++ b/backend/design/js/filemanager/lang/pl.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/pt_BR.php b/backend/design/js/filemanager/lang/pt_BR.php index 42fbdd2d2..c3fd60e84 100644 --- a/backend/design/js/filemanager/lang/pt_BR.php +++ b/backend/design/js/filemanager/lang/pt_BR.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/pt_PT.php b/backend/design/js/filemanager/lang/pt_PT.php index 837a35112..571397d79 100644 --- a/backend/design/js/filemanager/lang/pt_PT.php +++ b/backend/design/js/filemanager/lang/pt_PT.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/ro.php b/backend/design/js/filemanager/lang/ro.php index 9ef5867e8..f15e63a68 100644 --- a/backend/design/js/filemanager/lang/ro.php +++ b/backend/design/js/filemanager/lang/ro.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Adaugă fișiere (drag & drop - browsere moderne) sau click pe butonul Adaugă fișier(e), de mai sus apoi pe butonul Start upload. După ce upload-ul este finalizat, apăsați pe butonul Înapoi la lista de fișiere.", 'Upload_add_files' => 'Adaugă fișier(e)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'Dimensiunea fișierului uploadat depășește valoarea din directiva upload_max_filesize din fișierul php.ini', 2 => 'Dimensiunea fișierului uploadat depășește valoarea din directiva MAX_FILE_SIZE specificată în formularul HTML', 3 => 'Fișierul uploadat a fost încărcat parțial ', diff --git a/backend/design/js/filemanager/lang/ru.php b/backend/design/js/filemanager/lang/ru.php index 514598839..724a6be81 100755 --- a/backend/design/js/filemanager/lang/ru.php +++ b/backend/design/js/filemanager/lang/ru.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Перетащите файлы (современные браузеры) или нажмите на верхнюю кнопку, чтобы добавить файл(ы), и нажмите «Начать загрузку». Когда загрузка будет завершена, нажмите кнопку «Возврат к списку файлов».", 'Upload_add_files' => 'Добавить файл(ы)', 'Upload_start' => 'Начать загрузку', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'Загруженный файл превышает параметр upload_max_filesize в php.ini', 2 => 'Загруженный файл превышает параметр MAX_FILE_SIZE, указанный в HTML-форме', 3 => 'Файл был загружен только частично', diff --git a/backend/design/js/filemanager/lang/sk.php b/backend/design/js/filemanager/lang/sk.php index dc46a204d..726edb8e8 100644 --- a/backend/design/js/filemanager/lang/sk.php +++ b/backend/design/js/filemanager/lang/sk.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/sl.php b/backend/design/js/filemanager/lang/sl.php index 6119210ab..4db29473a 100644 --- a/backend/design/js/filemanager/lang/sl.php +++ b/backend/design/js/filemanager/lang/sl.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/sv_SE.php b/backend/design/js/filemanager/lang/sv_SE.php index 41c7a15d9..81558cbf5 100644 --- a/backend/design/js/filemanager/lang/sv_SE.php +++ b/backend/design/js/filemanager/lang/sv_SE.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/th_TH.php b/backend/design/js/filemanager/lang/th_TH.php index a25ba148d..03b1c1539 100644 --- a/backend/design/js/filemanager/lang/th_TH.php +++ b/backend/design/js/filemanager/lang/th_TH.php @@ -1,4 +1,5 @@ "สามารถอัพโหลดไฟล์แบบลากวางได้ หรือคลิกที่ปุ่มด้านบนเพื่อเพิ่มไฟล์และคลิกอัพโหลด เมื่อการอัปโหลดเสร็จสมบูรณ์คลิกปุ่ม \"กลับสู่หน้ารายการ\"", 'Upload_add_files' => 'เลือกไฟล์ (สามารเลือกได้หลายไฟล์)', 'Upload_start' => 'อัพโหลด', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/tr_TR.php b/backend/design/js/filemanager/lang/tr_TR.php index 1925ae2ae..4146a379b 100644 --- a/backend/design/js/filemanager/lang/tr_TR.php +++ b/backend/design/js/filemanager/lang/tr_TR.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Dosyaları sürükleyip bırakın (modern tarayıcılar) veya dosya ekleye tıklayarak dosyaları ekleyin ve Yüklemeyi başlat'a tıklayın. Yükleme tamamlandığında, 'Dosya listesine dön' düğmesini tıklayın.", 'Upload_add_files' => 'Dosya(ları) Ekle', 'Upload_start' => 'Yüklemeyi Başlat', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/uk_UA.php b/backend/design/js/filemanager/lang/uk_UA.php index 182b424c2..6a739702f 100644 --- a/backend/design/js/filemanager/lang/uk_UA.php +++ b/backend/design/js/filemanager/lang/uk_UA.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/vi.php b/backend/design/js/filemanager/lang/vi.php index d6725804e..22e180126 100644 --- a/backend/design/js/filemanager/lang/vi.php +++ b/backend/design/js/filemanager/lang/vi.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/design/js/filemanager/lang/zh_CN.php b/backend/design/js/filemanager/lang/zh_CN.php index f6a7166ee..6e239ecd0 100755 --- a/backend/design/js/filemanager/lang/zh_CN.php +++ b/backend/design/js/filemanager/lang/zh_CN.php @@ -44,7 +44,7 @@ 'Upload_base_help' => "Drag & Drop files(modern browsers) or click in upper button to Add the file(s) and click on Start upload. When the upload is complete, click the 'Return to files list' button.", 'Upload_add_files' => 'Add file(s)', 'Upload_start' => 'Start upload', - 'Upload_error_messages' =>array( + 'Upload_error_messages' => array( 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', diff --git a/backend/files/export/export.csv b/backend/files/export/export.csv deleted file mode 100644 index b09b04286..000000000 --- a/backend/files/export/export.csv +++ /dev/null @@ -1,273 +0,0 @@ -Category;Brand;Product;Variant;SKU;Price;"Old price";"Currency ID";Weight;Stock;Units;Visible;Featured;"Meta title";"Meta keywords";"Meta description";Annotation;Description;Images;URL;volume;Вес;Высота;Диаметр;Ткань;Предназначение;Наполнитель;"Количество наполнителя";"Объемный вес для транспортных компаний";"Производство ";Серия;Диагональ;"Тип экрана";"Разрешение экрана";Система;"Количество ядер";Процессор;"Частота процессора";"Оперативная память";"Встроенная память";"Поддержка SD карт";Sim-карт;"Размеры SIM";"GSM (2G)";3G;"LTE (4G)";WCDMA;"Основная камера";"Фронтальная камера";Вспышка;Bluetooth;GPS;A-GPS;Wi-Fi;"ИК порт";Разъемы;Аккумулятор;Размеры;Цвет;"Wi-Fi Hotspot";"Сканер отпечатков пальцев";"3G (UMTS)";Возраст;Пол;Комплектация;Материал;"Размеры упаковки";"Размер игрушки";"Страна производитель";"Тепловая мощность";"Максимальная температура теплоносителя";"Максимальное рабочее давление";"Конструкция радиатора";"Способ подключения";"Кол-во секций в радиаторе";"Объем воды в радиаторе";"Опрессовочное давление";"Способ установки";"Номинальный объем водонагревателя";"Положение установки";Форма;"Мощность ТЭНа";Гарантия;"Тип нагревательного элемента (ТЭНа)";Мощность;"Отвод газов";"Тип розжига";Производительность;"Производительность ГВС";"Модуляция мощности";"Вес (кг)";"Объем (м3)";"Габариты В.Ш.Г. (мм.)";"Мин. давление газа";Страна;"Вид мяса";Производитель;"Количество мест";"Высота сиденья";Водонепронецаемый;Автор;Язык;"Оригинальное название";Обложка;Страниц;Иллюстрации;"Год издания";Рубрика;"тип товара";"диаметр колеса";"длина хода вилки";вилка;"материал рамы";"задний переключатель";"тип заднего амортизатора";"тип тормозов";"Операционная сиситема";Память;Дисплей;Клавиатура;"Параметры сканера";"Тип штрих-кодов";PTT;Датчик;Пылевлагозащита;Ударопрочность;"Рабочая температура";"Габариты (мм)";"Ширина бумаги";"Тип корма";"Назначение корма";Порода;"Класс корма";Монтаж;Дверцы;Габариты -"Home furniture/Диваны";Puff;"Диван-кровать Max 1,2 в ткани Кордрой";Оранжевый;ccv6346;12900.00;;4;;;;1;0;"Диван-кровать Max 1,2 в ткани Кордрой";"Диван-кровать Max 1,2 в ткани Кордрой, Fitbit, Диваны";"Диван-кровать Max 1,2 в ткани Кордрой 336/кордрой 230 Механизм трансформации: аккордеон Спальное место: 120х200 см Каркас: металлический с буковыми ламелями Наполнение матраса: ППУ 10 см";"
  • Вес: 90кг
  • Тип: Аккордеон
  • Размер: 120х200 см
";"

Диван-кровать Max 1,2  в ткани Кордрой 336/кордрой 230

Механизм трансформации: аккордеон

Спальное место: 120х200 см

Каркас: металлический с буковыми ламелями

Наполнение матраса: ППУ 10 см

";"okay_couch2.jpg, okay_couch1.jpg, okay_couch3.jpg, okay_couch4.jpg";divan-krovat-max-12-v-tkani-kordroj;0.00000;90кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"150.9 х 115.2 х 87 cm";;;;;;;;"Буковые ламели";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;2;160;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"Диван-кровать Max 1,2 в ткани Кордрой";Белый;ccv6546;13900.00;;4;;0;;1;0;"Диван-кровать Max 1,2 в ткани Кордрой";"Диван-кровать Max 1,2 в ткани Кордрой, Fitbit, Диваны";"Диван-кровать Max 1,2 в ткани Кордрой 336/кордрой 230 Механизм трансформации: аккордеон Спальное место: 120х200 см Каркас: металлический с буковыми ламелями Наполнение матраса: ППУ 10 см";"
  • Вес: 90кг
  • Тип: Аккордеон
  • Размер: 120х200 см
";"

Диван-кровать Max 1,2  в ткани Кордрой 336/кордрой 230

Механизм трансформации: аккордеон

Спальное место: 120х200 см

Каркас: металлический с буковыми ламелями

Наполнение матраса: ППУ 10 см

";"okay_couch2.jpg, okay_couch1.jpg, okay_couch3.jpg, okay_couch4.jpg";divan-krovat-max-12-v-tkani-kordroj;0.00000;90кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"150.9 х 115.2 х 87 cm";;;;;;;;"Буковые ламели";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;2;160;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"Диван-кровать Max 1,2 в ткани Кордрой";Бежевый;ccv6586;14000.00;;4;;5;;1;0;"Диван-кровать Max 1,2 в ткани Кордрой";"Диван-кровать Max 1,2 в ткани Кордрой, Fitbit, Диваны";"Диван-кровать Max 1,2 в ткани Кордрой 336/кордрой 230 Механизм трансформации: аккордеон Спальное место: 120х200 см Каркас: металлический с буковыми ламелями Наполнение матраса: ППУ 10 см";"
  • Вес: 90кг
  • Тип: Аккордеон
  • Размер: 120х200 см
";"

Диван-кровать Max 1,2  в ткани Кордрой 336/кордрой 230

Механизм трансформации: аккордеон

Спальное место: 120х200 см

Каркас: металлический с буковыми ламелями

Наполнение матраса: ППУ 10 см

";"okay_couch2.jpg, okay_couch1.jpg, okay_couch3.jpg, okay_couch4.jpg";divan-krovat-max-12-v-tkani-kordroj;0.00000;90кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"150.9 х 115.2 х 87 cm";;;;;;;;"Буковые ламели";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;2;160;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"Диван RedKing";;vby5737;45067.00;540000.00;4;;;;1;0;"Диван RedKing";"Диван RedKing, Britax romer, Диваны";;"
  • Ширина: 120см
  • Глубина: 80см
  • Высота: 43см
";;"okay_couch1.jpg, okay_couch2.jpg, okay_couch3.jpg, okay_couch4.jpg";divan-redking;0.00000;"79 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;191-200см;;;;;;;;"Буковые ламели";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;RedKing;1;191;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для животных";"Royal Canin";"Сухой беззерновой полнорационный корм Optimeal для взрослых собак всех пород";;mnb7959;2600.00;;4;10.00;;;1;;"Сухой беззерновой полнорационный корм Optimeal для взрослых собак всех пород";"Сухой беззерновой полнорационный корм Optimeal для взрослых собак всех пород, Royal Canin, Товары для животных";"Optimeal беззерновой для взрослых собак индейка и овощи — оптимальный корм для вашего любимца. В его состав входит уникальный микс из полезных трав, ягод, отборных мясных ингредиентов и пребиотиков. Все это создает ежедневную естественную защиту иммунитета вашей собаки, а также гарантирует здоровье и красоту вашего любимца на долгие годы. Высокое содержание отборного мяса индейки, а также включение в рецептуру корма свежего мяса индейки в виде фарша, обеспечат вашего питомца высококачественным легко";"
  • Вес: 10г
  • Тип: Сухой корм
  • Класс: Супер премиум
";"

Optimeal беззерновой для взрослых собак индейка и овощи — оптимальный корм для вашего любимца. В его состав входит уникальный микс из полезных трав, ягод, отборных мясных ингредиентов и пребиотиков. Все это создает ежедневную естественную защиту иммунитета вашей собаки, а также гарантирует здоровье и красоту вашего любимца на долгие годы.

Высокое содержание отборного мяса индейки, а также включение в рецептуру корма свежего мяса индейки в виде фарша, обеспечат вашего питомца высококачественным легкоусвояемым протеином животного происхождения.

Отсутствие злаков и глютена сохранит пищеварительную систему вашей собаки от дополнительной нагрузки, которой обычно подвергается кишечник и поджелудочная железа для переваривания таких неестественных для собак компонентов корма, как злаковая клейковина (глютен).

Высокое содержание ягод и овощей обеспечивает организм собаки естественным источником витаминов и растительной клетчатки, что значительно улучшит работу пищеварительной системы и здоровья кишечника. Высокое содержание овощей (источников углеводов с низким гликемическим индексом) предотвратит развитие такого страшного заболевания, как сахарный диабет, а также сохранит здоровую стройную фигуру вашей собаки.

Уникальный комплекс для поддержки и укрепления иммунитета Immunity Support Mix состоит из:
Специально очищенные бета-глюканы — улучшают работу иммунной системы для долгой жизни питомца.
Пребиотик нового поколения Actigen — нормализует баланс полезной микрофлоры кишечника и обеспечивает здоровое пищеварение.
Натуральный антиоксидант SubStar ULTRA на основе розмарина благодаря противомикробным и противовоспалительным свойствам — предотвращает разрушение клеток организма и заболеваниям домашних любимцев.

OMEGA 3 и 6 / Цинк / Биотин — поддерживают кожу здоровой, а шерсть — красивой.
СОСТАВ:
дегидратированный протеин мяса индейки, не менее 39%, крахмал картофеля, мясо индейки 12%, жир из птицы, картофель cушена, тапиока, белок гидролизованный животный, зеленый горошек 4,11%, масло лосося, мякоть свеклы , растительная клетчатка, минералы, тыква 1%, морковь 0,79%, семян льна 0,79%, пивные дрожжи, клюква 0,2%, масло огуречника, MacroGard® (источник β-глюкан) 0,05%, Actigen ™ (источник пребиотик — мананоолигосахариды) 0,04%, бархатцы (источник лютеина) 0,04%, корень лопуха, корень алтея, цветы ромашки 0,03%, крапива 0,03%, экстракт юкки, тимьян 0,015 %.

Питательная ценность (анализ) корма:

сырой белок 32%, сырые масло и жири- 16%, сырая зола- 5,9%, сырая клитковина- 2,6%.

На 1 кг корма: омега-3 жирные кислоты- 6,4 г, в т. Ч. EPA / DHA- 3,8 г омега-6 жирные кислоты- 27,6 г.

Добавлено добавки: Питательные добавки (на 1 кг корма), мг / кг:

витамин А: 15000 МЕ, витамин Д3: 800 МЕ, витамин Е 500, витамин С 200, витамин B6 2,22, витамин Н (биотин): 0,14, таурин: 1100, цинк: 104,96 , медь: 11,24, марганец: 2,57, селен: 0,08.

Технологические добавки (на 1 кг корма), мг / кг: антиоксидант природного происхождения: 330, экстракт токоферола природного происхождения: 600.

Энергетическая ценность (калорийность) 100 г корма: 1 628,16 кДж (389,15 ккал).

Хранить в темном месте закрытой потребительской таре при температуре от +5 ° С до +25 ° С и относительной влажности не более 75%

";okay_animal6.jpg;suhoj-bezzernovoj-polnoratsionnyj-korm-optimeal-dlya-vzroslyh-sobak-vseh-porod;;10кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"От 1 года";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Сухой корм";Специальный;"Всех видов";"Супер премиум";;; -"Товары для животных";"Royal Canin";"Сухой беззерновой полнорационный корм Optimeal для взрослых собак всех пород";;;0.00;;4;;;;1;;"Сухой беззерновой полнорационный корм Optimeal для взрослых собак всех пород";"Сухой беззерновой полнорационный корм Optimeal для взрослых собак всех пород, Royal Canin, Товары для животных";"Optimeal беззерновой для взрослых собак индейка и овощи — оптимальный корм для вашего любимца. В его состав входит уникальный микс из полезных трав, ягод, отборных мясных ингредиентов и пребиотиков. Все это создает ежедневную естественную защиту иммунитета вашей собаки, а также гарантирует здоровье и красоту вашего любимца на долгие годы. Высокое содержание отборного мяса индейки, а также включение в рецептуру корма свежего мяса индейки в виде фарша, обеспечат вашего питомца высококачественным легко";"
  • Вес: 10г
  • Тип: Сухой корм
  • Класс: Супер премиум
";"

Optimeal беззерновой для взрослых собак индейка и овощи — оптимальный корм для вашего любимца. В его состав входит уникальный микс из полезных трав, ягод, отборных мясных ингредиентов и пребиотиков. Все это создает ежедневную естественную защиту иммунитета вашей собаки, а также гарантирует здоровье и красоту вашего любимца на долгие годы.

Высокое содержание отборного мяса индейки, а также включение в рецептуру корма свежего мяса индейки в виде фарша, обеспечат вашего питомца высококачественным легкоусвояемым протеином животного происхождения.

Отсутствие злаков и глютена сохранит пищеварительную систему вашей собаки от дополнительной нагрузки, которой обычно подвергается кишечник и поджелудочная железа для переваривания таких неестественных для собак компонентов корма, как злаковая клейковина (глютен).

Высокое содержание ягод и овощей обеспечивает организм собаки естественным источником витаминов и растительной клетчатки, что значительно улучшит работу пищеварительной системы и здоровья кишечника. Высокое содержание овощей (источников углеводов с низким гликемическим индексом) предотвратит развитие такого страшного заболевания, как сахарный диабет, а также сохранит здоровую стройную фигуру вашей собаки.

Уникальный комплекс для поддержки и укрепления иммунитета Immunity Support Mix состоит из:
Специально очищенные бета-глюканы — улучшают работу иммунной системы для долгой жизни питомца.
Пребиотик нового поколения Actigen — нормализует баланс полезной микрофлоры кишечника и обеспечивает здоровое пищеварение.
Натуральный антиоксидант SubStar ULTRA на основе розмарина благодаря противомикробным и противовоспалительным свойствам — предотвращает разрушение клеток организма и заболеваниям домашних любимцев.

OMEGA 3 и 6 / Цинк / Биотин — поддерживают кожу здоровой, а шерсть — красивой.
СОСТАВ:
дегидратированный протеин мяса индейки, не менее 39%, крахмал картофеля, мясо индейки 12%, жир из птицы, картофель cушена, тапиока, белок гидролизованный животный, зеленый горошек 4,11%, масло лосося, мякоть свеклы , растительная клетчатка, минералы, тыква 1%, морковь 0,79%, семян льна 0,79%, пивные дрожжи, клюква 0,2%, масло огуречника, MacroGard® (источник β-глюкан) 0,05%, Actigen ™ (источник пребиотик — мананоолигосахариды) 0,04%, бархатцы (источник лютеина) 0,04%, корень лопуха, корень алтея, цветы ромашки 0,03%, крапива 0,03%, экстракт юкки, тимьян 0,015 %.

Питательная ценность (анализ) корма:

сырой белок 32%, сырые масло и жири- 16%, сырая зола- 5,9%, сырая клитковина- 2,6%.

На 1 кг корма: омега-3 жирные кислоты- 6,4 г, в т. Ч. EPA / DHA- 3,8 г омега-6 жирные кислоты- 27,6 г.

Добавлено добавки: Питательные добавки (на 1 кг корма), мг / кг:

витамин А: 15000 МЕ, витамин Д3: 800 МЕ, витамин Е 500, витамин С 200, витамин B6 2,22, витамин Н (биотин): 0,14, таурин: 1100, цинк: 104,96 , медь: 11,24, марганец: 2,57, селен: 0,08.

Технологические добавки (на 1 кг корма), мг / кг: антиоксидант природного происхождения: 330, экстракт токоферола природного происхождения: 600.

Энергетическая ценность (калорийность) 100 г корма: 1 628,16 кДж (389,15 ккал).

Хранить в темном месте закрытой потребительской таре при температуре от +5 ° С до +25 ° С и относительной влажности не более 75%

";okay_animal6.jpg;suhoj-bezzernovoj-polnoratsionnyj-korm-optimeal-dlya-vzroslyh-sobak-vseh-porod;;10кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"От 1 года";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Сухой корм";Специальный;"Всех видов";"Супер премиум";;; -"Товары для животных";"Royal Canin";"Сухой корм для взрослых собак Club 4 paws для крупных пород";;mnc573;1400.00;1540.00;4;;;;1;0;"Сухой корм для взрослых собак Club 4 paws для крупных пород";"Сухой корм для взрослых собак Club 4 paws для крупных пород, Royal Canin, Товары для животных";;"
  • Вес: 14кг.
  • Тип: Сухой корм
  • Класс: Супер премиум
";;okay_animal4.jpg;suhoj-korm-dlya-vzroslyh-sobak-club-4-paws-dlya-krupnyh-porod;;14кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"От 12 мес.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Сухой корм";Специальный;"Для всех";Эконом;;; -"Товары для животных";"Royal Canin";"Royal Canin British Shorthair Kitten сухой корм для котят породы британская короткошерстная";;mm7327;7800.00;;4;;;;1;0;"Royal Canin British Shorthair Kitten сухой корм для котят породы британская короткошерстная";"Royal Canin British Shorthair Kitten сухой корм для котят породы британская короткошерстная, Royal Canin, Товары для животных";"СПЕЦИАЛЬНО ДЛЯ КОШЕК С БОЛЬШИМИ ЧЕЛЮСТЯМИ Специально разработанные крокеты кубической формы, которые адаптированы к большим квадратным челюстям кошек породы британская. Уникальная форма крокет стимулирует животное к их разгрызанию, что способствует поддержанию гигиены ротовой полости. ЭКСКЛЮЗИВНАЯ ФОРМУЛА: Таурин и омега-3 жирные кислоты ЗДОРОВЬЕ СУСТАВОВ Вследствие большого размера и веса кошки породы персидский ее суставы подвергаются значительной нагрузке на протяжении всей жизни животного. Фор";"
  • Вес: 10г
  • Тип: Сухой корм
  • Класс: Супер премиум
";"

СПЕЦИАЛЬНО ДЛЯ КОШЕК С БОЛЬШИМИ ЧЕЛЮСТЯМИ

Специально разработанные крокеты кубической формы, которые адаптированы к большим квадратным челюстям кошек породы британская. Уникальная форма крокет стимулирует животное к их разгрызанию, что способствует поддержанию гигиены ротовой полости.

ЭКСКЛЮЗИВНАЯ ФОРМУЛА:

Таурин и омега-3 жирные кислоты

ЗДОРОВЬЕ СУСТАВОВ

Вследствие большого размера и веса кошки породы персидский ее суставы подвергаются значительной нагрузке на протяжении всей жизни животного. Формула рациона способствует поддержанию здорового состояния костей и суставов. Рацион обогащен омега-3 жирными кислотами (ЭПК и ДГК).

ЗДОРОВЬЕ СЕРДЦА

Рацион обогащен такими питательными веществами, как таурин, ЭПК и ДГК, которые способствуют поддержанию здоровой работы сердца.

ЗДОРОВЬЕ КОЖИ И БЛЕСК ШЕРСТИ

Серосодержащие аминокислоты, витамины, омега-3 и омега-6 жирные кислоты способствуют поддержанию здоровья кожи и блеска шерсти.

ЗДОРОВЬЕ МОЧЕВЫДЕЛИТЕЛЬНОЙ СИСТЕМЫ

Соответствующий баланс минеральных веществ в рационе способствует поддержанию здоровья мочевыделительной системы взрослых кошек.

персидский является крупнейшим и самым длинным котом в мире, благодаря чему он производит впечатление чрезвычайной мощи и силы. британская может достигать 1 м в длину (вместе с хвостом), а вес котов этой породы может достигать 10-12 кг. Учитывая большой размер и тяжелый костяк особое внимание следует уделять защите суставов кошек этой породы.

";okay_animal3.jpg;royal-canin-british-shorthair-kitten-suhoj-korm-dlya-kotyat-porody-britanskaya-korotkosherstnaya;;10г;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Для котят";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Сухой корм";Специальный;Британская;"Супер премиум";;; -"Товары для животных";"Royal Canin";"Royal Canin Maine Coon Adult сухой корм для котов породы мейн-кун";;mn954;1300.00;;4;;;;1;;"Royal Canin Maine Coon Adult сухой корм для котов породы мейн-кун";"Royal Canin Maine Coon Adult сухой корм для котов породы мейн-кун, Royal Canin, Товары для животных";"СПЕЦИАЛЬНО ДЛЯ КОШЕК С БОЛЬШИМИ ЧЕЛЮСТЯМИ Специально разработанные крокеты кубической формы, которые адаптированы к большим квадратным челюстям кошек породы мейн-кун. Уникальная форма крокет стимулирует животное к их разгрызанию, что способствует поддержанию гигиены ротовой полости. ЭКСКЛЮЗИВНАЯ ФОРМУЛА: Таурин и омега-3 жирные кислоты ЗДОРОВЬЕ СУСТАВОВ Вследствие большого размера и веса кошки породы мейн-кун ее суставы подвергаются значительной нагрузке на протяжении всей жизни животного. Формула";"
  • Вес: 10г
  • Тип: Сухой корм
  • Класс: Супер премиум
";"

СПЕЦИАЛЬНО ДЛЯ КОШЕК С БОЛЬШИМИ ЧЕЛЮСТЯМИ

Специально разработанные крокеты кубической формы, которые адаптированы к большим квадратным челюстям кошек породы мейн-кун. Уникальная форма крокет стимулирует животное к их разгрызанию, что способствует поддержанию гигиены ротовой полости.

ЭКСКЛЮЗИВНАЯ ФОРМУЛА:

Таурин и омега-3 жирные кислоты

ЗДОРОВЬЕ СУСТАВОВ

Вследствие большого размера и веса кошки породы мейн-кун ее суставы подвергаются значительной нагрузке на протяжении всей жизни животного. Формула рациона способствует поддержанию здорового состояния костей и суставов. Рацион обогащен омега-3 жирными кислотами (ЭПК и ДГК).

ЗДОРОВЬЕ СЕРДЦА

Рацион обогащен такими питательными веществами, как таурин, ЭПК и ДГК, которые способствуют поддержанию здоровой работы сердца.

ЗДОРОВЬЕ КОЖИ И БЛЕСК ШЕРСТИ

Серосодержащие аминокислоты, витамины, омега-3 и омега-6 жирные кислоты способствуют поддержанию здоровья кожи и блеска шерсти.

ЗДОРОВЬЕ МОЧЕВЫДЕЛИТЕЛЬНОЙ СИСТЕМЫ

Соответствующий баланс минеральных веществ в рационе способствует поддержанию здоровья мочевыделительной системы взрослых кошек.

Мейн-кун

Мейн-кун является крупнейшим и самым длинным котом в мире, благодаря чему он производит впечатление чрезвычайной мощи и силы. Мейн-кун может достигать 1 м в длину (вместе с хвостом), а вес котов этой породы может достигать 10-12 кг. Учитывая большой размер и тяжелый костяк особое внимание следует уделять защите суставов кошек этой породы.

";okay_animal2.jpg;royal-canin-persian-adult-suhoj-korm-dlya-kotov-persidskoj-porody;;10г;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"От 12 мес.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Сухой корм";Специальный;Мейк-кун;"Супер премиум";;; -"Товары для животных";"Royal Canin";"Royal Canin Maine Coon Adult сухой корм для котов породы мейн-кун";;mn953;2300.00;;4;;;;1;;"Royal Canin Maine Coon Adult сухой корм для котов породы мейн-кун";"Royal Canin Maine Coon Adult сухой корм для котов породы мейн-кун, Royal Canin, Товары для животных";"СПЕЦИАЛЬНО ДЛЯ КОШЕК С БОЛЬШИМИ ЧЕЛЮСТЯМИ Специально разработанные крокеты кубической формы, которые адаптированы к большим квадратным челюстям кошек породы мейн-кун. Уникальная форма крокет стимулирует животное к их разгрызанию, что способствует поддержанию гигиены ротовой полости. ЭКСКЛЮЗИВНАЯ ФОРМУЛА: Таурин и омега-3 жирные кислоты ЗДОРОВЬЕ СУСТАВОВ Вследствие большого размера и веса кошки породы мейн-кун ее суставы подвергаются значительной нагрузке на протяжении всей жизни животного. Формула раци";"
  • Вес: 10г
  • Тип: Сухой корм
  • Класс: Супер премиум
";"

СПЕЦИАЛЬНО ДЛЯ КОШЕК С БОЛЬШИМИ ЧЕЛЮСТЯМИ

Специально разработанные крокеты кубической формы, которые адаптированы к большим квадратным челюстям кошек породы мейн-кун. Уникальная форма крокет стимулирует животное к их разгрызанию, что способствует поддержанию гигиены ротовой полости.

ЭКСКЛЮЗИВНАЯ ФОРМУЛА:

Таурин и омега-3 жирные кислоты

ЗДОРОВЬЕ СУСТАВОВ

Вследствие большого размера и веса кошки породы мейн-кун ее суставы подвергаются значительной нагрузке на протяжении всей жизни животного. Формула рациона способствует поддержанию здорового состояния костей и суставов. Рацион обогащен омега-3 жирными кислотами (ЭПК и ДГК).

ЗДОРОВЬЕ СЕРДЦА

Рацион обогащен такими питательными веществами, как таурин, ЭПК и ДГК, которые способствуют поддержанию здоровой работы сердца.

ЗДОРОВЬЕ КОЖИ И БЛЕСК ШЕРСТИ

Серосодержащие аминокислоты, витамины, омега-3 и омега-6 жирные кислоты способствуют поддержанию здоровья кожи и блеска шерсти.

ЗДОРОВЬЕ МОЧЕВЫДЕЛИТЕЛЬНОЙ СИСТЕМЫ

Соответствующий баланс минеральных веществ в рационе способствует поддержанию здоровья мочевыделительной системы взрослых кошек.

Мейн-кун

Мейн-кун является крупнейшим и самым длинным котом в мире, благодаря чему он производит впечатление чрезвычайной мощи и силы. Мейн-кун может достигать 1 м в длину (вместе с хвостом), а вес котов этой породы может достигать 10-12 кг. Учитывая большой размер и тяжелый костяк особое внимание следует уделять защите суставов кошек этой породы.

";okay_animal.jpg;royal-canin-maine-coon-adult-suhoj-korm-dlya-kotov-porody-mejn-kun;;10г;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"От 12 мес.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Сухой корм";Специальный;Мейк-кун;"Супер премиум";;; -"Товары для бизнеса";;"POS-терминал Sam4s Titan-S 260";;PVVD2525;16600.00;17700.00;4;;;;1;0;"POS-терминал Sam4s Titan-S 260";"POS-терминал Sam4s Titan-S 260, Товары для бизнеса";;"
  • Дисплей: 15"" LED
  • Процессор: Intel Quad
  • Память: 4 ГБ DDR3
  • ОС: Windows 7
";;"okay_tech10.jpg, okay_tech11.jpg, okay_tech12.jpg";schetchik-banknot-dors-800;;;;;;;;;;;;;;;;;"OMAP4 (1 ГГц)";;;;;;;;;;;;;;"Stonestreet-стек: Bluetooth 4.0 плюс BLE или WBA; Microsoft-стек: Bluetooth. версия 2.1 с EDR";;;"Android 4.4.4 (KitKat): IEEE 802.11 a/b/g/n/d/h/i/k/r или Windows Embedded CE 7.0 / Windows Embedded Handheld 6.5.3: IEEE 802.11 a/b/g/n/d/h/i (Защита: WPA2 Enterprise. 802.1x; EAP-TLS; TTLS (CHAP. MS-CHAP. MS-CHAPv2. PAP или MD5); PEAP (TLS. MSCHAPv2. EA";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Zebra (США)";;;;;;;;;;;;;;;;;;;;"Windows Embedded CE 7.0; Windows Embedded Handheld 6.5.3; Android 4.4.4 (KitKat)";"512 Мбайт SRAM; 2 Гбайт Flash стандартный; 1 Гбайт SRAM; 2 Гбайт Flash премиум";"3.7"" разрешение VGA (480 x 640); цветной; Стекло Corning GorillaTM";"На 28. 43 и 53 клавиши; эмуляция терминала (5250. 3270. VT)";"Motorola SE965. Motorola SE1524-ER. Motorola SE4500. Motorola SE4750. Motorola SE4600. Motorola SE4500-DL. Motorola SE4500-HD";"1D: Code 39. Code 128. Code 93. Codabar. Code 11. Discrete 2 of 5. Interleaved 2 of 5. EAN-8. EAN-13. MSI. UPCA. UPCE. UPC/EAN supplementals. Coupon Code. Trioptic 39. Webcode. RSS-14. RSS Limited. RSS Expanded. Chinese 2 of 5";"Встроенная технология UHF Gen2 (только для моделей категории ""Премиум"")";"Датчики на базе технологии IST (только для моделей категории ""Премиум"")";"IP64 (корпус. закрывающий электронное оборудование. дисплей и клавиатура) согласно стандартам защиты IEC";"Устойчивость к падениям: Многократные падения на бетонное покрытие до 1.8 м в диапазоне рабочих температур; соответствует и превышает требования стандарта MIL-STD 810G;Устойчивость к циклическим падениям: 4000 падений с высоты 1 м; соответствует и превыша";"от -20 до +50°C";"231 x 91 x 193 мм";"57.5 мм";;;;;;; -"Товары для бизнеса";;"Принтер чеков-этикеток JPRT LQ258";;PVVD2525;68600.00;78700.00;4;;;;1;0;"Принтер чеков-этикеток JPRT LQ258";"Принтер чеков-этикеток JPRT LQ258, Товары для бизнеса";;"
  • Разрешение: 203 dpi
  • Система: Android 3.2.1
  • Память: 1 МБ SDRAM
  • Материал: термобумага
";;"okay_tech8.jpg, okay_tech9.jpg";pos-terminal-sam4s-titan-s-260;;;;;;;;;;;;;;;;;"OMAP4 (1 ГГц)";;;;;;;;;;;;;;"Stonestreet-стек: Bluetooth 4.0 плюс BLE или WBA; Microsoft-стек: Bluetooth. версия 2.1 с EDR";;;"Android 4.4.4 (KitKat): IEEE 802.11 a/b/g/n/d/h/i/k/r или Windows Embedded CE 7.0 / Windows Embedded Handheld 6.5.3: IEEE 802.11 a/b/g/n/d/h/i (Защита: WPA2 Enterprise. 802.1x; EAP-TLS; TTLS (CHAP. MS-CHAP. MS-CHAPv2. PAP или MD5); PEAP (TLS. MSCHAPv2. EA";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Zebra (США)";;;;;;;;;;;;;;;;;;;;"Windows Embedded CE 7.0; Windows Embedded Handheld 6.5.3; Android 4.4.4 (KitKat)";"512 Мбайт SRAM; 2 Гбайт Flash стандартный; 1 Гбайт SRAM; 2 Гбайт Flash премиум";"3.7"" разрешение VGA (480 x 640); цветной; Стекло Corning GorillaTM";"На 28. 43 и 53 клавиши; эмуляция терминала (5250. 3270. VT)";"Motorola SE965. Motorola SE1524-ER. Motorola SE4500. Motorola SE4750. Motorola SE4600. Motorola SE4500-DL. Motorola SE4500-HD";"1D: Code 39. Code 128. Code 93. Codabar. Code 11. Discrete 2 of 5. Interleaved 2 of 5. EAN-8. EAN-13. MSI. UPCA. UPCE. UPC/EAN supplementals. Coupon Code. Trioptic 39. Webcode. RSS-14. RSS Limited. RSS Expanded. Chinese 2 of 5";"Встроенная технология UHF Gen2 (только для моделей категории ""Премиум"")";"Датчики на базе технологии IST (только для моделей категории ""Премиум"")";"IP64 (корпус. закрывающий электронное оборудование. дисплей и клавиатура) согласно стандартам защиты IEC";"Устойчивость к падениям: Многократные падения на бетонное покрытие до 1.8 м в диапазоне рабочих температур; соответствует и превышает требования стандарта MIL-STD 810G;Устойчивость к циклическим падениям: 4000 падений с высоты 1 м; соответствует и превыша";"от -20 до +50°C";"231 x 91 x 193 мм";"57.5 мм";;;;;;; -"Товары для бизнеса";;"Сканер Jepod JP-M234";;JJPT0387;11600.00;13700.00;4;;;;1;0;"Сканер Jepod JP-M234";"Сканер Jepod JP-M234, Товары для бизнеса";;"
  • Сканер: лазерный
  • Система: Android 3.2.1
  • Память: 1 ГБайт Flash
  • Частота: 433МГц
";;"okay_tech6.jpg, okay_tech7.jpg";printer-chekov-etiketok-jprt-lq258;;;;;;;;;;;;;;;;;"OMAP4 (1 ГГц)";;;;;;;;;;;;;;"Stonestreet-стек: Bluetooth 4.0 плюс BLE или WBA; Microsoft-стек: Bluetooth. версия 2.1 с EDR";;;"Android 4.4.4 (KitKat): IEEE 802.11 a/b/g/n/d/h/i/k/r или Windows Embedded CE 7.0 / Windows Embedded Handheld 6.5.3: IEEE 802.11 a/b/g/n/d/h/i (Защита: WPA2 Enterprise. 802.1x; EAP-TLS; TTLS (CHAP. MS-CHAP. MS-CHAPv2. PAP или MD5); PEAP (TLS. MSCHAPv2. EA";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Zebra (США)";;;;;;;;;;;;;;;;;;;;"Windows Embedded CE 7.0; Windows Embedded Handheld 6.5.3; Android 4.4.4 (KitKat)";"512 Мбайт SRAM; 2 Гбайт Flash стандартный; 1 Гбайт SRAM; 2 Гбайт Flash премиум";"3.7"" разрешение VGA (480 x 640); цветной; Стекло Corning GorillaTM";"На 28. 43 и 53 клавиши; эмуляция терминала (5250. 3270. VT)";"Motorola SE965. Motorola SE1524-ER. Motorola SE4500. Motorola SE4750. Motorola SE4600. Motorola SE4500-DL. Motorola SE4500-HD";"1D: Code 39. Code 128. Code 93. Codabar. Code 11. Discrete 2 of 5. Interleaved 2 of 5. EAN-8. EAN-13. MSI. UPCA. UPCE. UPC/EAN supplementals. Coupon Code. Trioptic 39. Webcode. RSS-14. RSS Limited. RSS Expanded. Chinese 2 of 5";"Встроенная технология UHF Gen2 (только для моделей категории ""Премиум"")";"Датчики на базе технологии IST (только для моделей категории ""Премиум"")";"IP64 (корпус. закрывающий электронное оборудование. дисплей и клавиатура) согласно стандартам защиты IEC";"Устойчивость к падениям: Многократные падения на бетонное покрытие до 1.8 м в диапазоне рабочих температур; соответствует и превышает требования стандарта MIL-STD 810G;Устойчивость к циклическим падениям: 4000 падений с высоты 1 м; соответствует и превыша";"от -20 до +50°C";"231 x 91 x 193 мм";"57.5 мм";;;;;;; -"Товары для бизнеса";;"Сканер Jepod JP-M234";Чёерный;JJPT0887;11600.00;;4;;;;1;0;"Сканер Jepod JP-M234";"Сканер Jepod JP-M234, Товары для бизнеса";;"
  • Сканер: лазерный
  • Система: Android 3.2.1
  • Память: 1 ГБайт Flash
  • Частота: 433МГц
";;"okay_tech6.jpg, okay_tech7.jpg";printer-chekov-etiketok-jprt-lq258;;;;;;;;;;;;;;;;;"OMAP4 (1 ГГц)";;;;;;;;;;;;;;"Stonestreet-стек: Bluetooth 4.0 плюс BLE или WBA; Microsoft-стек: Bluetooth. версия 2.1 с EDR";;;"Android 4.4.4 (KitKat): IEEE 802.11 a/b/g/n/d/h/i/k/r или Windows Embedded CE 7.0 / Windows Embedded Handheld 6.5.3: IEEE 802.11 a/b/g/n/d/h/i (Защита: WPA2 Enterprise. 802.1x; EAP-TLS; TTLS (CHAP. MS-CHAP. MS-CHAPv2. PAP или MD5); PEAP (TLS. MSCHAPv2. EA";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Zebra (США)";;;;;;;;;;;;;;;;;;;;"Windows Embedded CE 7.0; Windows Embedded Handheld 6.5.3; Android 4.4.4 (KitKat)";"512 Мбайт SRAM; 2 Гбайт Flash стандартный; 1 Гбайт SRAM; 2 Гбайт Flash премиум";"3.7"" разрешение VGA (480 x 640); цветной; Стекло Corning GorillaTM";"На 28. 43 и 53 клавиши; эмуляция терминала (5250. 3270. VT)";"Motorola SE965. Motorola SE1524-ER. Motorola SE4500. Motorola SE4750. Motorola SE4600. Motorola SE4500-DL. Motorola SE4500-HD";"1D: Code 39. Code 128. Code 93. Codabar. Code 11. Discrete 2 of 5. Interleaved 2 of 5. EAN-8. EAN-13. MSI. UPCA. UPCE. UPC/EAN supplementals. Coupon Code. Trioptic 39. Webcode. RSS-14. RSS Limited. RSS Expanded. Chinese 2 of 5";"Встроенная технология UHF Gen2 (только для моделей категории ""Премиум"")";"Датчики на базе технологии IST (только для моделей категории ""Премиум"")";"IP64 (корпус. закрывающий электронное оборудование. дисплей и клавиатура) согласно стандартам защиты IEC";"Устойчивость к падениям: Многократные падения на бетонное покрытие до 1.8 м в диапазоне рабочих температур; соответствует и превышает требования стандарта MIL-STD 810G;Устойчивость к циклическим падениям: 4000 падений с высоты 1 м; соответствует и превыша";"от -20 до +50°C";"231 x 91 x 193 мм";"57.5 мм";;;;;;; -"Товары для бизнеса";;"ТСД Motorola (Zebra/Symbol) MC9200";;VVw445;7600.00;8700.00;4;;;;1;0;"ТСД Motorola (Zebra/Symbol) MC9200";"ТСД Motorola (Zebra/Symbol) MC9200, Товары для бизнеса";;"
  • Сканер: 2D Imager PRZM
  • Система: Android 4.4.4
  • Память: 2 ГБайт Flash
  • Интерфейс: USB
";;"okay_tech4.jpg, okay_tech5.jpg";skaner-jepod-jp-m234;;;;;;;;;;;;;;;;;"OMAP4 (1 ГГц)";;;;;;;;;;;;;;"Stonestreet-стек: Bluetooth 4.0 плюс BLE или WBA; Microsoft-стек: Bluetooth. версия 2.1 с EDR";;;"Android 4.4.4 (KitKat): IEEE 802.11 a/b/g/n/d/h/i/k/r или Windows Embedded CE 7.0 / Windows Embedded Handheld 6.5.3: IEEE 802.11 a/b/g/n/d/h/i (Защита: WPA2 Enterprise. 802.1x; EAP-TLS; TTLS (CHAP. MS-CHAP. MS-CHAPv2. PAP или MD5); PEAP (TLS. MSCHAPv2. EA";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Zebra (США)";;;;;;;;;;;;;;;;;;;;"Windows Embedded CE 7.0; Windows Embedded Handheld 6.5.3; Android 4.4.4 (KitKat)";"512 Мбайт SRAM; 2 Гбайт Flash стандартный; 1 Гбайт SRAM; 2 Гбайт Flash премиум";"3.7"" разрешение VGA (480 x 640); цветной; Стекло Corning GorillaTM";"На 28. 43 и 53 клавиши; эмуляция терминала (5250. 3270. VT)";"Motorola SE965. Motorola SE1524-ER. Motorola SE4500. Motorola SE4750. Motorola SE4600. Motorola SE4500-DL. Motorola SE4500-HD";"1D: Code 39. Code 128. Code 93. Codabar. Code 11. Discrete 2 of 5. Interleaved 2 of 5. EAN-8. EAN-13. MSI. UPCA. UPCE. UPC/EAN supplementals. Coupon Code. Trioptic 39. Webcode. RSS-14. RSS Limited. RSS Expanded. Chinese 2 of 5";"Встроенная технология UHF Gen2 (только для моделей категории ""Премиум"")";"Датчики на базе технологии IST (только для моделей категории ""Премиум"")";"IP64 (корпус. закрывающий электронное оборудование. дисплей и клавиатура) согласно стандартам защиты IEC";"Устойчивость к падениям: Многократные падения на бетонное покрытие до 1.8 м в диапазоне рабочих температур; соответствует и превышает требования стандарта MIL-STD 810G;Устойчивость к циклическим падениям: 4000 падений с высоты 1 м; соответствует и превыша";"от -20 до +50°C";"231 x 91 x 193 мм";;;;;;;; -"Товары для бизнеса";;"ТСД Motorola (Zebra/Symbol) MC9200";;VVw4632;57000.00;;4;;;;1;;"ТСД Motorola (Zebra/Symbol) MC9200";"ТСД Motorola (Zebra/Symbol) MC9200, Товары для бизнеса";;"
  • Сканер: 2D Imager PRZM
  • Операционная система: Android 4.4.4
  • Память: 2 ГБайт Flash
  • Интерфейс подключения: USB
";;"okay_tech1.jpg, okay_tech2.jpg, okay_tech3.jpg";tsd-motorola-zebrasymbol-mc9200;;;;;;;;;;;;;;;;;"OMAP4 (1 ГГц)";;;;;;;;;;;;;;"Stonestreet-стек: Bluetooth 4.0 плюс BLE или WBA; Microsoft-стек: Bluetooth. версия 2.1 с EDR";;;"Android 4.4.4 (KitKat): IEEE 802.11 a/b/g/n/d/h/i/k/r или Windows Embedded CE 7.0 / Windows Embedded Handheld 6.5.3: IEEE 802.11 a/b/g/n/d/h/i (Защита: WPA2 Enterprise. 802.1x; EAP-TLS; TTLS (CHAP. MS-CHAP. MS-CHAPv2. PAP или MD5); PEAP (TLS. MSCHAPv2. EA";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"Zebra (США)";;;;;;;;;;;;;;;;;;;;"Windows Embedded CE 7.0; Windows Embedded Handheld 6.5.3; Android 4.4.4 (KitKat)";"512 Мбайт SRAM; 2 Гбайт Flash стандартный; 1 Гбайт SRAM; 2 Гбайт Flash премиум";"3.7"" разрешение VGA (480 x 640); цветной; Стекло Corning GorillaTM";"На 28. 43 и 53 клавиши; эмуляция терминала (5250. 3270. VT)";"Motorola SE965. Motorola SE1524-ER. Motorola SE4500. Motorola SE4750. Motorola SE4600. Motorola SE4500-DL. Motorola SE4500-HD";"1D: Code 39. Code 128. Code 93. Codabar. Code 11. Discrete 2 of 5. Interleaved 2 of 5. EAN-8. EAN-13. MSI. UPCA. UPCE. UPC/EAN supplementals. Coupon Code. Trioptic 39. Webcode. RSS-14. RSS Limited. RSS Expanded. Chinese 2 of 5";"Встроенная технология UHF Gen2 (только для моделей категории ""Премиум"")";"Датчики на базе технологии IST (только для моделей категории ""Премиум"")";"IP64 (корпус. закрывающий электронное оборудование. дисплей и клавиатура) согласно стандартам защиты IEC";"Устойчивость к падениям: Многократные падения на бетонное покрытие до 1.8 м в диапазоне рабочих температур; соответствует и превышает требования стандарта MIL-STD 810G;Устойчивость к циклическим падениям: 4000 падений с высоты 1 м; соответствует и превыша";"от -20 до +50°C";"231 x 91 x 193 мм";;;;;;;; -"Товары для спорта";Haibike;"Складной велосипед Forward Tracer 1.0 2018";"17 дюймов — 160-175 см";;14248.00;17530.00;4;;;;1;;"Складной велосипед Forward Tracer 1.0 2018";"Складной велосипед Forward Tracer 1.0 2018, Haibike, Товары для спорта";"Forward Tracer 1.0 станет настоящим подарком для поклонников активного времяпровождения и спортивных велозаездов. 6-скоростная трансмиссия переключается с помощью удобных манеток Shimano Tourney RS35 и дает райдеру полный контроль над движением. Система ободных тормозов Promax TX-119L сделает каждую остановку плавной и своевременной. Материалом для изготовления рамы послужил легкий, но устойчивый к деформации алюминий, а амортизационная вилка велосипеда выполнена из стали.";"
  • пол: мужской
  • тип рамы: хардтейл
  • цепь: KMC HV408
  • седло: FWD MTB
";"

Forward Tracer 1.0 станет настоящим подарком для поклонников активного времяпровождения и спортивных велозаездов. 6-скоростная трансмиссия переключается с помощью удобных манеток Shimano Tourney RS35 и дает райдеру полный контроль над движением. Система ободных тормозов Promax TX-119L сделает каждую остановку плавной и своевременной. Материалом для изготовления рамы послужил легкий, но устойчивый к деформации алюминий, а амортизационная вилка велосипеда выполнена из стали.

";"okay_sport5.jpg, okay_sport1.png, okay_sport2.png, okay_sport3.png, okay_sport4.png, okay_sport6.jpg";skladnoj-velosiped-forward-tracer-10-2018;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велосипед;"26.5 дюйма";"от 110 до 160 мм";"Жесткая стальная";"алюминий,,алюминий PRO";;"без амортизатора";ободной;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Экшн-камера ThiEYE T5 Edge";;;8600.00;9200.00;4;;;;1;0;"Экшн-камера ThiEYE T5 Edge";"Экшн-камера ThiEYE T5 Edge, Haibike, Товары для спорта";"Голосовое дистанционное управление (поддерживается только английское произношение, встроенная 1 x 3 В CR1632 батарея): прозвучат простые голосовые команды на пульте дистанционного управления или нажмите кнопку на пульте дистанционного управления, и вы можете управлять своей камерой. Удивительная технология голосовых команд дает вам свободный контроль над вашим T5 Edge. Родное видео с разрешением 4K 30 кадров в секунду: запись видео высокой четкости с разрешением 4K 30 кадров в секунду и захват действия боле";"
  • Матрица: CMOS
  • Диагональ: 2 дюйма
  • Формат: AVC (H.264/MPEG-4), MP4
";"

Голосовое дистанционное управление (поддерживается только английское произношение, встроенная 1 x 3 В CR1632 батарея): прозвучат простые голосовые команды на пульте дистанционного управления или нажмите кнопку на пульте дистанционного управления, и вы можете управлять своей камерой. Удивительная технология голосовых команд дает вам свободный контроль над вашим T5 Edge. Родное видео с разрешением 4K 30 кадров в секунду: запись видео высокой четкости с разрешением 4K 30 кадров в секунду и захват действия более подробно. Встроенный 2-дюймовый экран ульта HD IPS: больше, чем предыдущее поколение, для легкой настройки камеры, просмотра и воспроизведения видео и фотографий. 14 Mп фото, различные режимы фото: одиночная съемка, автоспуск, фотосъемка, фотоснимок и длительная экспозиция. Стабилизатор гироскопа (EIS при 4K и 1080P): минимизировать размытие и компенсировать дрожание устройства, чтобы обеспечить четкие и стабильные изображения. Поворотные крепления на 360 градусов и резьбовая вставка 1/4 дюйма. Уникальный поворот на 360 градусов позволяет вам регулировать различные углы съемки, а резьбовая вставка 1/4 дюйма автоматически соединяется с штативом или самоуправляемой рукояткой. Коррекция искажений: уменьшите или устраните искажения, чтобы получить более естественный эффект съемки. Длительное воздействие: вы можете записывать орбиты съемок, водопады, водителя автомобиля из-за продления времени экспозиции. Водонепроницаемость до 60 м: оснащен профессиональным водонепроницаемым корпусом IP68, водонепроницаемый T5 Edge до 60 м. APP для дистанционного управления, редактирования и совместного использования: Подключите устройства Android или iOS к камере действия через ThiEYE Cam APP (затем загрузите с официального сайта http://www.thieye.com), затем вы можете настроить камеру, просматривать, редактировать и делиться своими видео и фотографиями.

";"okay_sport22.jpg, okay_sport21.jpeg, okay_sport23.jpeg, okay_sport24.jpeg";ekshn-kamera-thieye-t34-edge;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Экшн-камера;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Экшн-камера ThiEYE T5 Edge";;;9800.00;10200.00;4;;;;1;0;"Экшн-камера ThiEYE T5 Edge";"Экшн-камера ThiEYE T5 Edge, Haibike, Товары для спорта";"Голосовое дистанционное управление (поддерживается только английское произношение, встроенная 1 x 3 В CR1632 батарея): прозвучат простые голосовые команды на пульте дистанционного управления или нажмите кнопку на пульте дистанционного управления, и вы можете управлять своей камерой. Удивительная технология голосовых команд дает вам свободный контроль над вашим T5 Edge. Родное видео с разрешением 4K 30 кадров в секунду: запись видео высокой четкости с разрешением 4K 30 кадров в секунду и захват действия боле";"
  • Матрица: CMOS
  • Диагональ: 2 дюйма
  • Формат: AVC (H.264/MPEG-4), MP4
";"

Голосовое дистанционное управление (поддерживается только английское произношение, встроенная 1 x 3 В CR1632 батарея): прозвучат простые голосовые команды на пульте дистанционного управления или нажмите кнопку на пульте дистанционного управления, и вы можете управлять своей камерой. Удивительная технология голосовых команд дает вам свободный контроль над вашим T5 Edge. Родное видео с разрешением 4K 30 кадров в секунду: запись видео высокой четкости с разрешением 4K 30 кадров в секунду и захват действия более подробно. Встроенный 2-дюймовый экран ульта HD IPS: больше, чем предыдущее поколение, для легкой настройки камеры, просмотра и воспроизведения видео и фотографий. 14 Mп фото, различные режимы фото: одиночная съемка, автоспуск, фотосъемка, фотоснимок и длительная экспозиция. Стабилизатор гироскопа (EIS при 4K и 1080P): минимизировать размытие и компенсировать дрожание устройства, чтобы обеспечить четкие и стабильные изображения. Поворотные крепления на 360 градусов и резьбовая вставка 1/4 дюйма. Уникальный поворот на 360 градусов позволяет вам регулировать различные углы съемки, а резьбовая вставка 1/4 дюйма автоматически соединяется с штативом или самоуправляемой рукояткой. Коррекция искажений: уменьшите или устраните искажения, чтобы получить более естественный эффект съемки. Длительное воздействие: вы можете записывать орбиты съемок, водопады, водителя автомобиля из-за продления времени экспозиции. Водонепроницаемость до 60 м: оснащен профессиональным водонепроницаемым корпусом IP68, водонепроницаемый T5 Edge до 60 м. APP для дистанционного управления, редактирования и совместного использования: Подключите устройства Android или iOS к камере действия через ThiEYE Cam APP (затем загрузите с официального сайта http://www.thieye.com), затем вы можете настроить камеру, просматривать, редактировать и делиться своими видео и фотографиями.

";"okay_sport21.jpeg, okay_sport22.jpg, okay_sport23.jpeg, okay_sport24.jpeg";ekshn-kamera-thieye-t5-edge;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Экшн-камера;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Фитнесс-трекер SOUSHTRACK IFaT ACTIVE 3 В 1";;sssr3425;78000.00;;4;;;;1;1;"Фитнесс-трекер SOUSHTRACK IFaT ACTIVE 3 В 1";"Фитнесс-трекер SOUSHTRACK IFaT ACTIVE 3 В 1, Haibike, Товары для спорта";"Фитнес-трекер NordicTrack iFit Active 3 в 1 – очень полезный умный гаджет людям живущим активной спортивной жизнью. Данный умный девайс поможет Вам упорядочить свои спортивные нагрузки, а также проанализировать полученные результаты с помощью устройств с ОС IOS или Android . Данное устройство поможет вам подсчитать съеденные калории, благодаря встроенной библиотеке блюд, что поможет вам построить правильную диету во время тренировочных нагрузок, кроме этого устройство умеет считывать параметры ";"
  • Цвет: черный
  • Тип: smart часы
  • Разъём: microUSB
  • Гарантия: 12 мес
";"

Фитнес-трекер NordicTrack iFit Active 3 в 1 – очень полезный умный гаджет людям живущим активной спортивной жизнью. Данный умный девайс поможет Вам упорядочить свои спортивные нагрузки, а также проанализировать полученные результаты с помощью устройств с ОС IOS или Android.

Данное устройство поможет вам подсчитать съеденные калории, благодаря встроенной библиотеке блюд, что поможет вам построить правильную диету во время тренировочных нагрузок, кроме этого устройство умеет считывать параметры вашего организма во время сна, и подбирает самое удачное время для вашего пробуждения. Еще одним достоинством является сопряжение устройства с системой Fit Active, где вы можете общаться и обмениваться своими результатами с другими пользователями.

Особенности:

- яркий экран;

- полная совместимость с устройствами IOS или Android;

- отображение количества пройдённых  шагов, дистанции, сколько было сожжено калорий, время и качества сна

NordicTrack – Американский бренд, составная часть группы компаний  ICON Health&Fitness, производитель с 40 летней репутацией, и огромнейшим количеством моделей. Больше 35 лет этот бренд известен своим оборудованием предназаначеным для оздоровления и фитнеса. На сегодня  компания не останавливается на достигнутых результатах, постоянно исследуя и применяя инновации в своей продукции. Простота конструкций, эффективность, качество и уникальные решения в проектировке – быстро завоевали симпатии клиентов и принесли компании узнаваемость и признание во всем мире.

";okay_sport20.jpg;fitness-treker-soushtrack-ifat-active-3-v-1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Фитнесс-трекер;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Гироборд 56GTA 102 10” POWER";;as513626;4800.00;;4;;;;1;;"Гироборд 56GTA 102 10” POWER";"Гироборд 56GTA 102 10” POWER, Haibike, Товары для спорта";"Гироборд 2Е HB 102 10” Power, серо-красный (2E-HB102-10P-GR) - совpеменное решение для пeредвижения по городу или прогулок. Гироскутер оборудован 10"" колесами с шиpокими покрышками, кoторые обеспечивают отличнyю проходимoсть. Заряда батареи хватает на 16 чаcов активной pаботы. Прорезиненные подножки обеспечивают отличное сцепление с обувью и устойчивость. Персональный транспорт это oтличная альтернатива pоликам или скейту. Также модель оборудована LED-диcплеем с подсвeткой. Особенности : пр";"
  • Цвет: красный
  • Запас хода: 30км
  • Вес: 13.8кг
  • Диаметр: 10дюймов
";"

Гироборд 2Е HB 102 10” Power, серо-красный (2E-HB102-10P-GR) - совpеменное решение для пeредвижения по городу или прогулок. Гироскутер оборудован 10"" колесами с шиpокими покрышками, кoторые обеспечивают отличнyю проходимoсть. Заряда батареи хватает на 16 чаcов активной pаботы. Прорезиненные подножки обеспечивают отличное сцепление с обувью и устойчивость. Персональный транспорт это oтличная альтернатива pоликам или скейту. Также модель оборудована LED-диcплеем с подсвeткой.

Особенности:

  • прочная конструкция;
  • стильный дизайн;
  • широкие колеса;
  • отличная проходимость;
  • мощная батарея;
  • нескользящие подножки;
  • автобалансировка;
  • LED-дисплей.

Характеристики:
 Тип: гиpоборд;
 Скорость мах: 12 км/ч;
 Диаметр колес: 10"";
 Мощноcть: 700 Вт (2 x 350 Вт);
 Батарея: Li-ion 158 Вт*ч;
 Длительность работы: до 16 ч;
 Время полоной зарядки: 130 мин;
 Размер: 73 х 28 х 30 см;
 Веc: 13,5 кг;
 Цвет: серый, красный;
 Гаpантия: 12 меcяцев.

";"okay_sport18.jpg, okay_sport19.jpg";girobord-56gta-102-10-power;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Гироборд;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Гироскутер NINEDROP BY SEGWAY 34, БЕЛЫЙ";;ff5211;16800.00;;4;;;;1;;"Гироскутер NINEDROP BY SEGWAY 34, БЕЛЫЙ";"Гироскутер NINEDROP BY SEGWAY 34, БЕЛЫЙ, Haibike, Товары для спорта";"Гироскутер Ninebot by Segway MiniPro 320, белый - невероятно удобная и простая в эксплуатации модель. Эргономичный дизайн и умные датчики облегчают поездку и делают ее безопасней. Рулевая ручка регулируется по высоте под пользователя, а также используется для транспортировки. Защита IP54 позволит передвигаться во время дождя или же по пыльной дороге. Гироскутер идеально приспособлен к городским условиям, так как инженеры оснастили его алгоритмом динамического питания, который определяет наклоны, препятстви";"
  • Цвет: белый
  • Запас хода: 30км
  • Вес: 3.8кг
  • Диаметр: 10 дюёмов
";"

Гироскутер Ninebot by Segway MiniPro 320, белый - невероятно удобная и простая в эксплуатации модель. Эргономичный дизайн и умные датчики облегчают поездку и делают ее безопасней. Рулевая ручка регулируется по высоте под пользователя, а также используется для транспортировки. Защита IP54 позволит передвигаться во время дождя или же по пыльной дороге. Гироскутер идеально приспособлен к городским условиям, так как инженеры оснастили его алгоритмом динамического питания, который определяет наклоны, препятствия и автоматически регулирует мощность. Персональный транспорт может быть синхронизирован со смартфоном для удобного дистанционного управления, определения динамики движения и диагностики неполадок. LED-подсветка выполняет декоративную функцию и сигнализирует о движении: синий - движение вперед, мигание желтого - поворот, красный - торможение. Рама выполнена из мaгниевого сплава, кoторый обеспечивaет прочность конcтрукции и имеет небoльшой вес. Компактные размеры позволяют с легкостью транспортировать гироскутер в общественном транспорте или же в багажнике автомобиля.

 

Особенности:

  • стильный дизайн;
  • увеличенные колеса;
  • LED-подсветка;
  • цветовая сигнализация о движении;
  • система идентификации;
  • безопасное передвижение;
  • ручка для управления и переноски;
  • синхронизация со смартфоном;
  • мобильное приложение;
  • дистанционное управление;
  • пыле- и влагозащитный корпус;
  • длительная работа на одном заряде;
  • мощный электромотор;
  • удлинитель для подкачки колес;
  • компактные размеры;
  • прочный корпус.

 

Характеристики:
 Тип: гироскутер;
 Возрастная категория: 16 - 60 лет;
 Рекомендyемый рост: 120 - 200 см;
 Диаметр кoлес: 10,5"";
 Скорость мах: 18 км/ч;
 Угoл нaклoна: до 15 градусов;
 Мощность: 1600 Вт;
 Батарея: Li-ion 310 Вт-ч;
 Запас хoда: до 30 км;
 Врeмя полоной зарядки: 4 ч;
 Нагрузка: до 100 кг;
 Защита: IP54;
 Беспроводная связь: Bluetоoth;
 Размeр: 54,6x26,2x61,1-86,6 см;
 Вeс: 12,8 кг;
 Цвeт: белый;
 Гарaнтия: 12 месяцев.

";"okay_sport16.jpg, okay_sport17.jpg";giroskuter-ninedrop-by-segway-34-belyj;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Гироскутер;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"ТРЕНАЖЕР ДЛЯ МЫШЦ ПЛЕЧА MARBO-SPORT 348";;nir5225;79450.00;120000.00;4;;;;1;1;"ТРЕНАЖЕР ДЛЯ МЫШЦ ПЛЕЧА MARBO-SPORT 348";"ТРЕНАЖЕР ДЛЯ МЫШЦ ПЛЕЧА MARBO-SPORT 348, Haibike, Товары для спорта";"Тренажер для мышц плеча Marbo-Sport MF-U007 (MF-U007-k1t4) – высококачественное, эффективное оборудование, предназначенное для проработки рук и плечевого пояса. Нагрузка на данной моделе основана на свободном весе. Конструкция тренажера изготовлена из толстостенного стального профиля, обеспечивающего надежность и безопасность. В качестве покрытия использована качественная порошковая краска, защищающая раму от коррозии. Регулировка высоты сиденья осуществляется быстро и легко при помощи специальных пр";"
  • Цвет: черный
  • Назначение: руки, плечи
  • Габариты: 139x140x147
";"

Тренажер для мышц плеча Marbo-Sport MF-U007 (MF-U007-k1t4) – высококачественное, эффективное оборудование, предназначенное для проработки рук и плечевого пояса. Нагрузка на данной моделе основана на свободном весе. Конструкция тренажера изготовлена из толстостенного стального профиля, обеспечивающего надежность и безопасность. В качестве покрытия использована качественная порошковая краска, защищающая раму от коррозии. Регулировка высоты сиденья осуществляется быстро и легко при помощи специальных приводов. Для расположения олимпийских блинов, на оборудовании предусмотрены специальные посадочные места, которые покрыты полиамидом, препятствующим появление царапин, а также снижающие уровень шума при касании инвентаря о втулки. Благодаря трехступенчатой регулировке угла наклона спинки, спортсмен может увеличивать или уменьшать нагрузку. Ручки, имеют покрытие из прочного, противоскользящего вспененного полимера, гарантирующего качественное сцепление. Данное оборудование максимально устойчиво даже на не совсем ровной поверхности благодаря специальным компенсаторам неровности пола и резиновым накладкам на ножках. Тренажер прошел необходимое тестирование и имеет сертификат безопасности Европейского центра качества.

Особенности тренажера:

  • силовой, на свободных весах;
  • для коммерческого использования;
  • оптимальное удобство и эффективность;
  • безопасность при использовании;
  • для проработки мышц рук и плечевого пояса;
  • конструкция из высокопрочного стального профиля и цельнокроеных металлических пластин;
  • обивка из высокотехнологичной синтетической кожи;
  • резиновые накладки на ножках. 

Характеристики:
 Тип: силовой тренажер;
 Вид: для горизонтального жима;
 Нагрузка: на свободных весах;
 Назначение: руки, плечи;
 Профили: 100х50x3 мм;
 Общий вес тренажера: 121 кг;
 Нагрузка: до 400 кг;
 Габариты: 139х140х147 см.

Производитель: Marbo-Sport (Польша).
Торговая марка Marbo-Sport была основана более 30 лет назад в Польше, городе Страховице. Компания специализируется на производстве высококачественного спортивного оборудования. Ассортимент товаров бренда представлен стойками, скамьями, силовыми станциями, многофункциональными турниками, мультистанциями и многими другими тренажерами и аксессуарами к ним. Продукция Marbo-Sport отличается высоким качеством, надежностью и безопасностью, проходит обязательное тестирование и имеет длительную гарантию.

";"okay_sport13.jpg, okay_sport15.jpg, okay_sport14.jpg";trenazher-dlya-myshts-plecha-marbo-sport-348;;"139 кг";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Тренажер;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Велотренажер HouseFit";;uu65222;2389.00;2780.00;4;;;;1;;"Велотренажер HouseFit";"Велотренажер HouseFit, Haibike, Товары для спорта";"Велотренажер HSF 8012. Механическая бесступенчатая регулируемая система нагрузки. Корпус с полированным лакокрасочным покрытием. Очень легкая и компактная конструкция. Регулируемое по высоте сиденье. Ножки с регулировкой для неровного пола. Компьютер: функции – скорость, потраченные калории, время, расстояние. В наличии цвета: оранжевый, синий.";"
  • Страна: США
  • Питание: батарея
  • Тип: классика
";"

Велотренажер HSF 8012. Механическая бесступенчатая регулируемая система нагрузки. Корпус с полированным лакокрасочным покрытием. Очень легкая и компактная конструкция. Регулируемое по высоте сиденье. Ножки с регулировкой для неровного пола. Компьютер: функции – скорость, потраченные калории, время, расстояние. В наличии цвета: оранжевый, синий.

";"okay_sport7.jpg, okay_sport8.jpg, okay_sport9.jpg";velotrenazher-housefit;;13.5кг.,,136.5кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велотренажер;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Гантели Lifemaxx 8 кг";;sd6784;1945.00;;4;;;;1;;"Гантели Lifemaxx 8 кг";"Гантели Lifemaxx 8 кг, Haibike, Товары для спорта";"Гантели – широко известный универсальный снаряд с давней историей. Предназначена она для увеличения мышечной массы и выполнения силовых упражнений. Тренировки с гирей позволяют накачать бицепс, трицепс, мышцы спины, брюшного пресса, ягодичные мышцы. Имея дома один такой универсальный снаряд, вы можете обеспечить себя полноценной физической нагрузкой. Преимущество резиновых гирь с хромированной ручкой - нет необходимости оборудования страховочной поверхности на полу. Гири в резиновой оболочке прекрасно";;"

Гантели – широко известный универсальный снаряд с давней историей. Предназначена она для увеличения мышечной массы и выполнения силовых упражнений. Тренировки с гирей позволяют накачать бицепс, трицепс, мышцы спины, брюшного пресса, ягодичные мышцы. Имея дома один такой универсальный снаряд, вы можете обеспечить себя полноценной физической нагрузкой. Преимущество резиновых гирь с хромированной ручкой - нет необходимости оборудования страховочной поверхности на полу. Гири в резиновой оболочке прекрасно подходят для занятий как в спортивном зале, так и дома. Выбирать гирю необходимо в соответствии со своей физической формой и весом.

";"okay_sport8.jpg, okay_sport9.jpg, okay_sport12.jpg";ganteli-lifemaxx-8-kg;;"8 кг";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Гантели;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Гиря Hop-Sport 6 кг";;HD902;670.00;790.00;4;;;;1;;"Гиря Hop-Sport 6 кг";"Гиря Hop-Sport 6 кг, Haibike, Товары для спорта";"Физические нагрузки, которые вы получите с помощью гирь, наиболее оптимальны для вашего организма. Гири спроектированы специальным образом, имеют удобную ручку для хвата. Характеристики: обхват – 50 см; объем ручки – 10 см; покрытие – винил; наполнение – бетон. Также занятия с гирями доставят удовольствие женщинам, так как помогут сбросить вес и обрести подтянутую фигуру.";;"

Физические нагрузки, которые вы получите с помощью гирь, наиболее оптимальны для вашего организма. Гири спроектированы специальным образом, имеют удобную ручку для хвата. Характеристики: обхват – 50 см; объем ручки – 10 см; покрытие – винил; наполнение – бетон. Также занятия с гирями доставят удовольствие женщинам, так как помогут сбросить вес и обрести подтянутую фигуру.

";"okay_sport12.jpg, okay_sport7.jpg, okay_sport8.jpg, okay_sport9.jpg, okay_sport10.jpg, okay_sport11.jpg";girya-hop-sport-6-kg;;"6 кг";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Гиря;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Велообувь Shimano SH-RP501 Men";;;9480.00;;4;;;;1;;"Велообувь Shimano SH-RP501 Men";"Велообувь Shimano SH-RP501 Men, Haibike, Товары для спорта";;;;"okay_sport3.png, okay_sport1.png, okay_sport2.png, okay_sport4.png";veloobuv-shimano-sh-rp501-men;;"332 г";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велообувь;;;нет;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Велоперчатка BBB BBW-54 LiteZone";;awe352;2290.00;;4;;;;1;;"Велоперчатка BBB BBW-54 LiteZone";"Велоперчатка BBB BBW-54 LiteZone, Haibike, Товары для спорта";"Вентилируемая перчатка из практичных материалов. Велоперчатки LiteZone BBW-54 обеспечивают надежный крепкий хват во время велопрогулки и тренировки. Надежное удержание руля повышает уверенность велосипедиста на дороге. Ладонь дополнена силиконовыми вставками, большой и указательный пальцы дополнительно защищены. Руки в перчатках хорошо вентилируются.";;"

Вентилируемая перчатка из практичных материалов. Велоперчатки LiteZone BBW-54 обеспечивают надежный крепкий хват во время велопрогулки и тренировки. Надежное удержание руля повышает уверенность велосипедиста на дороге. Ладонь дополнена силиконовыми вставками, большой и указательный пальцы дополнительно защищены. Руки в перчатках хорошо вентилируются.

";"okay_sport4.png, okay_sport1.png, okay_sport2.png, okay_sport3.png";veloperchatka-bbb-bbw-54-litezone;;"149 г";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велоперчатка;нет;нет;нет;;;;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Велошлем Scott Arx";Белый;qw5934;7600.00;7800.00;4;;;;1;;"Велошлем Scott Arx";"Велошлем Scott Arx, Haibike, Товары для спорта";"Велошлем с усиленной защитой и системой подгонки MRAS2. Scott ARX – велосипедный шлем с технологией in-mold: при падении шлем не раскалывается, а сохраняет цельность. Плотно облегает, хорошо защищает голову. Крупные отверстия создают равномерную вентиляцию кожи во время поездки. Легкий (240 г), прочный шлем подходит для гонок и тренировок. Система подгонки позволяет одной рукой с помощью небольшого колесика отрегулировать посадку, не прерывая движение.";;"

Велошлем с усиленной защитой и системой подгонки MRAS2. Scott ARX – велосипедный шлем с технологией in-mold: при падении шлем не раскалывается, а сохраняет цельность. Плотно облегает, хорошо защищает голову. Крупные отверстия создают равномерную вентиляцию кожи во время поездки. Легкий (240 г), прочный шлем подходит для гонок и тренировок. Система подгонки позволяет одной рукой с помощью небольшого колесика отрегулировать посадку, не прерывая движение.

";"okay_sport2.png, okay_sport1.png, okay_sport3.png, okay_sport4.png";veloshlem-scott-arx;;"138 г,,793 г";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велошлем;нет;нет;нет;нет;нет;нет;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Велошлем Scott Arx";Красный;qw735h64;7.00;;4;;;;1;;"Велошлем Scott Arx";"Велошлем Scott Arx, Haibike, Товары для спорта";"Велошлем с усиленной защитой и системой подгонки MRAS2. Scott ARX – велосипедный шлем с технологией in-mold: при падении шлем не раскалывается, а сохраняет цельность. Плотно облегает, хорошо защищает голову. Крупные отверстия создают равномерную вентиляцию кожи во время поездки. Легкий (240 г), прочный шлем подходит для гонок и тренировок. Система подгонки позволяет одной рукой с помощью небольшого колесика отрегулировать посадку, не прерывая движение.";;"

Велошлем с усиленной защитой и системой подгонки MRAS2. Scott ARX – велосипедный шлем с технологией in-mold: при падении шлем не раскалывается, а сохраняет цельность. Плотно облегает, хорошо защищает голову. Крупные отверстия создают равномерную вентиляцию кожи во время поездки. Легкий (240 г), прочный шлем подходит для гонок и тренировок. Система подгонки позволяет одной рукой с помощью небольшого колесика отрегулировать посадку, не прерывая движение.

";"okay_sport2.png, okay_sport1.png, okay_sport3.png, okay_sport4.png";veloshlem-scott-arx;;"138 г,,793 г";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велошлем;нет;нет;нет;нет;нет;нет;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"16 дюймов — 155-166 см";er45373;389000.00;;4;;;;1;0;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019, Haibike, Товары для спорта";"Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторам";;"

Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторами, которые позволят настроить байк под любую трассу. За переключение передач отвечает оборудование Sram NX Eagle.

";"okay_sport1.png, okay_sport6.jpg, okay_sport2.png, okay_sport3.png, okay_sport4.png";elektrovelosiped-haibike-sduro-fullseven-90-i500wh-12-g-nx-2018;;"24.8 кг";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велосипед;"27.5 дюйма";"от 100 до 150 мм";"RockShox Revelation RC Lockout. air. Travel: 120mm. aluminium steerer tube 1 18 - 1 12 tapered. thru-axle Boost";алюминий;"Sram NX Eagle. 12 speed";;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"16 дюймов — 155-166 см";er45373;469000.00;;4;;;;1;1;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019, Haibike, Товары для спорта";"Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторам";;"

Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторами, которые позволят настроить байк под любую трассу. За переключение передач отвечает оборудование Sram NX Eagle.

";"okay_sport6.jpg, okay_sport1.png, okay_sport2.png, okay_sport3.png, okay_sport4.png";elektrovelosiped-haibike-sduro-fullseven-90-i500wh-12-g-nx-2019;;"24.8 кг";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велосипед;"27.5 дюйма";"от 100 до 150 мм";"RockShox Revelation RC Lockout. air. Travel: 120mm. aluminium steerer tube 1 18 - 1 12 tapered. thru-axle Boost";алюминий;"Sram NX Eagle. 12 speed";;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"18 дюймов — 165-180 см";er453753;479000.00;;4;;;;1;1;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019, Haibike, Товары для спорта";"Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторам";;"

Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторами, которые позволят настроить байк под любую трассу. За переключение передач отвечает оборудование Sram NX Eagle.

";"okay_sport6.jpg, okay_sport1.png, okay_sport2.png, okay_sport3.png, okay_sport4.png";elektrovelosiped-haibike-sduro-fullseven-90-i500wh-12-g-nx-2019;;"24.8 кг";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велосипед;"27.5 дюйма";"от 100 до 150 мм";"RockShox Revelation RC Lockout. air. Travel: 120mm. aluminium steerer tube 1 18 - 1 12 tapered. thru-axle Boost";алюминий;"Sram NX Eagle. 12 speed";;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"20 дюймов — 175-190 см";er456;469000.00;489000.00;4;;;;1;1;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019, Haibike, Товары для спорта";"Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторам";;"

Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторами, которые позволят настроить байк под любую трассу. За переключение передач отвечает оборудование Sram NX Eagle.

";"okay_sport6.jpg, okay_sport1.png, okay_sport2.png, okay_sport3.png, okay_sport4.png";elektrovelosiped-haibike-sduro-fullseven-90-i500wh-12-g-nx-2019;;"24.8 кг";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велосипед;"27.5 дюйма";"от 100 до 150 мм";"RockShox Revelation RC Lockout. air. Travel: 120mm. aluminium steerer tube 1 18 - 1 12 tapered. thru-axle Boost";алюминий;"Sram NX Eagle. 12 speed";;;;;;;;;;;;;;;;;;;;;; -"Товары для спорта";Haibike;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"22 дюйма — 187-200 см";er45354;499000.00;;4;;;;1;1;"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019";"Электровелосипед Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019, Haibike, Товары для спорта";"Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторам";;"

Модель Haibike SDURO FullSeven 9.0 i500Wh 12-G NX 2019 предназначена как для агрессивного катания по лесным трассам, так и для неторопливых прогулок по живописным маршрутам. Основная особенность модели - мощный электромотор, который поможет без труда заехать в сложный подъем или преодолеть длительный трансфер от одной трассы к другой. Рама велосипед изготовлена из алюминиевого сплава с применением технологии гидроформинга, что позволило сделать велосипед легким и прочным. Модель оснащена двумя амортизаторами, которые позволят настроить байк под любую трассу. За переключение передач отвечает оборудование Sram NX Eagle.

";"okay_sport6.jpg, okay_sport1.png, okay_sport2.png, okay_sport3.png, okay_sport4.png";elektrovelosiped-haibike-sduro-fullseven-90-i500wh-12-g-nx-2019;;"24.8 кг";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Велосипед;"27.5 дюйма";"от 100 до 150 мм";"RockShox Revelation RC Lockout. air. Travel: 120mm. aluminium steerer tube 1 18 - 1 12 tapered. thru-axle Boost";алюминий;"Sram NX Eagle. 12 speed";;;;;;;;;;;;;;;;;;;;;; -"Книги и канцтовары";;"Ловец снов Стивен Кинг";;book36;1180.00;;4;;;;1;;"Ловец снов Стивен Кинг";"Ловец снов Стивен Кинг, Книги и канцтовары";"Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже. 1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар): После добавления файла - сохраните товар. 2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после ";"
  • Автор: Стивен Кинг
  • Страниц: 672
  • Год издания: 2016
";"

Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже.

1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар):

После добавления файла - сохраните товар.

2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после оплаты заказа.

Письмо клиенту до оплаты:

Письмо клиенту после оплаты заказа:

3. Получение клиентом заказанного товара:

Тоже самое можно проделать с любым электронным файлом.

  • Если вы продаете музыкальные композиции - загрузите в описание к товару кусочек своего трека.
  • Если вы занимаетесь продажей видеороликов - добавьте в качестве фотографий товара несколько скриншотов своего видео. 
  • Возможно вы продаете ценную информацию, например, книги? Несколько абзацев в описании к товару заинтересуют покупателя.

Пробуйте и у вас получится.

";"okay-book8.jpg, okay-book3.jpg, okay-book4.jpg, okay-book5.jpg, okay-book6.jpg";protivostoyanie-tom-1-stiven-king;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"С. Кінг";украинский;"The Outsider";переплет;592;ч/б;2019;"Фантастика. фэнтези";;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Набор меосметических кистей Bellapierre";;;143.00;;4;;;;1;;"Набор меосметических кистей Bellapierre";"Набор меосметических кистей Bellapierre, GARNIER, Косметика";;;;"okay_beauty15.jpg, okay_beauty16.jpg, okay_beauty17.jpg";nabor-meosmeticheskih-kistej-bellapierre;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Мягкие матовые тени";;BEAU_026;167.00;;4;;;;1;;"Мягкие матовые тени";"Мягкие матовые тени, GARNIER, Косметика";;;;"okay_beauty19.jpg, okay_beauty20.jpg, okay_beauty18.jpg";komplekt-iz-matovoj-aomady-i-teni-dlya-glah;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Мягкий матовый крем для губ";;BEAU_025;120.00;;4;;;;1;;"Мягкий матовый крем для губ";"Мягкий матовый крем для губ, GARNIER, Косметика";;;;"okay_beauty20.jpg, okay_beauty18.jpg, okay_beauty19.jpg";myagkie-matovye-teni;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Крем для лица ESTEE LAUDER";;BEAU_022;122.00;;4;;;;1;;"Крем для лица ESTEE LAUDER";"Крем для лица ESTEE LAUDER, GARNIER, Косметика";;;;"okay_beauty18.jpg, okay_beauty19.jpg, okay_beauty20.jpg";myagkij-matovyj-krem-dlya-gub;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Духи Versace Bright crystal ABSOLU";;BEAU_022;122.00;;4;;;;1;1;"Духи Versace Bright crystal ABSOLU";"Духи Versace Bright crystal ABSOLU, GARNIER, Косметика";;;;"okay_beauty13.jpg, okay_beauty14.jpg";krem-dlya-litsa-estee-lauder;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Универсальная маска для лица";;BEAU_021;131.00;;4;;;;1;0;"Универсальная маска для лица";"Универсальная маска для лица, GARNIER, Косметика";;;;"okay_beauty11.jpg, okay_beauty10.jpg, okay_beauty12.jpg";duhi-versace-bright-crystal-absolu;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Духи с ароматом стородины";;BEAU_020;133.00;;4;;;;1;;"Духи с ароматом стородины";"Духи с ароматом стородины, GARNIER, Косметика";;;;"okay_beauty7.jpg, okay_beauty9.jpg, okay_beauty8.jpg";universalnaya-maska-dlya-litsa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Наращивание ресниц";;BEAU_019;134.00;;4;;;;1;;"Наращивание ресниц";"Наращивание ресниц, GARNIER, Косметика";;;;"okay_beauty4.jpg, okay_beauty5.jpg, okay_beauty6.jpg";duhi-s-aromatom-storodiny;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Косметика";GARNIER;"Наращивание ресниц";;BEAU_015;135.00;;4;;;;1;;"Наращивание ресниц";"Наращивание ресниц, GARNIER, Косметика";;;;"okay_beauty1.jpg, okay_beauty2.jpg, okay_beauty3.jpg";naraschivanie-resnits;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Книги и канцтовары";;"Спящие красавицы Стивен Кинг";;book30;1880.00;;4;;;;1;;"Спящие красавицы Стивен Кинг";"Спящие красавицы Стивен Кинг, Книги и канцтовары";"Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже. 1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар): После добавления файла - сохраните товар. 2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после ";"
  • Автор: Стивен Кинг
  • Страниц: 880
  • Год издания: 2018
";"

Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже.

1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар):

После добавления файла - сохраните товар.

2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после оплаты заказа.

Письмо клиенту до оплаты:

Письмо клиенту после оплаты заказа:

3. Получение клиентом заказанного товара:

Тоже самое можно проделать с любым электронным файлом.

  • Если вы продаете музыкальные композиции - загрузите в описание к товару кусочек своего трека.
  • Если вы занимаетесь продажей видеороликов - добавьте в качестве фотографий товара несколько скриншотов своего видео. 
  • Возможно вы продаете ценную информацию, например, книги? Несколько абзацев в описании к товару заинтересуют покупателя.

Пробуйте и у вас получится.

";"okay-book7.jpg, okay-book1.jpg, okay-book2.jpg, okay-book3.jpg";lovets-snov-stiven-king;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"С. Кінг";украинский;"The Outsider";переплет;592;ч/б;2019;"Фантастика. фэнтези";;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Книги и канцтовары";;"Стрелец. Темная Башня Стивен Кинг";;book30;1280.00;;4;;;;1;;"Стрелец. Темная Башня Стивен Кинг";"Стрелец. Темная Башня Стивен Кинг, Книги и канцтовары";"Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже. 1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар): После добавления файла - сохраните товар. 2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после ";"
  • Автор: Стивен Кинг
  • Страниц: 240
  • Год издания: 2017
";"

Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже.

1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар):

После добавления файла - сохраните товар.

2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после оплаты заказа.

Письмо клиенту до оплаты:

Письмо клиенту после оплаты заказа:

3. Получение клиентом заказанного товара:

Тоже самое можно проделать с любым электронным файлом.

  • Если вы продаете музыкальные композиции - загрузите в описание к товару кусочек своего трека.
  • Если вы занимаетесь продажей видеороликов - добавьте в качестве фотографий товара несколько скриншотов своего видео. 
  • Возможно вы продаете ценную информацию, например, книги? Несколько абзацев в описании к товару заинтересуют покупателя.

Пробуйте и у вас получится.

";"okay-book6.jpg, okay-book3.jpg, okay-book4.jpg, okay-book5.jpg";spyaschie-krasavitsy-stiven-king;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"С. Кінг";украинский;"The Outsider";переплет;592;ч/б;2019,,2017;"Фантастика. фэнтези";;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Книги и канцтовары";;"Зеленая миля Стивен Кинг";;book47;980.00;;4;;;;1;1;"Зеленая миля Стивен Кинг";"Зеленая миля Стивен Кинг, Книги и канцтовары";"Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже. 1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар): После добавления файла - сохраните товар. 2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после ";"
  • Автор: Стивен Кинг
  • Страниц: 432
  • Год издания: 2017
";"

Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже.

1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар):

После добавления файла - сохраните товар.

2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после оплаты заказа.

Письмо клиенту до оплаты:

Письмо клиенту после оплаты заказа:

3. Получение клиентом заказанного товара:

Тоже самое можно проделать с любым электронным файлом.

  • Если вы продаете музыкальные композиции - загрузите в описание к товару кусочек своего трека.
  • Если вы занимаетесь продажей видеороликов - добавьте в качестве фотографий товара несколько скриншотов своего видео. 
  • Возможно вы продаете ценную информацию, например, книги? Несколько абзацев в описании к товару заинтересуют покупателя.

Пробуйте и у вас получится.

";"okay-book5.jpg, okay-book7.jpg, okay-book8.jpg";strelets-temnaya-bashnya-stiven-king;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"С. Кінг";украинский;"The Outsider";переплет;592;ч/б;2019;"Фантастика. фэнтези";;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Книги и канцтовары";;"Керри животных Стивен Кинг";;book67;410.00;580.00;4;;;;1;;"Керри животных Стивен Кинг";"Керри животных Стивен Кинг, Книги и канцтовары";"Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже. 1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар): После добавления файла - сохраните товар. 2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после ";"
  • Автор: Стивен Кинг
  • Страниц: 224
  • Год издания: 2018
";"

Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже.

1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар):

После добавления файла - сохраните товар.

2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после оплаты заказа.

Письмо клиенту до оплаты:

Письмо клиенту после оплаты заказа:

3. Получение клиентом заказанного товара:

Тоже самое можно проделать с любым электронным файлом.

  • Если вы продаете музыкальные композиции - загрузите в описание к товару кусочек своего трека.
  • Если вы занимаетесь продажей видеороликов - добавьте в качестве фотографий товара несколько скриншотов своего видео. 
  • Возможно вы продаете ценную информацию, например, книги? Несколько абзацев в описании к товару заинтересуют покупателя.

Пробуйте и у вас получится.

";"okay-book4.jpg, okay-book5.jpg, okay-book6.jpg";zelenaya-milya-stiven-king;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"С. Кінг";украинский;"The Outsider";переплет;592;ч/б;2019;"Фантастика. фэнтези";;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Книги и канцтовары";;"Кладбище домашних животных Стивен Кинг";;book98;490.00;600.00;4;;;;1;;"Кладбище домашних животных Стивен Кинг";"Кладбище домашних животных Стивен Кинг, Книги и канцтовары";"Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже. 1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар): После добавления файла - сохраните товар. 2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после ";"
  • Автор: Стивен Кинг
  • Страниц:400
  • Год издания: 2019
";"

Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже.

1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар):

После добавления файла - сохраните товар.

2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после оплаты заказа.

Письмо клиенту до оплаты:

Письмо клиенту после оплаты заказа:

3. Получение клиентом заказанного товара:

Тоже самое можно проделать с любым электронным файлом.

  • Если вы продаете музыкальные композиции - загрузите в описание к товару кусочек своего трека.
  • Если вы занимаетесь продажей видеороликов - добавьте в качестве фотографий товара несколько скриншотов своего видео. 
  • Возможно вы продаете ценную информацию, например, книги? Несколько абзацев в описании к товару заинтересуют покупателя.

Пробуйте и у вас получится.

";"okay-book3.jpg, okay-book1.jpg, okay-book8.jpg, okay-book7.jpg";kerri-zhivotnyh-stiven-king;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"С. Кінг";украинский;"The Outsider";переплет;592;ч/б;2019;"Фантастика. фэнтези";;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Книги и канцтовары";;"Аутсайдер Стивен Кинг";;book96;590.00;800.00;4;;;;1;;"Аутсайдер Стивен Кинг";"Аутсайдер Стивен Кинг, Книги и канцтовары";"Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже. 1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар): После добавления файла - сохраните товар. 2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после ";"
  • Автор: Стивен Кинг
  • Страниц: 592
  • Год издания: 2019
";"

Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже.

1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар):

После добавления файла - сохраните товар.

2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после оплаты заказа.

Письмо клиенту до оплаты:

Письмо клиенту после оплаты заказа:

3. Получение клиентом заказанного товара:

Тоже самое можно проделать с любым электронным файлом.

  • Если вы продаете музыкальные композиции - загрузите в описание к товару кусочек своего трека.
  • Если вы занимаетесь продажей видеороликов - добавьте в качестве фотографий товара несколько скриншотов своего видео. 
  • Возможно вы продаете ценную информацию, например, книги? Несколько абзацев в описании к товару заинтересуют покупателя.

Пробуйте и у вас получится.

";"okay-book2.jpg, okay-book4.jpg, okay-book6.jpg, okay-book5.jpg, okay-book3.jpg";kladbische-domashnih-zhivotnyh-stiven-king;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"С. Кінг";украинский;"The Outsider";переплет;592;ч/б;2019;"Фантастика. фэнтези";;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Книги и канцтовары";;"Аутсайдер Стивен Кинг";;;390.00;;4;;;;1;;"Аутсайдер Стивен Кинг";"Аутсайдер Стивен Кинг, Книги и канцтовары";"Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже. 1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар): После добавления файла - сохраните товар. 2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после ";"
  • Автор: Стивен Кинг
  • Страниц: 592
  • Год издания: 2019
";"

Как загрузить цифровой товар на сайт? Как предоставить файл покупателю после оплаты заказа? Следуйте инструкции ниже.

1. В административной панели сайта вы создаете товар обычным образом - заполняете все необходимые поля (название, категорию, мета данные, описание и т.п.), а также прикрепляете файл (ваш цифровой товар):

После добавления файла - сохраните товар.

2. Если оформил заказ на покупку товара ""Авторская фотография суровой собачки"", оригинал фотографии он сможет получить только после оплаты заказа.

Письмо клиенту до оплаты:

Письмо клиенту после оплаты заказа:

3. Получение клиентом заказанного товара:

Тоже самое можно проделать с любым электронным файлом.

  • Если вы продаете музыкальные композиции - загрузите в описание к товару кусочек своего трека.
  • Если вы занимаетесь продажей видеороликов - добавьте в качестве фотографий товара несколько скриншотов своего видео. 
  • Возможно вы продаете ценную информацию, например, книги? Несколько абзацев в описании к товару заинтересуют покупателя.

Пробуйте и у вас получится.

";"okay-book1.jpg, okay-book2.jpg, okay-book4.jpg, okay-book5.jpg, okay-book3.jpg";autsajder-stiven-king;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"С. Кінг";украинский;"The Outsider";переплет;592;ч/б;2019;"Фантастика. фэнтези";;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Строительные инструменты";Bosch;"Отбойный молоток Bosch GSH 7 VC";;;7500.00;;4;;;;1;;"Отбойный молоток Bosch GSH 7 VC";"Отбойный молоток Bosch GSH 7 VC, Bosch, Строительные инструменты";;;;"okay_screwdriver22.jpg, okay_screwdriver23.jpg, okay_screwdriver24.jpg";shurupovert-akkumulyatornyj-bosch;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Строительные инструменты";Bosch;"Профессиональный мини-пылесос Bosch";;;29500.00;;4;;;;1;;"Профессиональный мини-пылесос Bosch";"Профессиональный мини-пылесос Bosch, Bosch, Строительные инструменты";;;;"okay_screwdriver19.jpg, okay_screwdriver20.jpg, okay_screwdriver21.jpg";otbojnyj-molotok-bosch-gsh-7-vc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Строительные инструменты";Bosch;"Пылезащитный прибор Bosch";;;9500.00;;4;;;;1;;"Пылезащитный прибор Bosch";"Пылезащитный прибор Bosch, Bosch, Строительные инструменты";;;;"okay_screwdriver16.jpg, okay_screwdriver17.jpg, okay_screwdriver18.jpg";professionalnyj-mini-pylesos-bosch;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Строительные инструменты";Bosch;"Отбойный молоток Bosch GSH 6784";;;12700.00;;4;;;;1;0;"Отбойный молоток Bosch GSH 6784";"Отбойный молоток Bosch GSH 6784, Bosch, Строительные инструменты";;;;"okay_screwdriver13.jpg, okay_screwdriver14.jpg, okay_screwdriver15.jpg";pylezaschitnyj-pribor-bosch;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Строительные инструменты";Bosch;"Беспроводной перфоратор Bosch Plus";;;11000.00;;4;;;;1;0;"Беспроводной перфоратор Bosch Plus";"Беспроводной перфоратор Bosch Plus, Bosch, Строительные инструменты";;;;"okay_screwdriver10.jpg, okay_screwdriver11.jpg, okay_screwdriver12.jpg";otbojnyj-molotok-bosch-gsh-6784;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Строительные инструменты";Bosch;"Корпус дисковой пилы Bosch";;;83000.00;;4;;;;1;1;"Корпус дисковой пилы Bosch";"Корпус дисковой пилы Bosch, Bosch, Строительные инструменты";;;;"okay_screwdriver7.jpg, okay_screwdriver8.jpg, okay_screwdriver9.jpg";besprovodnoj-perforator-bosch-plus;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Строительные инструменты";Bosch;"Шуруповерт аккумуляторный DeWalt Speed";;;71700.00;;4;;;;1;;"Шуруповерт аккумуляторный DeWalt Speed";"Шуруповерт аккумуляторный DeWalt Speed, Bosch, Строительные инструменты";;;;"okay_screwdriver4.jpg, okay_screwdriver5.jpg, okay_screwdriver6.jpg";korpus-diskovoj-pily-bosch;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Строительные инструменты";Bosch;"Шуруповерт аккумуляторный DeWalt Speed";;;36700.00;;4;;;;1;;"Шуруповерт аккумуляторный DeWalt Speed";"Шуруповерт аккумуляторный DeWalt Speed, Bosch, Строительные инструменты";;;;"okay_screwdriver1.jpg, okay_screwdriver2.jpg, okay_screwdriver3.jpg";shurupovert-akkumulyatornyj-dewalt-speed;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Ваннаа комната/Тумбы для ванной,, Home furniture/Ваннаа комната/Комплекты для ванной";Beretta;"Тумба под раковину Aquwastyle4";;qsdc62;25300.00;;4;;;;1;;"Тумба под раковину Aquwastyle4";"Тумба под раковину Aquwastyle4, Beretta, Тумбы для ванной";;;;"okay_bathroom_tumba4.png, okay_bathroom_tumba4.jpg, okay_bathroom_tumba5.jpg, okay_bathroom_tumba3.jpg, okay_bathroom_tumba2.jpg, okay_bathroom_tumba1.jpg";tumba-pod-rakovinu-aquwastyle5;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Белый;;;;;;;Керамика;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;напольный;Распашные;545x850x440 -"Home furniture/Ваннаа комната/Тумбы для ванной,, Home furniture/Ваннаа комната/Комплекты для ванной";Beretta;"Тумба под раковину Aquwastyle3";;qsdc62;15300.00;;4;;;;1;;"Тумба под раковину Aquwastyle3";"Тумба под раковину Aquwastyle3, Beretta, Тумбы для ванной";;;;"okay_bathroom_tumba5.jpg, okay_bathroom_tumba5.jpg, okay_bathroom_tumba3.jpg, okay_bathroom_tumba2.jpg, okay_bathroom_tumba1.jpg, okay_bathroom_tumba4.jpg";tumba-pod-rakovinu-aquwastyle4;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Silver;;;;;;;Дерево;;;;;;;;;;;;;;;;;"5 лет";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;напольный;Распашные;545x850x440 -"Home furniture/Ваннаа комната/Тумбы для ванной,, Home furniture/Ваннаа комната/Комплекты для ванной";Beretta;"Тумба под раковину Aquwastyle2";;qsdc62;17700.00;;4;;;;1;;"Тумба под раковину Aquwastyle2";"Тумба под раковину Aquwastyle2, Beretta, Тумбы для ванной";;;;"okay_bathroom_tumba6.png, okay_bathroom_tumba3.jpg, okay_bathroom_tumba2.jpg, okay_bathroom_tumba1.jpg, okay_bathroom_tumba5.jpg, okay_bathroom_tumba4.jpg";tumba-pod-rakovinu-aquwastyle3;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Красный;;;;;;;Керамика;;;;;;;;;;;;;;;;;"8 лет";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;напольный;Распашные;545x850x440 -"Home furniture/Ваннаа комната/Тумбы для ванной,, Home furniture/Ваннаа комната/Комплекты для ванной";Beretta;"Тумба под раковину Aquwastyle1";;qsdc62;11700.00;;4;;;;1;;"Тумба под раковину Aquwastyle1";"Тумба под раковину Aquwastyle1, Beretta, Тумбы для ванной";;;;"okay_bathroom_tumba2.jpg, okay_bathroom_tumba1.jpg, okay_bathroom_tumba3.jpg, okay_bathroom_tumba5.jpg, okay_bathroom_tumba4.jpg";tumba-pod-rakovinu-aquwastyle2;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Ваннаа комната/Тумбы для ванной,, Home furniture/Ваннаа комната/Комплекты для ванной";Beretta;"Тумба под раковину Aquwastyle1";;qsdc62;8700.00;;4;;;;1;;"Тумба под раковину Aquwastyle1";"Тумба под раковину Aquwastyle1, Beretta, Тумбы для ванной";;;;"okay_bathroom_tumba1.jpg, okay_bathroom_tumba2.jpg, okay_bathroom_tumba3.jpg, okay_bathroom_tumba5.jpg, okay_bathroom_tumba4.jpg";tumba-pod-rakovinu-aquwastyle1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Ваннаа комната/Пеналы";;"Пенал для ванной Grinder2";;;9600.00;;4;;;;1;;"Пенал для ванной Grinder2";"Пенал для ванной Grinder2, Пеналы";;;;"okay_bathroom_penal3.jpg, okay_bathroom_penal2.jpg, okay_bathroom_penal1.jpg";penal-dlya-vannoj-grinder-bronze;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Ваннаа комната/Пеналы";;"Пенал для ванной Grinder";;tyu683;4400.00;;4;;;;1;;"Пенал для ванной Grinder";"Пенал для ванной Grinder, Пеналы";;;;"okay_bathroom_penal2.jpg, okay_bathroom_penal1.jpg, okay_bathroom_penal3.jpg";penal-dlya-vannoj-grinder2;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Ваннаа комната/Пеналы";;"Пенал для ванной Bookends3";;dcf521;7400.00;;4;;;;1;;"Пенал для ванной Bookends3";"Пенал для ванной Bookends3, Пеналы";;;;"okay_bathroom_penal1.jpg, okay_bathroom_penal2.jpg, okay_bathroom_penal3.jpg";penal-dlya-vannoj-grinder;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Ваннаа комната/Пеналы";;"Пенал для ванной Bookends2";;;8489.00;;4;;;;1;;"Пенал для ванной Bookends2";"Пенал для ванной Bookends2, Пеналы";;;;"okay_bathroom_penal6.jpg, okay_bathroom_penal5.jpg, okay_bathroom_penal4.jpg";penal-dlya-vannoj-bookends3;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Ваннаа комната/Пеналы";;"Пенал для ванной Bookends1";;;7489.00;;4;;;;1;;"Пенал для ванной Bookends1";"Пенал для ванной Bookends1, Пеналы";;;;"okay_bathroom_penal5.jpg, okay_bathroom_penal4.jpg, okay_bathroom_penal6.jpg";penal-dlya-vannoj-bookends2;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Ваннаа комната/Пеналы";;"Пенал для ванной Bookends1";;;3489.00;;4;;;;1;;"Пенал для ванной Bookends1";"Пенал для ванной Bookends1, Пеналы";;;;"okay_bathroom_penal4.jpg, okay_bathroom_penal5.jpg, okay_bathroom_penal6.jpg";penal-dlya-vannoj-bookends1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Для спальни/Кровати";;"Кровать Kerela Quilt Cover";;opwe8;25900.00;;4;;;;1;;"Кровать Kerela Quilt Cover";"Кровать Kerela Quilt Cover, Кровати";;;;"okay_quilt6.jpg, okay_quilt7.jpg, okay_quilt8.jpg";krovat-kerela-quilt;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Для спальни/Кровати";;"Кровать Shelly Cover Natural2";;opwe8;25900.00;;4;;;;1;;"Кровать Shelly Cover Natural2";"Кровать Shelly Cover Natural2, Кровати";;;;"okay_quilt7.jpg, okay_quilt4.jpg, okay_quilt5.jpg, okay_quilt6.jpg";krovat-kerela-quilt-cover;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Для спальни/Кровати";;"Кровать Shelly Cover Natural";;opwe6;45900.00;;4;;;;1;;"Кровать Shelly Cover Natural";"Кровать Shelly Cover Natural, Кровати";;;;"okay_quilt3.jpg, okay_quilt5.jpg";krovat-shelly-cover-natural2;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Для спальни/Кровати";;"Кровать Shelly Beach Quilt 2";;;14900.00;;4;;;;1;;"Кровать Shelly Beach Quilt 2";"Кровать Shelly Beach Quilt 2, Кровати";;;;"okay_quilt2.jpg, 79-large_default.jpg, okay_quilt4.jpg";krovat-shelly-cover-natural;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Для спальни/Кровати";;"Кровать Chikasha Natural Quilt";;;14900.00;;4;;;;1;;"Кровать Chikasha Natural Quilt";"Кровать Chikasha Natural Quilt, Кровати";;;;"okay_quilt1.jpg, okay_quilt3.jpg, okay_quilt4.jpg, okay_quilt5.jpg";krovat-shelly-beach-quilt-2;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Для спальни/Кровати";;"Кровать Chikasha Natural Quilt";;;8900.00;;4;;;;1;;"Кровать Chikasha Natural Quilt";"Кровать Chikasha Natural Quilt, Кровати";;;;"79-large_default.jpg, okay_quilt3.jpg, okay_quilt1.jpg, okay_quilt1.jpg, okay_quilt2.jpg";krovat-chikasha-natural-quilt;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Складной стул Shine Catalina 34";;mno96;1680.00;;4;;;;1;;"Складной стул Shine Catalina 34";"Складной стул Shine Catalina 34, Fitbit, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Франция
  • Вес: 13кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire2.jpg, okay_chaire6png.jpg, okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina-37;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Складной стул Shine Catalina 34";Синий;mno97;1500.00;1680.00;4;;1;;1;;"Складной стул Shine Catalina 34";"Складной стул Shine Catalina 34, Fitbit, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Франция
  • Вес: 13кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire2.jpg, okay_chaire6png.jpg, okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina-37;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Складной стул Shine Catalina 34";Красный;mno95;1680.00;1700.00;4;;0;;1;;"Складной стул Shine Catalina 34";"Складной стул Shine Catalina 34, Fitbit, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Франция
  • Вес: 13кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire2.jpg, okay_chaire6png.jpg, okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina-37;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Складной стул Shine Catalina 34";Коричневый;mno98;1500.00;;4;;12;;1;;"Складной стул Shine Catalina 34";"Складной стул Shine Catalina 34, Fitbit, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Франция
  • Вес: 13кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire2.jpg, okay_chaire6png.jpg, okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina-37;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Складной стул Shine Catalina";Синий;mno97;1500.00;1680.00;4;;1;;1;;"Складной стул Shine Catalina";"Складной стул Shine Catalina, Fitbit, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Франция
  • Вес: 11.5кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire6png.jpg, okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire2.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina-34;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Складной стул Shine Catalina";Красный;mno95;1680.00;1700.00;4;;0;;1;;"Складной стул Shine Catalina";"Складной стул Shine Catalina, Fitbit, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Франция
  • Вес: 11.5кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire6png.jpg, okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire2.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina-34;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Складной стул Shine Catalina";Коричневый;mno98;1500.00;;4;;12;;1;;"Складной стул Shine Catalina";"Складной стул Shine Catalina, Fitbit, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Франция
  • Вес: 11.5кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire6png.jpg, okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire2.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina-34;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Складной стул Shine Catalina";;mno96;1680.00;;4;;;;1;;"Складной стул Shine Catalina";"Складной стул Shine Catalina, Fitbit, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Франция
  • Вес: 11.5кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire6png.jpg, okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire2.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina-34;;"13 кг.";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Folding chair Catalina";Красный;mno95;1680.00;1700.00;4;;0;;1;1;"Folding chair Catalina";"Folding chair Catalina, Beretta, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Италия
  • Вес: 12кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire6png.jpg, okay_chaire2.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Дерево;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Folding chair Catalina";Коричневый;mno98;1500.00;;4;;15;;1;1;"Folding chair Catalina";"Folding chair Catalina, Beretta, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Италия
  • Вес: 12кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire6png.jpg, okay_chaire2.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Дерево;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Folding chair Catalina";;mno96;1680.00;;4;;;;1;1;"Folding chair Catalina";"Folding chair Catalina, Beretta, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Италия
  • Вес: 12кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire6png.jpg, okay_chaire2.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Дерево;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Fitbit;"Folding chair Catalina";Синий;mno97;1500.00;1680.00;4;;0;;1;1;"Folding chair Catalina";"Folding chair Catalina, Beretta, Складные стулья";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)91.0, (W)54ю8
  • Количество мест: 1
  • Высота сиденья: 1.2м
  • Страна: Италия
  • Вес: 12кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire7.jpg, okay_chaire5.jpg, okay_chaire6png.jpg, okay_chaire2.jpg, okay_chaire1.jpg, okay_chaire3.jpg";skladnoj-stul-shine-catalina;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Дерево;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Beretta;"Классический табурет Trex40";Коричневый;mno98;1500.00;;4;;10;;1;0;"Классический табурет Trex40";"Классический табурет Trex40, Beretta, табуреты";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";;"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire5.jpg, okay_chaire6png.jpg, okay_chaire2.jpg, okay_chaire7.jpg, okay_chaire1.jpg, okay_chaire3.jpg";folding-chair-catalina;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Дерево;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Beretta;"Классический табурет Trex40";;mno96;1680.00;;4;;;;1;0;"Классический табурет Trex40";"Классический табурет Trex40, Beretta, табуреты";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";;"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire5.jpg, okay_chaire6png.jpg, okay_chaire2.jpg, okay_chaire7.jpg, okay_chaire1.jpg, okay_chaire3.jpg";folding-chair-catalina;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Дерево;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Beretta;"Классический табурет Trex40";Красный;mno95;1680.00;1700.00;4;;0;;1;0;"Классический табурет Trex40";"Классический табурет Trex40, Beretta, табуреты";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";;"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire5.jpg, okay_chaire6png.jpg, okay_chaire2.jpg, okay_chaire7.jpg, okay_chaire1.jpg, okay_chaire3.jpg";folding-chair-catalina;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Дерево;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Складная мебель";Beretta;"Классический табурет Trex40";Синий;mno97;1500.00;1680.00;4;;1;;1;0;"Классический табурет Trex40";"Классический табурет Trex40, Beretta, табуреты";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";;"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_chaire5.jpg, okay_chaire6png.jpg, okay_chaire2.jpg, okay_chaire7.jpg, okay_chaire1.jpg, okay_chaire3.jpg";folding-chair-catalina;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)91.0. (W)54ю8";;;;;;;;Дерево;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;1;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/табуреты";Beretta;"Классический табурет Trex30";;ur468;1720.00;;4;;0;;1;0;"Классический табурет Trex30";"Классический табурет Trex30, Beretta, табуреты";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";;"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_stool4.jpg, okay_stool1.jpg, okay_stool2.jpg, okay_stool3.jpg, okay_stool5.jpg";klassicheskij-taburet-trex40;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/табуреты";Beretta;"Классический табурет Trex37";;ur878;1220.00;1400.00;4;;;;1;;"Классический табурет Trex37";"Классический табурет Trex37, Beretta, табуреты";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";;"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_stool1.jpg, okay_stool2.jpg, okay_stool3.jpg, okay_stool4.jpg, okay_stool5.jpg";klassicheskij-taburet-trex30;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/табуреты";Beretta;"Классический табурет Trex34";;ur84;1240.00;;4;;;;1;;"Классический табурет Trex34";"Классический табурет Trex34, Beretta, табуреты";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";;"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_stool2.jpg, okay_stool3.jpg, okay_stool4.jpg, okay_stool1.jpg, okay_stool5.jpg";klassicheskij-taburet-trex37;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/табуреты";Beretta;"Классический табурет Trex34";;;1200.00;;4;;;;1;;"Классический табурет Trex34";"Классический табурет Trex34, Beretta, табуреты";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";;"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_stool3.jpg, okay_stool4.jpg, okay_stool1.jpg, okay_stool2.jpg, okay_stool5.jpg";klassicheskij-taburet-trex34;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Lazy Outdoor Wicker";Бирюзовый;tyuiee753;16900.00;18900.00;4;;0;;1;;"LexMod Twirl Lazy Outdoor Wicker";"LexMod Twirl Lazy Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 7
  • Высота сиденья: 1.3м
  • Водонепронецаемый: нет
  • Вес: 140кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch4.jpg, okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg";lexmod-twirl-medium-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Lazy Outdoor Wicker";Оранжевый;otu64626;18400.00;;4;120.00;4;;1;;"LexMod Twirl Lazy Outdoor Wicker";"LexMod Twirl Lazy Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 7
  • Высота сиденья: 1.3м
  • Водонепронецаемый: нет
  • Вес: 140кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch4.jpg, okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg";lexmod-twirl-medium-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Lazy Outdoor Wicker";Белый;ytu235235;11900.00;13400.00;4;120.00;4;;1;;"LexMod Twirl Lazy Outdoor Wicker";"LexMod Twirl Lazy Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 7
  • Высота сиденья: 1.3м
  • Водонепронецаемый: нет
  • Вес: 140кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch4.jpg, okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg";lexmod-twirl-medium-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Lazy Outdoor Wicker";Бежевый;twe532;16800.00;;4;120.00;0;;1;;"LexMod Twirl Lazy Outdoor Wicker";"LexMod Twirl Lazy Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 7
  • Высота сиденья: 1.3м
  • Водонепронецаемый: нет
  • Вес: 140кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch4.jpg, okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg";lexmod-twirl-medium-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Wicker Patio Daybed";"89L x 69.5W x 28.5H";dte352;45114.00;;4;;;;1;1;"LexMod Taiji Wicker Patio Daybed";"LexMod Taiji Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 65.5L x 89.5W x 25.5H
  • Количество мест: 7
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch7png.jpg, okay_couch5.jpg, okay_couch10.jpg, okay_couch6.jpg, okay_couch9.jpg, okay_couch8png.jpg";lexmod-pro-taiji-wicker-patio-daybed;;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Wicker Patio Daybed";"65.5L x 89.5W x 25.5H";yiyt8675;58000.00;11200.00;4;;;;1;1;"LexMod Taiji Wicker Patio Daybed";"LexMod Taiji Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 65.5L x 89.5W x 25.5H
  • Количество мест: 7
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch7png.jpg, okay_couch5.jpg, okay_couch10.jpg, okay_couch6.jpg, okay_couch9.jpg, okay_couch8png.jpg";lexmod-pro-taiji-wicker-patio-daybed;;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Medium Outdoor Wicker";Бирюзовый;tyuiee753;23650.00;18900.00;4;;1;;1;;"LexMod Twirl Medium Outdoor Wicker";"LexMod Twirl Medium Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 5
  • Высота сиденья: 1.45м
  • Водонепронецаемый: да
  • Вес: 145кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch2.jpg, okay_couch4.jpg, okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg";lexmod-twirl-mono-outdoor-wicker;;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Outdoor Wicker Patio Daybed";"47L x 104W x 33.5H";pui76969;34000.00;;4;;;;1;;"LexMod Taiji Outdoor Wicker Patio Daybed";"LexMod Taiji Outdoor Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 47L x 104W x 33.5H
  • Количество мест: 5
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch6.jpg, okay_couch10.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg, okay_couch5.jpg";lexmod-taiji-outdoor-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Outdoor Wicker Patio Daybed";"65.5L x 89.5W x 25.5H";yiyt8675;58000.00;11200.00;4;;;;1;;"LexMod Taiji Outdoor Wicker Patio Daybed";"LexMod Taiji Outdoor Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 47L x 104W x 33.5H
  • Количество мест: 5
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch6.jpg, okay_couch10.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg, okay_couch5.jpg";lexmod-taiji-outdoor-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Outdoor Wicker Patio Daybed";"89L x 69.5W x 28.5H";dte352;56000.00;;4;;0;;1;;"LexMod Taiji Outdoor Wicker Patio Daybed";"LexMod Taiji Outdoor Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 47L x 104W x 33.5H
  • Количество мест: 5
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch6.jpg, okay_couch10.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg, okay_couch5.jpg";lexmod-taiji-outdoor-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl pro Outdoor Wicker";Оранжевый;otu64626;18400.00;18900.00;4;120.00;5;;1;;"LexMod Twirl pro Outdoor Wicker";"LexMod Twirl pro Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 6
  • Высота сиденья: 1.6м
  • Водонепронецаемый: нет
  • Вес: 130кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg, okay_couch4.jpg";lexmod-twirl-lazy-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl pro Outdoor Wicker";Белый;ytu235235;11900.00;13400.00;4;120.00;5;;1;;"LexMod Twirl pro Outdoor Wicker";"LexMod Twirl pro Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 6
  • Высота сиденья: 1.6м
  • Водонепронецаемый: нет
  • Вес: 130кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg, okay_couch4.jpg";lexmod-twirl-lazy-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl pro Outdoor Wicker";Бежевый;twe532;16800.00;;4;120.00;0;;1;;"LexMod Twirl pro Outdoor Wicker";"LexMod Twirl pro Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 6
  • Высота сиденья: 1.6м
  • Водонепронецаемый: нет
  • Вес: 130кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg, okay_couch4.jpg";lexmod-twirl-lazy-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl pro Outdoor Wicker";Бирюзовый;tyuiee753;19900.00;;4;;1;;1;;"LexMod Twirl pro Outdoor Wicker";"LexMod Twirl pro Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)208.5, (W)136.0
  • Количество мест: 6
  • Высота сиденья: 1.6м
  • Водонепронецаемый: нет
  • Вес: 130кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch5.jpg, okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg, okay_couch4.jpg";lexmod-twirl-lazy-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Out Wicker Patio Daybed";"89L x 69.5W x 28.5H";dte352;56000.00;;4;;0;;1;;"LexMod Taiji Out Wicker Patio Daybed";"LexMod Taiji Out Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 65.5L x 89.5W x 25.5H
  • Количество мест: 6
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch5.jpg, okay_couch10.jpg, okay_couch6.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg";lexmod-taiji-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Out Wicker Patio Daybed";"65.5L x 89.5W x 25.5H";yiyt8675;58000.00;11200.00;4;;;;1;;"LexMod Taiji Out Wicker Patio Daybed";"LexMod Taiji Out Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 65.5L x 89.5W x 25.5H
  • Количество мест: 6
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch5.jpg, okay_couch10.jpg, okay_couch6.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg";lexmod-taiji-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Out Wicker Patio Daybed";"47L x 104W x 33.5H";pui76969;34000.00;;4;;;;1;;"LexMod Taiji Out Wicker Patio Daybed";"LexMod Taiji Out Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 65.5L x 89.5W x 25.5H
  • Количество мест: 6
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch5.jpg, okay_couch10.jpg, okay_couch6.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg";lexmod-taiji-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Outdoor Wicker";Белый;ytu235235;18500.00;19000.00;4;120.00;5;;1;;"LexMod Twirl Outdoor Wicker";"LexMod Twirl Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)204.5, (W)133.0
  • Количество мест: 6
  • Высота сиденья: 1.5м
  • Водонепронецаемый: да
  • Вес: 120кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch1.jpg, okay_couch3.jpg, okay_couch2.jpg, okay_couch5.jpg, okay_couch4.jpg";lexmod-twirl-pro-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Outdoor Wicker";Бежевый;twe532;12000.00;;4;120.00;;;1;;"LexMod Twirl Outdoor Wicker";"LexMod Twirl Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)204.5, (W)133.0
  • Количество мест: 6
  • Высота сиденья: 1.5м
  • Водонепронецаемый: да
  • Вес: 120кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch1.jpg, okay_couch3.jpg, okay_couch2.jpg, okay_couch5.jpg, okay_couch4.jpg";lexmod-twirl-pro-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Outdoor Wicker";Оранжевый;otu64626;17500.00;18500.00;4;120.00;0;;1;;"LexMod Twirl Outdoor Wicker";"LexMod Twirl Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)204.5, (W)133.0
  • Количество мест: 6
  • Высота сиденья: 1.5м
  • Водонепронецаемый: да
  • Вес: 120кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch1.jpg, okay_couch3.jpg, okay_couch2.jpg, okay_couch5.jpg, okay_couch4.jpg";lexmod-twirl-pro-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Outdoor Wicker";Бирюзовый;tyuiee753;11000.00;;4;;1;;1;;"LexMod Twirl Outdoor Wicker";"LexMod Twirl Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)204.5, (W)133.0
  • Количество мест: 6
  • Высота сиденья: 1.5м
  • Водонепронецаемый: да
  • Вес: 120кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch1.jpg, okay_couch3.jpg, okay_couch2.jpg, okay_couch5.jpg, okay_couch4.jpg";lexmod-twirl-pro-outdoor-wicker;0.00000;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Outdoor Wicker Patio Daybed";"65.5L x 89.5W x 25.5H";yiyt8675;58000.00;11200.00;4;;;;1;;"LexMod Taiji Outdoor Wicker Patio Daybed";"LexMod Taiji Outdoor Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 47L x 104W x 33.5H
  • Количество мест: 5
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch10.jpg, okay_couch6.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg, okay_couch5.jpg";lexmod-taiji-out-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Outdoor Wicker Patio Daybed";"47L x 104W x 33.5H";pui76969;34000.00;;4;;;;1;;"LexMod Taiji Outdoor Wicker Patio Daybed";"LexMod Taiji Outdoor Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 47L x 104W x 33.5H
  • Количество мест: 5
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch10.jpg, okay_couch6.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg, okay_couch5.jpg";lexmod-taiji-out-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";"Britax romer";"LexMod Taiji Outdoor Wicker Patio Daybed";"89L x 69.5W x 28.5H";dte352;56000.00;;4;;0;;1;;"LexMod Taiji Outdoor Wicker Patio Daybed";"LexMod Taiji Outdoor Wicker Patio Daybed, Britax romer, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: 47L x 104W x 33.5H
  • Количество мест: 5
  • Высота сиденья: 1.55м
  • Водонепронецаемый: да
  • Вес: 215кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch10.jpg, okay_couch6.jpg, okay_couch9.jpg, okay_couch8png.jpg, okay_couch7png.jpg, okay_couch5.jpg";lexmod-taiji-out-wicker-patio-daybed;0.00000;215;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"147.7 х 70.8 х 7.3 cм";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Outdoor Wicker";Бежевый;twe532;15000.00;;4;120.00;;;1;;"LexMod Twirl Outdoor Wicker";"LexMod Twirl Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)204.5, (W)133.0, (Dia)133.0cm
  • Количество мест: 6
  • Высота сиденья: 1.5м
  • Водонепронецаемый: да
  • Вес: 120кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg, okay_couch5.jpg, okay_couch4.jpg";lexmod-twirl-outdoor-wicker;;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Outdoor Wicker";Белый;ytu235235;17500.00;19000.00;4;120.00;0;;1;;"LexMod Twirl Outdoor Wicker";"LexMod Twirl Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)204.5, (W)133.0, (Dia)133.0cm
  • Количество мест: 6
  • Высота сиденья: 1.5м
  • Водонепронецаемый: да
  • Вес: 120кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg, okay_couch5.jpg, okay_couch4.jpg";lexmod-twirl-outdoor-wicker;;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Outdoor Wicker";Оранжевый;otu64626;17500.00;18500.00;4;120.00;;;1;;"LexMod Twirl Outdoor Wicker";"LexMod Twirl Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)204.5, (W)133.0, (Dia)133.0cm
  • Количество мест: 6
  • Высота сиденья: 1.5м
  • Водонепронецаемый: да
  • Вес: 120кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg, okay_couch5.jpg, okay_couch4.jpg";lexmod-twirl-outdoor-wicker;;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Диваны";Puff;"LexMod Twirl Outdoor Wicker";Бирюзовый;tyuiee753;15000.00;;4;;1;;1;;"LexMod Twirl Outdoor Wicker";"LexMod Twirl Outdoor Wicker, Puff, Диваны";"Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.";"
  • Размеры: (H)204.5, (W)133.0, (Dia)133.0cm
  • Количество мест: 6
  • Высота сиденья: 1.5м
  • Водонепронецаемый: да
  • Вес: 120кг.
";"

Мода в этом случае - действительно интересное явление - нам часто нравятся красивые вещи. И это нормально - ведь мы получаем максимум информации через глаза. И наш продукт представляет собой идеальное сочетание симпатичных форм и действительно хорошего контента. Это действительно продукт нового поколения. Мы обещаем, что с нашим добром вы всегда будете в моде и стильно.

";"okay_couch3.jpg, okay_couch1.jpg, okay_couch2.jpg, okay_couch5.jpg, okay_couch4.jpg";lexmod-twirl-outdoor-wicker;;120кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"(H)204.5. (W)133.0. (Dia)133.0cm";;;;;;;;Коттон;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Франция;6;1.5м;да;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";"Britax romer";"Куркума Куркумин с биоперином 1500 мг";;ertw743362;1278.00;1366.00;4;;;;1;;"Куркума Куркумин с биоперином 1500 мг";"Куркума Куркумин с биоперином 1500 мг, Britax romer, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"
  1. Lorem ipsum dolor amet.
  2. Aliquam tincidunt.
  3. Vestibulum auctor dapibus.
";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals21.jpg, okay-heals22.jpg, okay-heals23.jpg";kurkuma-kurkumin-2250-mg;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";"Britax romer";"Premium Collagen Peptides";;ertw743362;958.00;;4;;;;1;1;"Premium Collagen Peptides";"Premium Collagen Peptides, Britax romer, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"
  1. Lorem ipsum dolor amet.
  2. Aliquam tincidunt.
  3. Vestibulum auctor dapibus.
";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals18.jpg, okay-heals20.jpg, okay-heals19.jpg";kurkuma-kurkumin-s-bioperinom-1500-mg;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";"Britax romer";"MuscleTech Platinum";;ertw532462;258.00;;4;;;;1;0;"MuscleTech Platinum";"MuscleTech Platinum, Fitbit, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"
  1. Lorem ipsum dolor amet.
  2. Aliquam tincidunt.
  3. Vestibulum auctor dapibus.
";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals15.jpg, okay-heals16.jpg, okay-heals17.jpg";premium-collagen-peptides;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";Fitbit;"Коромега Омега 3";;ertw532462;258.00;;4;;;;1;;"Коромега Омега 3";"Коромега Омега 3, Fitbit, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"
  1. Lorem ipsum dolor amet.
  2. Aliquam tincidunt.
  3. Vestibulum auctor dapibus.
";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals12.jpg, okay-heals13.jpg, okay-heals14.jpg";muscletech-platinum;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";Fitbit;"Коромега Омега 3";"Dora Dora";;258.00;;4;;0;;1;;"Коромега Омега 3";"Коромега Омега 3, Fitbit, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"
  1. Lorem ipsum dolor amet.
  2. Aliquam tincidunt.
  3. Vestibulum auctor dapibus.
";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals12.jpg, okay-heals13.jpg, okay-heals14.jpg";muscletech-platinum;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";Fitbit;"Коромега Омега 3";Тачки;;457.00;679.00;4;;3;;1;;"Коромега Омега 3";"Коромега Омега 3, Fitbit, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"
  1. Lorem ipsum dolor amet.
  2. Aliquam tincidunt.
  3. Vestibulum auctor dapibus.
";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals12.jpg, okay-heals13.jpg, okay-heals14.jpg";muscletech-platinum;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";Fitbit;"Клинический Лобный Термометр Новый 2017";;ertw532462;658.00;899.00;4;;;;1;;"Клинический Лобный Термометр Новый 2017";"Клинический Лобный Термометр Новый 2017, Britax romer, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"
  1. Lorem ipsum dolor amet.
  2. Aliquam tincidunt.
  3. Vestibulum auctor dapibus.
";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals9.jpg, okay-heals10.jpg, okay-heals11.jpg";koromega-omega-3;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";"Britax romer";"Country Life Экстракт расторопши";;ertw532462;958.00;1288.00;4;;;;1;;"Country Life Экстракт расторопши";"Country Life Экстракт расторопши, Britax romer, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"

<ol>
   <li>Lorem ipsum dolor amet.</li>
   <li>Aliquam tincidunt.</li>
   <li>Vestibulum auctor dapibus.</li>
</ol>

";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals6.jpg, okay-heals8.jpg, okay-heals7.jpg";klinicheskij-lobnyj-termometr-novyj-2017;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";"Britax romer";"Молочный чертополох";;ertw532462;458.00;;4;;;;1;0;"Молочный чертополох";"Молочный чертополох, Britax romer, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"

<ol>
   <li>Lorem ipsum dolor amet.</li>
   <li>Aliquam tincidunt.</li>
   <li>Vestibulum auctor dapibus.</li>
</ol>

";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals3.jpg, okay-heals5.jpg, okay-heals4.jpg";country-life-ekstrakt-rastoropshi;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Красота и здоровье/Витамины";"Britax romer";"Молочный чертополох";;;458.00;;4;;;;1;0;"Молочный чертополох";"Молочный чертополох, Витамины";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";"

<ol>
   <li>Lorem ipsum dolor amet.</li>
   <li>Aliquam tincidunt.</li>
   <li>Vestibulum auctor dapibus.</li>
</ol>

";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-heals1.jpg, okay-heals2.jpg";molochnyj-chertopoloh;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Бакалея";"Nature product";"Йогурт с клубникой и черникой в ​​греческом стиле";;fgeh624;36.00;;4;;;;1;;"Йогурт с клубникой и черникой в ​​греческом стиле";"Йогурт с клубникой и черникой в ​​греческом стиле, Nature product, Бакалея";;;;okay-food45.jpg;jogurt-s-klubnikoj-i-chernikoj-v-grecheskom-stile;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Алкогольные напитки";;Bitburger;;eovc85;8.00;;4;;;;1;;Bitburger;"Bitburger, Алкогольные напитки";;;;"okay-food44.jpg, okay-food43.jpg";bitburger;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Алкогольные напитки";"Organik food";"Heineken Светлое пиво";;ee349;23.00;27.00;4;;;;1;;"Heineken Светлое пиво";"Heineken Светлое пиво, Organik food, Алкогольные напитки";;;;"okay-food42.jpg, okay-food41.jpg";heineken-svetloe-pivo;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Алкогольные напитки";"Nature product";"Bottega Millesimato Brut Prosecco";;ghh436;629.00;;4;;;;1;;"Bottega Millesimato Brut Prosecco";"Bottega Millesimato Brut Prosecco, Nature product, Алкогольные напитки";;;;okay-food40.jpg;bottega-millesimato-brut-prosecco;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Алкогольные напитки";"Nature product";"Moet & Chandon Ice Imperial";;ouy325;1789.00;;4;;;;1;;"Moet & Chandon Ice Imperial";"Moet & Chandon Ice Imperial, Nature product, Алкогольные напитки";;;;okay-food39.jpg;moet--chandon-ice-imperial;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Алкогольные напитки";"Nature product";"World's Cellar Chateau Gillet Bordeaux Красное вино";;wr24;852.00;979.00;4;;;;1;0;"World's Cellar Chateau Gillet Bordeaux Красное вино";"World's Cellar Chateau Gillet Bordeaux Красное вино, Nature product, Алкогольные напитки";;;;okay-food38.jpg;worlds-cellar-chateau-gillet-bordeaux-krasnoe-vino;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Алкогольные напитки";"Organik food";"Боттега Манцони Пинк Платт Москато Роуз";;wine349;568.00;;4;;;;1;;"Боттега Манцони Пинк Платт Москато Роуз";"Боттега Манцони Пинк Платт Москато Роуз, Organik food, Алкогольные напитки";;;;"okay-food36.jpg, okay-food37.jpg";bottega-mantsoni-pink-platt-moskato-rouz;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Соки";"Organik food";"пелый клюквенный суперфруктовый сок";;cv5623;24.00;;4;;;;1;;"пелый клюквенный суперфруктовый сок";"пелый клюквенный суперфруктовый сок, Organik food, Соки";;;;okay-food34.jpg;pelyj-klyukvennyj-superfruktovyj-sok;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Соки";"Organik food";"Спелый сок манго";;;23.00;;4;;;;1;;"Спелый сок манго";"Спелый сок манго, Organik food, Соки";;;;"okay-food31.jpg, okay-food33.jpg";spelyj-sok-mango;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Бакалея,, Детские товары/Соки";"Nature product";"Chobani Банановый кленовый мешок овса";;uirt88679;34.00;;4;;;;1;;"Chobani Банановый кленовый мешок овса";"Chobani Банановый кленовый мешок овса, Nature product, Бакалея";;;;"okay-food29.jpg, okay-food30.jpg";chobani-bananovyj-klenovyj-meshok-ovsa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Бакалея";"Nature product";"San Remo Пенне Ригате № 18";;ui6246;96.00;;4;;;;1;0;"San Remo Пенне Ригате № 18";"San Remo Пенне Ригате № 18, Nature product, Бакалея";;;;"okay-food27.jpg, okay-food28.jpg";san-remo-penne-rigate--18;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Бакалея";"Nature product";"Heinz Томатный кетчуп";;ktc75;35.00;;4;;;;1;0;"Heinz Томатный кетчуп";"Heinz Томатный кетчуп, Nature product, Бакалея";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";;"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";okay-food26.jpg;heinz-tomatnyj-ketchup;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Мясо и рыба";"Organik food";"Свежие кубики говядины";;;567.00;604.00;4;;;кг;1;;"Свежие кубики говядины";"Свежие кубики говядины, Organik food, Мясо и рыба";"Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим";;"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца. Также все другие известные генераторы Lorem Ipsum используют один и тот же текст, который они просто повторяют, пока не достигнут нужный объём. Это делает предлагаемый здесь генератор единственным настоящим Lorem Ipsum генератором. Он использует словарь из более чем 200 латинских слов, а также набор моделей предложений. В результате сгенерированный Lorem Ipsum выглядит правдоподобно, не имеет повторяющихся абзацей или ""невозможных"" слов.

";"okay-food25.jpg, okay-food24.jpg";svezhie-kubiki-govyadiny;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Австралия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Мясо и рыба";"Organik food";"Мясо со свиным торцом - Австралия";;;768.00;;4;;;;1;;"Мясо со свиным торцом - Австралия";"Мясо со свиным торцом - Австралия, Organik food, Мясо и рыба";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";"

Многие думают, что Lorem Ipsum - взятый с потолка псевдо-латинский набор слов, но это не совсем так.

";"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food23.jpg, okay-food22.jpg";myaso-so-svinym-tortsom---avstraliya;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Австралия;Свинина;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Мясо и рыба";"Nature product";"Котлеты из баранины 6 в упаковке";;meat45;678.00;700.00;4;;;кг;1;0;"Котлеты из баранины 6 в упаковке";"Котлеты из баранины 6 в упаковке, Nature product, Мясо и рыба";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";;"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food20.jpg, okay-food21.jpg";kotlety-iz-baraniny-6-v-upakovke;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Мясо и рыба";"Organik food";"Свежие цельные куриные ножки";;gh616;456.00;;4;;;кг;1;;"Свежие цельные куриные ножки";"Свежие цельные куриные ножки, Organik food, Мясо и рыба";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";;"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food19.jpg, okay-food18.jpg, okay-food17.jpg";svezhie-tselnye-kurinye-nozhki;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Курица;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Фрукты и овощи";"Organik food";"Органический свежий австралийский кабачок цуккини";;;43.00;52.00;4;;;кг;1;;"Органический свежий австралийский кабачок цуккини";"Органический свежий австралийский кабачок цуккини, Organik food, Фрукты и овощи";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";;"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food11.jpg, okay-food2.jpg, okay-food1.jpg, okay-food7.jpg, okay-food8.jpg, okay-food9.jpg";organicheskij-svezhij-avstralijskij-kabachok-tsukkini;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Фрукты и овощи";"Nature product";"Красная капуста";;op099;43.00;;4;;;кг;1;;"Красная капуста";"Красная капуста, Nature product, Фрукты и овощи";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";;"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food7.jpg, okay-food4.jpg, okay-food14.jpg, okay-food11.jpg, okay-food9.jpg";krasnaya-kapusta;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Фрукты и овощи";"Organik food";Горох;;rt743;14.00;;4;;;кг;1;;Горох;"Горох, Organik food, Фрукты и овощи";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";;"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food1.jpg, okay-food11.jpg, okay-food12.jpg";goroh;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Польша;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Фрукты и овощи";"Organik food";"Сладкая кукуруза";;we58u;34.00;38.00;4;;;кг;1;1;"Сладкая кукуруза";"Сладкая кукуруза, Organik food, Фрукты и овощи";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации

";"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food2.jpg, okay-food3.jpg";sladkaya-kukuruza;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Фрукты и овощи";"Nature product";"Вегетарианский радужный шпинат";;;32.00;43.00;4;;;кг;1;;"Вегетарианский радужный шпинат";"Вегетарианский радужный шпинат, Nature product, Фрукты и овощи";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь.

";"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food8.jpg, okay-food1.jpg, okay-food14.jpg, okay-food5.jpg, okay-food9.jpg, okay-food10.jpg";vegetarianskij-raduzhnyj-shpinat;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Польша;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Фрукты и овощи";"Nature product";"Желтый стручковый перец";;qw90po;78.00;87.00;4;;;кг;1;;"Желтый стручковый перец";"Желтый стручковый перец, Nature product, Фрукты и овощи";"Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу ";"

Есть много вариантов Lorem Ipsum, но большинство из них имеет не всегда приемлемые модификации, например, юмористические вставки или слова, которые даже отдалённо не напоминают латынь. Если вам нужен Lorem Ipsum для серьёзного проекта, вы наверняка не хотите какой-нибудь шутки, скрытой в середине абзаца.

";"

Давно выяснено, что при оценке дизайна и композиции читаемый текст мешает сосредоточиться. Lorem Ipsum используют потому, что тот обеспечивает более или менее стандартное заполнение шаблона, а также реальное распределение букв и пробелов в абзацах, которое не получается при простой дубликации ""Здесь ваш текст.. Здесь ваш текст.. Здесь ваш текст.."" Многие программы электронной вёрстки и редакторы HTML используют Lorem Ipsum в качестве текста по умолчанию, так что поиск по ключевым словам ""lorem ipsum"" сразу показывает, как много веб-страниц всё ещё дожидаются своего настоящего рождения. За прошедшие годы текст Lorem Ipsum получил много версий. Некоторые версии появились по ошибке, некоторые - намеренно (например, юмористические варианты).

";"okay-food13.jpg, okay-food15.jpg, okay-food2.jpg, okay-food4.jpg, okay-food11.jpg";zheltyj-struchkovyj-perets;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Фрукты и овощи";"Organik food";"Китайская петрушка Кориандр";;24e452;28.00;;4;;;;1;;"Китайская петрушка Кориандр";"Китайская петрушка Кориандр, Organik food, Фрукты и овощи";;;;"okay-food12.jpg, okay-food2.jpg, okay-food5.jpg, okay-food8.jpg, okay-food9.jpg";kitajskaya-petrushka-koriandr;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары,, Детские товары/Фрукты и овощи";"Organik food";"Савойская капуста";;rtrwt535;97.00;;4;;;;1;;"Савойская капуста";"Савойская капуста, Напитки и продукты";;;;"okay-food4.jpg, okay-food9.jpg, okay-food7.jpg, okay-food8.jpg";savojskaya-kapusta;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Польша;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Детские товары/Соки";"Nature product";Сок;;qwer21;34.00;;4;;;;1;1;Сок;"Сок, Напитки и продукты";;;;4-600x600.jpg;apelsinovyj-sok;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Vaillant;"Газовая колонка Vaillant MAG OE 11-0/0 XZ C+ H (пьезорозжиг)";;311187;195.00;212.00;4;0.00;;;1;1;"Газовая колонка Vaillant MAG OE 11-0/0 XZ C+ H (пьезорозжиг)";"Газовая колонка Vaillant MAG OE 11-0/0 XZ C+ H (пьезорозжиг)";"Газовая колонка Vaillant MAG OE 11-0/0 XZ C+ H (пьезорозжиг)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";mag_pro_370x500.png;gazovaya-kolonka-vaillant-mag-oe-11-00-xz-c-h-pezorozzhig;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;19.2кВт.;дымоход;пьезорозжиг;;11л.;"Две ступени";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Vaillant;"Газовая колонка Vaillant MAG mini OE 11-0/0 RXZ H (пьезорозжиг)";;311292;260.00;289.00;4;0.00;0;;1;;"Газовая колонка Vaillant MAG mini OE 11-0/0 RXZ H (пьезорозжиг)";"Газовая колонка Vaillant MAG mini OE 11-0/0 RXZ H (пьезорозжиг)";"Газовая колонка Vaillant MAG mini OE 11-0/0 RXZ H (пьезорозжиг)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.jpg";gazovaya-kolonka-vaillant-mag-mini-oe-11-00-rxz-h-pezorozzhig;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;19.2кВт.;дымоход;пьезорозжиг;;11л.;"Десять ступеней";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Vaillant;"Газовая колонка Vaillant MAG mini OE 11-0/0 RXI H (электро розжиг)";;311260;280.00;311.00;4;0.00;0;;1;0;"Газовая колонка Vaillant MAG mini OE 11-0/0 RXI H (электро розжиг)";"Газовая колонка Vaillant MAG mini OE 11-0/0 RXI H (электро розжиг)";"Газовая колонка Vaillant MAG mini OE 11-0/0 RXI H (электро розжиг)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.jpg";gazovaya-kolonka-vaillant-mag-mini-oe-11-00-rxi-h-elektro-rozzhig-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;18.6кВт.;дымоход;"от батареек (электро)";;11л.;"Десять ступеней";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Vaillant;"Газовая колонка Vaillant MAG OE 14-0/0 RXZ H (пьезорозжиг)";;311392;312.00;347.00;4;0.00;0;;1;0;"Газовая колонка Vaillant MAG OE 14-0/0 RXZ H (пьезорозжиг)";"Газовая колонка Vaillant MAG OE 14-0/0 RXZ H (пьезорозжиг)";"Газовая колонка Vaillant MAG OE 14-0/0 RXZ H (пьезорозжиг)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.jpg";gazovaya-kolonka-vaillant-mag-oe-14-00-rxz-h-pezorozzhig;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;24.4кВт.;дымоход;пьезорозжиг;;14л.;"Десять ступеней";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Vaillant;"Газовая колонка Vaillant MAG OE 14-0/0 RXI H (электро розжиг)";;311591;355.00;395.00;4;0.00;0;;1;1;"Газовая колонка Vaillant MAG OE 14-0/0 RXI H (электро розжиг)";"Газовая колонка Vaillant MAG OE 14-0/0 RXI H (электро розжиг)";"Газовая колонка Vaillant MAG OE 14-0/0 RXI H (электро розжиг)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.jpg";gazovaya-kolonka-vaillant-mag-oe-14-00-rxi-h-elektro-rozzhig;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;24.4кВт.;дымоход;"от батареек (электро)";;14л.;"Десять ступеней";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 2000 O W 10 KB";;7736500992;140.00;156.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 2000 O W 10 KB";"Газовая колонка Bosch Therm 2000 O W 10 KB";"Газовая колонка Bosch Therm 2000 O W 10 KB";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"THERM_2000_O.jpg, bosch-therm-2000.jpg";gazovaya-kolonka-bosch-therm-2000-o-w-10-kb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;17.4кВт.;дымоход;"от батареек (электро)";;10л/мин.;нет;10.00;0.04;580х310х220;0.15бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 4000 O W 10-2 P";;7701331010;144.00;160.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 4000 O W 10-2 P";"Газовая колонка Bosch Therm 4000 O W 10-2 P";"Газовая колонка Bosch Therm 4000 O W 10-2 P";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Therm_4000.jpg;gazovaya-kolonka-bosch-therm-4000-o-w-10-2-p;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;17.4кВт.;дымоход;пьезорозжиг;;10л/мин.;нет;11.00;0.04;580х310х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 4000 O WR 10-2 P";;7701331615;169.00;188.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 4000 O WR 10-2 P";"Газовая колонка Bosch Therm 4000 O WR 10-2 P";"Газовая колонка Bosch Therm 4000 O WR 10-2 P";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Therm_4000.jpg;gazovaya-kolonka-bosch-therm-4000-o-wr-10-2-p;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;17.4кВт.;дымоход;пьезорозжиг;;10л/мин.;есть;11.00;0.04;580х310х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 4000 O WR 10-2 B";;7701331617;263.00;293.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 4000 O WR 10-2 B";"Газовая колонка Bosch Therm 4000 O WR 10-2 B";"Газовая колонка Bosch Therm 4000 O WR 10-2 B";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";bossh_wr_13_2_b.jpg;gazovaya-kolonka-bosch-therm-4000-o-wr-10-2-b;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;17.4кВт.;дымоход;"от батареек (электро)";;10л/мин.;есть;11.00;0.04;580х310х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 6000 O WR 10-2 G";;7701331616;277.00;308.00;4;0.00;;;1;1;"Газовая колонка Bosch Therm 6000 O WR 10-2 G";"Газовая колонка Bosch Therm 6000 O WR 10-2 G";"Газовая колонка Bosch Therm 6000 O WR 10-2 G";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";bosch-wrd-13-2g.jpg;gazovaya-kolonka-bosch-therm-6000-o-wr-10-2-g;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;17.4кВт.;дымоход;"гидротурбинка (электро)";;10л/мин.;есть;11.00;0.04;580х310х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 4000 O WR 13-2 P";;7702331716;238.00;265.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 4000 O WR 13-2 P";"Газовая колонка Bosch Therm 4000 O WR 13-2 P";"Газовая колонка Bosch Therm 4000 O WR 13-2 P";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Therm_4000.jpg;gazovaya-kolonka-bosch-therm-4000-o-wr-13-2-p;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;22.6кВт.;дымоход;пьезорозжиг;;13л/мин.;есть;14.00;0.05;655х350х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 4000 O WR 13-2 B";;7702331718;282.00;314.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 4000 O WR 13-2 B";"Газовая колонка Bosch Therm 4000 O WR 13-2 B";"Газовая колонка Bosch Therm 4000 O WR 13-2 B";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";bossh_wr_13_2_b.jpg;gazovaya-kolonka-bosch-therm-4000-o-wr-13-2-b;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;22.6кВт.;дымоход;"от батареек (электро)";;13л/мин.;есть;14.00;0.05;655х350х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 6000 O WR 13-2 G";;7702331717;326.00;362.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 6000 O WR 13-2 G";"Газовая колонка Bosch Therm 6000 O WR 13-2 G";"Газовая колонка Bosch Therm 6000 O WR 13-2 G";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";bosch-wrd-13-2g.jpg;gazovaya-kolonka-bosch-therm-6000-o-wr-13-2-g;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;22.6кВт.;дымоход;"гидротурбинка (электро)";;13л/мин.;есть;14.00;0.05;655х350х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 4000 O WR 15-2 P";;7703331746;266.00;294.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 4000 O WR 15-2 P";"Газовая колонка Bosch Therm 4000 O WR 15-2 P";"Газовая колонка Bosch Therm 4000 O WR 15-2 P";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Therm_4000.jpg;gazovaya-kolonka-bosch-therm-4000-o-wr-15-2-p;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;26.2кВт.;дымоход;пьезорозжиг;;15л/мин.;есть;16.00;0.06;655х425х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 4000 O WR 15-2 B";;7703331748;320.00;356.00;4;0.00;;;1;0;"Газовая колонка Bosch Therm 4000 O WR 15-2 B";"Газовая колонка Bosch Therm 4000 O WR 15-2 B";"Газовая колонка Bosch Therm 4000 O WR 15-2 B";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Therm_4000.jpg;gazovaya-kolonka-bosch-therm-4000-o-wr-15-2-b;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;26.2кВт.;дымоход;"от батареек (электро)";;15л/мин.;есть;16.00;0.06;655х425х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Bosch;"Газовая колонка Bosch Therm 6000 O WR 15-2 G";;7703331747;350.00;389.00;4;0.00;;;1;1;"Газовая колонка Bosch Therm 6000 O WR 15-2 G";"Газовая колонка Bosch Therm 6000 O WR 15-2 G";"Газовая колонка Bosch Therm 6000 O WR 15-2 G";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";bosch-wrd-13-2g.jpg;gazovaya-kolonka-bosch-therm-6000-o-wr-15-2-g;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Португалия;;;;;;;;;;;;;;;;26.2кВт.;дымоход;"гидротурбинка (электро)";;15л/мин.;есть;16.00;0.06;655х425х220;0.1бар;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Beretta;"Газовая колонка Beretta Aqua Idrabagno 11";;20042533;130.00;;4;0.00;0;;1;0;"Газовая колонка Beretta Aqua Idrabagno 11";"Газовая колонка Beretta Aqua Idrabagno 11";"Газовая колонка Beretta Aqua Idrabagno 11";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Aqua_Idrabagno.jpg;gazovaya-kolonka-beretta-aqua-idrabagno-11;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;;;;;;;;;;;;;;;;"22 кВт.";дымоход;"пьезорозжиг (кнопкой)";"11 л/мин";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Beretta;"Газовая колонка Beretta Aqua Idrabagno 11i";;20042534;176.00;;4;0.00;0;;1;0;"Газовая колонка Beretta Aqua Idrabagno 11i";"Газовая колонка Beretta Aqua Idrabagno 11i";"Газовая колонка Beretta Aqua Idrabagno 11i";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Aqua_Idrabagno.jpg;gazovaya-kolonka-beretta-aqua-idrabagno-11i;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;;;;;;;;;;;;;;;;"22 кВт.";дымоход;"от батареек (электронный)";"11 л/мин";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Beretta;"Газовая колонка Beretta Aqua Idrabagno 14";;20042537;176.00;;4;0.00;0;;1;0;"Газовая колонка Beretta Aqua Idrabagno 14";"Газовая колонка Beretta Aqua Idrabagno 14";"Газовая колонка Beretta Aqua Idrabagno 14";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Aqua_Idrabagno.jpg;gazovaya-kolonka-beretta-aqua-idrabagno-14;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;;;;;;;;;;;;;;;;"27.2 кВт.";дымоход;"пьезорозжиг (кнопкой)";"14 л/мин";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Beretta;"Газовая колонка Beretta Aqua Idrabagno 14i";;20042540;211.00;;4;0.00;0;;1;0;"Газовая колонка Beretta Aqua Idrabagno 14i";"Газовая колонка Beretta Aqua Idrabagno 14i";"Газовая колонка Beretta Aqua Idrabagno 14i";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";Aqua_Idrabagno.jpg;gazovaya-kolonka-beretta-aqua-idrabagno-14i;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;;;;;;;;;;;;;;;;"27.2 кВт.";дымоход;"от батареек (электронный)";"14 л/мин";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 080 D400-1-M O'Pro Profi";;;2408.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 080 D400-1-M O'Pro Profi";"Бойлер Atlantic VM 080 D400-1-M O'Pro Profi";"Бойлер Atlantic VM 080 D400-1-M O'Pro Profi";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";atlantic-opro-profi.jpg;vodonagrevatel-atlantic-vm-080-d400-1-m-opro-profi;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;80л.;Вертикальный;Цилиндрическая;1.5кВт.;"3 года";"открытый (""мокрый"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 080 D400-2-B O'Pro Turbo";;080-D400-2-B;2836.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 080 D400-2-B O'Pro Turbo";"Бойлер Atlantic VM 080 D400-2-B O'Pro Turbo";"Бойлер Atlantic VM 080 D400-2-B O'Pro Turbo";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"atlantic-vm-080-d400-2-bc.jpg, opro-turbo-1.jpg, opro-turbo.jpg";vodonagrevatel-atlantic-vm-080-d400-2-b-opro-turbo;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;80л.;Вертикальный;Цилиндрическая;2.5кВт.;"5 лет";"открытый (""мокрый"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 30 S3С CUBE STEATITE";;;3949.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 30 S3С CUBE STEATITE";"Бойлер Atlantic VM 30 S3С CUBE STEATITE";"Бойлер Atlantic VM 30 S3С CUBE STEATITE";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"atlantic-statite-cube.jpg, atlantic-statite-cube.jpg";vodonagrevatel-atlantic-vm-30-s3s-cube-steatite;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;30л.;Вертикальный;прямоугольная;1.5кВт.;"7 лет";"скрытый (""сухой"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 75 S4 СМ CUBE STEATITE";;;5505.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 75 S4 СМ CUBE STEATITE";"Бойлер Atlantic VM 75 S4 СМ CUBE STEATITE";"Бойлер Atlantic VM 75 S4 СМ CUBE STEATITE";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"atlantic-statite-cube.jpg, atlantic-statite-cube.jpg";vodonagrevatel-atlantic-vm-75-s4-sm-cube-steatite;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;75л.;Универсальный;прямоугольная;2.4кВт.;"7 года";"скрытый (""сухой"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 100 S4 СМ CUBE STEATITE";;;6109.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 100 S4 СМ CUBE STEATITE";"Бойлер Atlantic VM 100 S4 СМ CUBE STEATITE";"Бойлер Atlantic VM 100 S4 СМ CUBE STEATITE";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"atlantic-statite-cube.jpg, atlantic-statite-cube.jpg";vodonagrevatel-atlantic-vm-100-s4-sm-cube-steatite;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;100л.;Универсальный;прямоугольная;2.4кВт.;"7 года";"скрытый (""сухой"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер ATLANTIC STEATITE SLIM VM 080 D325-2-BC";;;4301.00;;4;0.00;;;1;0;"Бойлер ATLANTIC STEATITE SLIM VM 080 D325-2-BC";"Бойлер ATLANTIC STEATITE SLIM VM 080 D325-2-BC";"Бойлер ATLANTIC STEATITE SLIM VM 080 D325-2-BC";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"atlantic-slim-steatite-vm-80-n3cm-e-1_1.jpg, atlantic-slim-steatite-80_1.jpg";bojler-atlantic-steatite-slim-vm-080-d325-2-bc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;80л.;Вертикальный;Цилиндрическая;2.1кВт.;"7 лет";"скрытый (""сухой"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic Steatite PRO VM 050 D400-2-BC";;;3832.00;;4;0.00;;;1;0;"Бойлер Atlantic Steatite PRO VM 050 D400-2-BC";"Бойлер Atlantic Steatite PRO VM 050 D400-2-BC";"Бойлер Atlantic Steatite PRO VM 050 D400-2-BC";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"Atlantic_VM_050_D400-2-BC.jpg, atlantic-statite-elite-pro3.jpg, atlantic-statite-elite-pro.jpg";vodonagrevatel-atlantic-vm-050-d400-2-bc-steatite-pro;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;50л.;Вертикальный;Цилиндрическая;1.5кВт.;"8 лет";"скрытый (""сухой"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic PC 30 O'Pro Slim";;;3177.00;;4;0.00;;;1;0;"Бойлер Atlantic PC 30 O'Pro Slim";"Бойлер Atlantic PC 30 O'Pro Slim";"Бойлер Atlantic PC 30 O'Pro Slim";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"atlantic-opro-slim.jpg, atlantic-opro-slim-2.jpg";vodonagrevatel-atlantic-pc-30-opro-slim;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;30л.;Вертикальный;Цилиндрическая;2кВт.;"5 лет";"открытый (""мокрый"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 050 D400-1-M O'Pro Profi";;;2231.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 050 D400-1-M O'Pro Profi";"Бойлер Atlantic VM 050 D400-1-M O'Pro Profi";"Бойлер Atlantic VM 050 D400-1-M O'Pro Profi";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";atlantic-opro-profi.jpg;vodonagrevatel-atlantic-vm-050-d400-1-m-opro-profi;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;50л.;Вертикальный;Цилиндрическая;1.5кВт.;"3 года";"открытый (""мокрый"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 100 D400-1-M O'Pro Profi";;;2754.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 100 D400-1-M O'Pro Profi";"Бойлер Atlantic VM 100 D400-1-M O'Pro Profi";"Бойлер Atlantic VM 100 D400-1-M O'Pro Profi";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";atlantic-opro-profi.jpg;vodonagrevatel-atlantic-vm-100-d400-1-m-opro-profi;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;100л.;Вертикальный;Цилиндрическая;1.5кВт.;"3 года";"открытый (""мокрый"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 050 D400-2-B O'Pro Turbo";;;2601.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 050 D400-2-B O'Pro Turbo";"Бойлер Atlantic VM 050 D400-2-B O'Pro Turbo";"Бойлер Atlantic VM 050 D400-2-B O'Pro Turbo";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"Atlantic_VM_050_D400-2-B2.jpg, opro-turbo.jpg";vodonagrevatel-atlantic-vm-050-d400-2-b-opro-turbo;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;50л.;Вертикальный;Цилиндрическая;2.5кВт.;"5 лет";"открытый (""мокрый"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Gadgets/Водонагреватели;Atlantic;"Бойлер Atlantic VM 100 D400-2-B O'Pro Turbo";;;3168.00;;4;0.00;;;1;0;"Бойлер Atlantic VM 100 D400-2-B O'Pro Turbo";"Бойлер Atlantic VM 100 D400-2-B O'Pro Turbo";"Бойлер Atlantic VM 100 D400-2-B O'Pro Turbo";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"atlantic-vm-080-d400-2-bc.jpg, opro-turbo-1.jpg, opro-turbo.jpg";vodonagrevatel-atlantic-vm-100-d400-2-b-opro-turbo;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;;;;;;;;;Настенный;100л.;Вертикальный;Цилиндрическая;2.5кВт.;"5 лет";"открытый (""мокрый"")";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Gadgets/Радиаторы отопления";Global;"Алюминиевый радиатор Global VOX 500/100";;vox500/100;235.04;;4;0.00;;;1;1;"Алюминиевый радиатор Global VOX 500/100";"Алюминиевый радиатор Global VOX 500/100";"Алюминиевый радиатор Global VOX 500/100";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";1362382587_aluminium-radiator-global.jpg;alyuminievyj-radiator-global-vox-500100;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Gadgets/Радиаторы отопления";Global;"Алюминиевый радиатор Global VOX 350/100";;vox350/100;235.03;;4;0.00;;;1;0;"Алюминиевый радиатор Global VOX 350/100";"Алюминиевый радиатор Global VOX 350/100";"Алюминиевый радиатор Global VOX 350/100";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";1362382587_aluminium-radiator-global.jpg;alyuminievyj-radiator-global-vox-350100;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;145Вт.;110град.;"16 бар";Секционная;Боковое;10;0.35л.;"2.4 бар";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Gadgets/Радиаторы отопления";Global;"Алюминиевый радиатор Global ISEO 500/80";;iseo500/80;235.02;;4;0.00;;;1;0;"Алюминиевый радиатор Global ISEO 500/80";"Алюминиевый радиатор Global ISEO 500/80";"Алюминиевый радиатор Global ISEO 500/80";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";iseo.jpg;alyuminievyj-radiator-global-iseo-50080;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;181Вт.;110град.;"16 бар";Секционная;Боковое;10;0.5л.;"2.4 бар";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Gadgets/Радиаторы отопления";Ferroli;"Радиаторы алюминиевые Ferroli Infiniti 500/10";;86141;235.01;;4;0.00;;;1;0;"Радиаторы алюминиевые Ferroli Infiniti 500/10";"Радиаторы алюминиевые Ferroli Infiniti 500/10";"Радиаторы алюминиевые Ferroli Infiniti 500/10";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";record_137200700.jpg;radiatory-alyuminievye-ferroli-infiniti-50010;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Италия;;"116 град.";"16 бар";Секционная;Боковое;"10 секц.";0.37л;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Gadgets/Радиаторы отопления";Алтермо;"Биметаллический радиатор АЛТЕРМО ЛРБ 500";;;235.00;;4;0.00;;;1;0;"Биметаллический радиатор АЛТЕРМО ЛРБ 500";"Биметаллический радиатор АЛТЕРМО ЛРБ 500";"Биметаллический радиатор АЛТЕРМО ЛРБ 500";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";lrb500.jpg;bimetallicheskie-radiator-altermo-lrb;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;169Вт.;130°C;18бар;Секционная;Боковое;"10 секций";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Gadgets/Радиаторы отопления";Алтермо;"Биметаллический радиатор АЛТЕРМО РИО 500";;;220.00;;4;0.00;;;1;0;"Биметаллический радиатор АЛТЕРМО РИО 500";"Биметаллический радиатор АЛТЕРМО РИО 500";"Биметаллический радиатор АЛТЕРМО РИО 500";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";bimetall-ALTERMO-400x400.jpg;bimetallicheskie-radiator-altermo-rio;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Украина;166Вт.;130°C;18бар;Секционная;Боковое;"10 секций";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Аксессуар-препятствие для трека (4 вида)";;DLF01;149.00;;4;0.00;;;1;0;"Hot Wheels Аксессуар-препятствие для трека (4 вида)";"Hot Wheels Аксессуар-препятствие для трека (4 вида)";"Hot Wheels Аксессуар-препятствие для трека (4 вида)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_726.jpg, 2_625.jpg, 3_580.jpg, 5_444.jpg, 6_402.jpg, 4_529.jpg, 7_319.jpg";hot-wheels-aksessuar-prepyatstvie-dlya-treka-4-vida;;;;;;;;;;;"Track builder";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;"28 х 16 см";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Набор дополнительных дорожек трека";;CCX79;109.00;;4;0.00;;;1;0;"Hot Wheels Набор дополнительных дорожек трека";"Hot Wheels Набор дополнительных дорожек трека";"Hot Wheels Набор дополнительных дорожек трека";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"3_579.jpg, 2_624.jpg, 1_725.jpg";hot-wheels-nabor-dopolnitelnyh-dorozhek-treka;;;;;;;;;;;"Track builder";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;"31 х 5 х 3 см";"30 см (длина одной дорожки)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Игровой трек ""Перекрестное столкновение""";;DTN42;2799.00;;4;0.00;;;1;0;"Hot Wheels Игровой трек ""Перекрестное столкновение""";"Hot Wheels Игровой трек ""Перекрестное столкновение""";"Hot Wheels Игровой трек ""Перекрестное столкновение""";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"9_150.jpg, 5_443.jpg, 1_724.jpg, 2_623.jpg, 6_401.jpg, 7_318.jpg, 3_578.jpg, 8_218.jpg, 4_528.jpg";hot-wheels-igrovoj-trek-perekrestnoe-stolknovenie;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5+;мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Трек ""Противостояние""";;DHY25;1499.00;1979.00;4;0.00;;;1;0;"Hot Wheels Трек ""Противостояние""";"Hot Wheels Трек ""Противостояние""";"Hot Wheels Трек ""Противостояние""";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_723.jpg, 2_622.jpg, 7_317.jpg, 3_577.jpg, 6_400.jpg, 5_442.jpg, 4_527.jpg";hot-wheels-trek-protivostoyanie;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;;"1:64 (машинка)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Трек ""Рекордный прыжок""";;DJC05;1599.00;;4;0.00;;;1;0;"Hot Wheels Трек ""Рекордный прыжок""";"Hot Wheels Трек ""Рекордный прыжок""";"Hot Wheels Трек ""Рекордный прыжок""";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_722.jpg, 2_621.jpg, 4_526.jpg, 3_576.jpg";hot-wheels-trek-rekordnyj-pryzhok;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;"51 х 30.5 х 7 см";"1:64 (машинка)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Игровой набор ""Атака дракона""";;DWL04;999.00;;4;0.00;;;1;0;"Hot Wheels Игровой набор ""Атака дракона""";"Hot Wheels Игровой набор ""Атака дракона""";"Hot Wheels Игровой набор ""Атака дракона""";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"6_399.jpg, 7_316.jpg, 4_525.jpg, 2_620.jpg, 3_575.jpg, 1_721.jpg, 5_441.jpg";hot-wheels-igrovoj-nabor-ataka-drakona;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Набор ""Готов к игре!"" (несколько видов)";;X9295;799.00;999.00;4;0.00;;;1;0;"Hot Wheels Набор ""Готов к игре!"" (несколько видов)";"Hot Wheels Набор ""Готов к игре!"" (несколько видов)";"Hot Wheels Набор ""Готов к игре!"" (несколько видов)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"3_574.jpg, 2_619.jpg, 1_720.jpg";hot-wheels-nabor-gotov-k-igre-neskolko-vidov;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;"25х25х9 см";"1:64 (машинка)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Базовый трек (3 вида)";;DNR74;699.00;799.00;4;0.00;;;1;0;"Hot Wheels Базовый трек (3 вида)";"Hot Wheels Базовый трек (3 вида)";"Hot Wheels Базовый трек (3 вида)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"2_618.jpg, 5_440.jpg, 9_149.jpg, 1_719.jpg, 3_573.jpg, 4_524.jpg, 6_398.jpg, 8_217.jpg, 7_315.jpg";hot-wheels-bazovyj-trek-3-vida;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;"40.5 х 19 х 6 см";"1:64 (машинка)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Игровой набор ""Автосервис"" (4 вида)";;DWK99;499.00;;4;0.00;;;1;0;"Hot Wheels Игровой набор ""Автосервис"" (4 вида)";"Hot Wheels Игровой набор ""Автосервис"" (4 вида)";"Hot Wheels Игровой набор ""Автосервис"" (4 вида)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_718.jpg, 9_148.jpg, 5_439.jpg, 3_572.jpg, 2_617.jpg, 4_523.jpg, 6_397.jpg, 7_314.jpg, 8_216.jpg";hot-wheels-igrovoj-nabor-avtoservis-4-vida;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;;"1:64 (машинка)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Игровой набор ""Город чудовищ"" (3 вида)";;DWK94;599.00;;4;0.00;;;1;0;"Hot Wheels Игровой набор ""Город чудовищ"" (3 вида)";"Hot Wheels Игровой набор ""Город чудовищ"" (3 вида)";"Hot Wheels Игровой набор ""Город чудовищ"" (3 вида)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"2_616.jpg, 1_717.jpg, 8_215.jpg, 7_313.jpg, 6_396.jpg, 3_571.jpg, 4_522.jpg, 5_438.jpg, 9_147.jpg";hot-wheels-igrovoj-nabor-gorod-chudovisch-3-vida;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;;"1:64 (машинка)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Карманный трек ""Гонки"" (несколько видов)";;CKJ08;399.00;;4;0.00;;;1;0;"Hot Wheels Карманный трек ""Гонки"" (несколько видов)";"Hot Wheels Карманный трек ""Гонки"" (несколько видов)";"Hot Wheels Карманный трек ""Гонки"" (несколько видов)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_716.jpg, 2_615.jpg, 3_570.jpg, 7_312.jpg, 4_521.jpg, 5_437.jpg, 6_395.jpg, 8_214.jpg, 9_146.jpg";hot-wheels-karmannyj-trek-gonki-neskolko-vidov;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;"21.5 х 16 х 6 см";"1:64 (машинка)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Легендарный гараж";;CMP80;7499.00;;4;0.00;;;1;0;"Hot Wheels Легендарный гараж";"Hot Wheels Легендарный гараж";"Hot Wheels Легендарный гараж";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"2_614.jpg, 3_569.jpg, 1_715.jpg, 4_520.jpg, 7_311.jpg, 6_394.jpg, 5_436.jpg, 8_213.jpg";hot-wheels-legendarnyj-garazh;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5+;мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Грузовик-транспортёр";;CKC09;949.00;;4;0.00;0;;1;0;"Hot Wheels Грузовик-транспортёр";"Hot Wheels Грузовик-транспортёр";"Hot Wheels Грузовик-транспортёр";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"3_568.jpg, 2_613.jpg, 4_519.jpg, 1_714.jpg";hot-wheels-gruzovik-transporter;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;3+;мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Трек ""Мега Прыжок"" (несколько видов)";;DNN77;299.00;;4;0.00;;;1;0;"Hot Wheels Трек ""Мега Прыжок"" (несколько видов)";"Hot Wheels Трек ""Мега Прыжок"" (несколько видов)";"Hot Wheels Трек ""Мега Прыжок"" (несколько видов)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"2_612.jpg, 3_567.jpg, 1_713.jpg";hot-wheels-trek-mega-pryzhok-neskolko-vidov;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;5+;мальчик;;;"31 х 8х 5 см";"1:64 (машинка)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Трек ""Охота на акулу"" серии ""Измени цвет""";;BGK04;999.90;1249.00;4;0.00;;;1;0;"Hot Wheels Трек ""Охота на акулу"" серии ""Измени цвет""";"Hot Wheels Трек ""Охота на акулу"" серии ""Измени цвет""";"Hot Wheels Трек ""Охота на акулу"" серии ""Измени цвет""";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"bgk04_1.jpg, bgk04_5.jpg, bgk04_4.jpg";hot-wheels-trek-ohota-na-akulu-serii-izmeni-tsvet;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;"2 емкости с водой. 1 машинка серии ""Измени цвет""";Пластик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Игровой набор ""Приключения в большом городе"" (2 вида)";;CDM44;399.00;469.00;4;0.00;;;1;0;"Hot Wheels Игровой набор ""Приключения в большом городе"" (2 вида)";"Hot Wheels Игровой набор ""Приключения в большом городе"" (2 вида)";"Hot Wheels Игровой набор ""Приключения в большом городе"" (2 вида)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_99.jpg, 2_95.jpg, 3_82.jpg, 4_79.jpg, 5_62.jpg";hot-wheels-igrovoj-nabor-priklyucheniya-v-bolshom-gorode-v-assort;;;;;;;;;;;"Базовые треки и игровые наборы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Игровой набор ""Взрыв цветов"" (2 вида)";;BHN12;449.00;;4;0.00;0;;1;0;"Hot Wheels Игровой набор ""Взрыв цветов"" (2 вида)";"Hot Wheels Игровой набор ""Взрыв цветов"" (2 вида)";"Hot Wheels Игровой набор ""Взрыв цветов"" (2 вида)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_101.jpg, 2_97.jpg, 3_84.jpg, 4_81.jpg";hot-wheels-igrovoj-nabor-vzryv-tsvetov-v-assort;;;;;;;;;;;City;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Набор Суперскорость";;BGX82;399.00;;4;0.00;0;;1;0;"Hot Wheels Набор Суперскорость";"Hot Wheels Набор Суперскорость";"Hot Wheels Набор Суперскорость";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_103.jpg, 2_99.jpg, 6_53.jpg, 4_83.jpg, 5_65.jpg, 3_86.jpg";hot-wheels-nabor-superskorost;;;;;;;;;;;Workshop;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Hot Wheels Набор Препятствия в пути (2 вида)";;BGX66;199.00;;4;0.00;0;;1;0;"Hot Wheels Набор Препятствия в пути (2 вида)";"Hot Wheels Набор Препятствия в пути (2 вида)";"Hot Wheels Набор Препятствия в пути (2 вида)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_102.jpg, 4_82.jpg, 2_98.jpg, 5_64.jpg, 3_85.jpg, 6_52.jpg";hot-wheels-nabor-prepyatstviya-v-puti;;;;;;;;;;;Workshop;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Kids cars";;"Машинка Hot Wheels ""Измени цвет""";;BHR15;199.00;249.00;4;0.00;;;1;0;"Машинка Hot Wheels ""Измени цвет""";"Машинка Hot Wheels ""Измени цвет""";"Машинка Hot Wheels ""Измени цвет""";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"6_51.jpg, 1_100.jpg, 2_96.jpg, 7_37.jpg, 9_7.jpg, 3_83.jpg, 8_25.jpg, 5_63.jpg, 4_80.jpg";mashinka-hot-wheels-izmeni-tsvet-v-assort;;;;;;;;;;;City;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;3+;мальчик;"1 машинка";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Stuffed toys";;"Hatchimals: Драко в яйце номер 2";;SM19100/6034335;2429.00;;4;0.00;0;;1;0;"Hatchimals: Драко в яйце номер 2";"Hatchimals: Драко в яйце номер 2";"Hatchimals: Драко в яйце номер 2";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_94.jpg, 2_92.jpg, 7_36.jpg, 6_50.jpg, 8_24.jpg, 9_6.jpg, 4_77.jpg, 5_60.jpg, 3_80.jpg";hatchimals-drako-v-yajtse--2;;;;;;;;;;;"Другие интерактивные питомцы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;девочка..мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Stuffed toys";;"Hatchimals: Пингви в яйце номер 2";;SM19100/6034333;2429.00;;4;0.00;0;;1;0;"Hatchimals: Пингви в яйце номер 2";"Hatchimals: Пингви в яйце номер 2";"Hatchimals: Пингви в яйце номер 2";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_93.jpg, 2_91.jpg, 7_35.jpg, 6_49.jpg, 8_23.jpg, 9_5.jpg, 4_76.jpg, 5_59.jpg, 3_79.jpg";hatchimals-pingvi-v-yajtse--2;;;;;;;;;;;"Другие интерактивные питомцы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;девочка..мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Stuffed toys";;"Hatchimals: Драко в яйце номер 1";;SM19100/6028895;2699.00;;4;0.00;1;;1;;"Hatchimals: Драко в яйце номер 1";"Hatchimals: Драко в яйце номер 1";"Hatchimals: Драко в яйце номер 1";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_92.jpg, 2_90.jpg, 5_58.jpg, 6_48.jpg, 7_34.jpg, 4_75.jpg, 3_78.jpg";hatchimals-drako-v-yajtse-1;;;;;;;;;;;"Другие интерактивные питомцы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;девочка..мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Kids toys/Stuffed toys";;"Hatchimals: Пингви в яйце номер 1";;SM19100/6028874;2429.00;2699.00;4;0.00;;;1;1;"Hatchimals: Пингви в яйце номер 1";"Hatchimals: Пингви в яйце номер 1";"Hatchimals: Пингви в яйце номер 1";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"1_91.jpg, 2_89.jpg, 5_57.jpg, 6_47.jpg, 8_21.jpg, 9_4.jpg, 4_74.jpg, 7_33.jpg, 3_77.jpg";hatchimals-pingvi-v-yajtse--1;;;;;;;;;;;"Другие интерактивные питомцы";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;4+;девочка..мальчик;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi 4X - 2+16Gb";"В наличии";;5000.00;;4;0.00;;;1;;"Телефон Xiaomi Redmi 4X - 2+16Gb";"Телефон Xiaomi Redmi 4X - 2+16Gb";"Телефон Xiaomi Redmi 4X - 2+16Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";4h.jpg;telefon-xiaomi-redmi-4x---216gb;;"150 г";;;;;;;;;"Redmi 4";"5.0""";IPS;"1280x720 пикс.";"Android 6.0.1 + MIUI V8";"8 (Cortex A-53)";"Snapdragon 435";"1.4 ГГц";"2 ГБ";"16 ГБ";"до 64 ГБ";"2 sim";"micro-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";"1800 / 1900 / 2100 / 2300 / 2500 / 2600 МГц";"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.2;Есть;Есть;"802.11 b/g/n";Есть;"microUSB. Аудио 3.5 мм";"4100 мАч";"139.2 x 70.0 x 8.7 мм";"Черный. Champagne. розовый";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi 4X - 2+16Gb";синий;;5100.00;;4;;;;1;;"Телефон Xiaomi Redmi 4X - 2+16Gb";"Телефон Xiaomi Redmi 4X - 2+16Gb";"Телефон Xiaomi Redmi 4X - 2+16Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";4h.jpg;telefon-xiaomi-redmi-4x---216gb;;"150 г";;;;;;;;;"Redmi 4";"5.0""";IPS;"1280x720 пикс.";"Android 6.0.1 + MIUI V8";"8 (Cortex A-53)";"Snapdragon 435";"1.4 ГГц";"2 ГБ";"16 ГБ";"до 64 ГБ";"2 sim";"micro-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";"1800 / 1900 / 2100 / 2300 / 2500 / 2600 МГц";"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.2;Есть;Есть;"802.11 b/g/n";Есть;"microUSB. Аудио 3.5 мм";"4100 мАч";"139.2 x 70.0 x 8.7 мм";"Черный. Champagne. розовый";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi 4X - 2+16Gb";красный;;5200.00;;4;;;;1;;"Телефон Xiaomi Redmi 4X - 2+16Gb";"Телефон Xiaomi Redmi 4X - 2+16Gb";"Телефон Xiaomi Redmi 4X - 2+16Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";4h.jpg;telefon-xiaomi-redmi-4x---216gb;;"150 г";;;;;;;;;"Redmi 4";"5.0""";IPS;"1280x720 пикс.";"Android 6.0.1 + MIUI V8";"8 (Cortex A-53)";"Snapdragon 435";"1.4 ГГц";"2 ГБ";"16 ГБ";"до 64 ГБ";"2 sim";"micro-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";"1800 / 1900 / 2100 / 2300 / 2500 / 2600 МГц";"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.2;Есть;Есть;"802.11 b/g/n";Есть;"microUSB. Аудио 3.5 мм";"4100 мАч";"139.2 x 70.0 x 8.7 мм";"Черный. Champagne. розовый";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi 3S - 3+32Gb";;;154.45;;4;0.00;;;1;0;"Телефон Xiaomi Redmi 3S - 3+32Gb";"Телефон Xiaomi Redmi 3S - 3+32Gb";"Телефон Xiaomi Redmi 3S - 3+32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s020304-01.jpg;telefon-xiaomi-redmi-3s---332gb;;"144 г";;;;;;;;;"Redmi 3S";"5.0""";IPS;"1280x720 пикс.";"Android 6.0.1 + MIUI V8";"8 (Cortex A-53)";"Snapdragon 430";"1.4 ГГц";"3 ГБ";"32 ГБ";"до 128 ГБ";"2 sim";"micro-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";"1800 / 1900 / 2100 / 2300 / 2500 / 2600 МГц";"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.1;Есть;Есть;"802.11 b/g/n";Есть;"microUSB. OTG. Аудио 3.5 мм";"4100 мАч";"139.3 x 69.6 x 8.5 мм";"Серый. серебряный. золотой";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi Note 4X - 4+64Gb";;;0.00;;4;0.00;;;1;0;"Телефон Xiaomi Redmi Note 4X - 4+64Gb";"Телефон Xiaomi Redmi Note 4X - 4+64Gb";"Телефон Xiaomi Redmi Note 4X - 4+64Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s020306-01.jpg;telefon-xiaomi-redmi-note-4x---464gb;;"175 г";;;;;;;;;"Redmi Note";"5.5""";IPS;"1080x1920 пикс.";"Android 6.0.1 + MIUI V8";"8 (Cortex A-53)";"Snapdragon 625";"2.0 ГГц";"4 ГБ";"64 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. micro-SIM (microSD)";"850 / 900 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";"1800. 1900. 2100. 2300. 2500. 2600. 850. 900 МГц";"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;"4.2 LE";Есть;Есть;"802.11 a/b/g/n/ac";Есть;"microUSB. OTG. Аудио 3.5 мм";"4100 мАч";"151.0 x 76.0 x 8.4 мм";"Черный. розовый. золотой. серебряный. бирюзовый";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi Note 4X - 3+32Gb";"В наличии";;179.71;;4;0.00;;;1;0;"Телефон Xiaomi Redmi Note 4X - 3+32Gb";"Телефон Xiaomi Redmi Note 4X - 3+32Gb";"Телефон Xiaomi Redmi Note 4X - 3+32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s010303-01.jpg;telefon-xiaomi-redmi-note-4x---332gb;;"175 г";;;;;;;;;"Redmi Note";"5.5""";IPS;"1080x1920 пикс.";"Android 6.0.1 + MIUI V8";"8 (Cortex A-53)";"Snapdragon 625";"2.0 ГГц";"3 ГБ";"32 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. micro-SIM (microSD)";"850 / 900 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";"1800. 1900. 2100. 2300. 2500. 2600. 850. 900 МГц";"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;"4.2 LE";Есть;Есть;"802.11 a/b/g/n/ac";Есть;"microUSB. OTG. Аудио 3.5 мм";"4100 мАч";"151.0 x 76.0 x 8.4 мм";"Черный. розовый. золотой. серебряный. бирюзовый";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi Note 4X - 2+32Gb";;;0.00;;4;0.00;;;1;0;"Телефон Xiaomi Redmi Note 4X - 2+32Gb";"Телефон Xiaomi Redmi Note 4X - 2+32Gb";"Телефон Xiaomi Redmi Note 4X - 2+32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s020307-01.jpg;telefon-xiaomi-redmi-note-4x;;"175 г";;;;;;;;;"Redmi Note";"5.5""";IPS;"1080x1920 пикс.";"Android 6.0.1 + MIUI V8";"8 (Cortex A-53)";"Snapdragon 625";"2.0 ГГц";"2 ГБ";"32 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. micro-SIM (microSD)";"850 / 900 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";"1800. 1900. 2100. 2300. 2500. 2600. 850. 900 МГц";"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;"4.2 LE";Есть;Есть;"802.11 a/b/g/n/ac";Есть;"microUSB. OTG. Аудио 3.5 мм";"4100 мАч";"151.0 x 76.0 x 8.4 мм";"Черный. розовый. золотой. серебряный. бирюзовый";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi 4";"В наличии";;131.20;;4;0.00;;;1;0;"Телефон Xiaomi Redmi 4";"Телефон Xiaomi Redmi 4";"Телефон Xiaomi Redmi 4";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";ss210205-01.jpg;telefon-xiaomi-redmi-4;;"160 г";;;;;;;;;;"5.0""";IPS;"1280x720 пикс.";"Android 6.0.1 + MIUI V8";"8 (Cortex A-53)";"Snapdragon 430";"1.4 ГГц";"2 ГБ";"16 ГБ";"до 64 ГБ";"2 sim";"micro-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";"1800 / 1900 / 2100 / 2300 / 2500 / 2600 МГц";"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.1;Есть;Есть;"802.11 b/g/n";Есть;"microUSB. OTG. Аудио 3.5 мм";"4100 мАч";"141.3 x 69.6 x 8.9 мм";"Серый. серебряный. золотой";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi 4 Pro";"В наличии";;195.76;;4;0.00;0;;1;0;"Телефон Xiaomi Redmi 4 Pro";"Телефон Xiaomi Redmi 4 Pro";"Телефон Xiaomi Redmi 4 Pro";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s020301-01.jpg;telefon-xiaomi-redmi-4-pro;;"156 г";;;;;;;;;"Redmi 4";"5.0""";IPS;"1080x1920 пикс.";"Android 6.0.1 + MIUI 8";8;"Snapdragon 625";"2.0 ГГц";"3 ГБ";"32 ГБ";"до 128 ГБ";"2 sim";"micro-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";FDD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.2;Есть;Есть;"802.11 b/g/n";Есть;;"4100 мАч";"141.3 x 69.6 x 8.9 мм";"Темно-серый. золотой. серебряный";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Mi5s Plus 6+128Gb";"В наличии";;400.74;;4;0.00;1;;1;0;"Телефон Xiaomi Mi5s Plus 6+128Gb";"Телефон Xiaomi Mi5s Plus 6+128Gb";"Телефон Xiaomi Mi5s Plus 6+128Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";151220-01.jpg;telefon-xiaomi-mi5s-plus-6128-gb;;"168 г";;;;;;;;;Mi5s;"5.7""";IPS;"1080x1920 пикс.";"Android 6.0.1 + MIUI 8";4;"Snapdragon 821";"2.35 ГГц";"6 ГБ";"128 ГБ";;"2 sim";"nano-SIM. nano-SIM";"850 / 900 / 1800 / 1900 МГц";"850 / 1900 / 2100 МГц";"1800. 1900. 2100. 2300. 2500. 2600. 850. 900 МГц";"850 / 900 / 1900 / 2100 МГц";"двойная. по 13 Мп каждая";"4 Мп";Есть;4.2;Есть;Есть;"802.11 a/b/g/n/ac";Есть;"microUSB. Type-C. OTG. Аудио 3.5 мм";"3800 мАч";"154.6 х 77.7 х 8 мм";"Серый. серебряный. золотой. Rose-gold";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Mi5s Plus 4+64Gb";;;0.00;;4;0.00;;;1;0;"Телефон Xiaomi Mi5s Plus 4+64Gb";"Телефон Xiaomi Mi5s Plus 4+64Gb";"Телефон Xiaomi Mi5s Plus 4+64Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";151218-01.jpg;telefon-xiaomi-mi5s-plus-464-gb;;"168 г";;;;;;;;;Mi5s;"5.7""";IPS;"1080x1920 пикс.";"Android 6.0.1 + MIUI 8";4;"Snapdragon 82";"2.35 ГГц";"4 ГБ";"64 ГБ";;"2 sim";"nano-SIM. nano-SIM";"850 / 900 / 1800 / 1900 МГц";"850 / 1900 / 2100 МГц";"1800. 1900. 2100. 2300. 2500. 2600. 850. 900 МГц";"850 / 900 / 1900 / 2100 МГц";"двойная. по 13 Мп каждая";"4 Мп";Есть;4.2;Есть;Есть;"802.11 a/b/g/n/ac";Есть;"microUSB. Type-C. OTG. Аудио 3.5 мм";"3800 мАч";"154.6 х 77.7 х 8 мм";"Серый. серебряный. золотой. Rose-gold";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi Note 4 - 3+32Gb";"В наличии";;178.52;;4;0.00;;;1;0;"Телефон Xiaomi Redmi Note 4 - 3+32Gb";"Телефон Xiaomi Redmi Note 4 - 3+32Gb";"Телефон Xiaomi Redmi Note 4 - 3+32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";151213-02.jpg;telefon-xiaomi-redmi-note-4---332-gb;;"175 г";;;;;;;;;"Redmi Note 4";"5.5""";IPS;"1080x1920 пикс.";"Android 6.0.1 + MIUI 8";10;"MediaTek MT6797";"2.1 ГГц";"3 ГБ";"32 ГБ";"до 128 ГБ";"2 sim";"micro-SIM. nano-SIM (microSD)";"900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";FDD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;"4.2 LE";Есть;Есть;"802.11 a/b/g/n/ac";Есть;"microUSB. Type-C. OTG. Аудио 3.5 мм";"4100 мАч";"151.0 x 76.0 x 8.35 мм";"Серый. серебряный. золотой";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Mi4C 3+32Gb";"Под заказ";;4200.00;;4;0.00;;;1;0;"Телефон Xiaomi Mi4C 3+32Gb";"Телефон Xiaomi Mi4C 3+32Gb";"Телефон Xiaomi Mi4C 3+32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040753-02.jpg;telefon-xiaomi-mi4c-332-gb;;"132 г";;;;;;;;;Mi4C;"5.0""";"IPS Full HD";"1080x1920 пикс.";"Android 5.1";"6 (Cortex A-57)";"Snapdragon 808";"1.8 ГГц";"3 ГБ";"32 ГБ";;"2 sim";micro-SIM;"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.1;Есть;Есть;"802.11 a/b/g/n/ac";Есть;"microUSB. Аудио 3.5 мм";"3080 мАч";"138.1 х 69.59 x 7.8 мм";"Черный. голубой. розовый. белый. желтый";Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi 2 Enhanced 2+16Gb";"Под заказ";;2700.00;;4;0.00;10;;1;0;"Телефон Xiaomi Redmi 2 Enhanced 2+16Gb";"Телефон Xiaomi Redmi 2 Enhanced 2+16Gb";"Телефон Xiaomi Redmi 2 Enhanced 2+16Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040727-01.jpg;telefon-xiaomi-redmi-2-enhanced-216-gb;;"134 г";;;;;;;;;;"4.7""";"IPS HD";"1280x720 пикс.";"Android 4.4";"4 (Cortex A-53)";"Qualcomm Snapdragon 410";"1.2 ГГц";"2 ГБ";"16 ГБ";"до 32 ГБ";"2 sim";micro-SIM;"900 / 1800 МГц";"850 / 1900 / 2100 МГц";TD-LTE;"900 / 1900 / 2100 МГц";"8 Мп";"2 Мп";Есть;4.0;Есть;Есть;"802.11 b/g/n";;"microUSB. Аудио 3.5 мм";"2200 мАч";"134.0 x 67.2 x 9.2 мм";"Черный. голубой. розовый. белый. желтый";Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Xiaomi;;"Телефон Xiaomi Redmi 2 Enhanced 2+16Gb";"Из наличия";;2915.00;;4;0.00;50;;1;0;"Телефон Xiaomi Redmi 2 Enhanced 2+16Gb";"Телефон Xiaomi Redmi 2 Enhanced 2+16Gb";"Телефон Xiaomi Redmi 2 Enhanced 2+16Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040727-01.jpg;telefon-xiaomi-redmi-2-enhanced-216-gb;;"134 г";;;;;;;;;;"4.7""";"IPS HD";"1280x720 пикс.";"Android 4.4";"4 (Cortex A-53)";"Qualcomm Snapdragon 410";"1.2 ГГц";"2 ГБ";"16 ГБ";"до 32 ГБ";"2 sim";micro-SIM;"900 / 1800 МГц";"850 / 1900 / 2100 МГц";TD-LTE;"900 / 1900 / 2100 МГц";"8 Мп";"2 Мп";Есть;4.0;Есть;Есть;"802.11 b/g/n";;"microUSB. Аудио 3.5 мм";"2200 мАч";"134.0 x 67.2 x 9.2 мм";"Черный. голубой. розовый. белый. желтый";Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu M3 Note 16Gb";"В наличии";;136.71;;4;0.00;;;1;0;"Телефон Meizu M3 Note 16Gb";"Телефон Meizu M3 Note 16Gb";"Телефон Meizu M3 Note 16Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040704-01.jpg;telefon-meizu-m3-note-16-gb;;"163 г";;;;;;;;;M3;"5.5""";"IPS Full HD";"1080x1920 пикс.";"Android 5.1";"8 (Cortex A-53)";"MediaTek MT6755";"4 x 1.8 Ггц + 4 x 1.0 ГГц";"2 ГБ";"16 ГБ";"до 128 ГБ";"2 sim";nano-SIM;"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n";;"microUSB. Аудио 3.5 мм";"4100 мАч";"153.6 х 75.5 х 8.2 мм";"Белый. серый. золотой";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu Pro 5 64Gb";"В наличии";;396.43;;4;0.00;;;1;0;"Телефон Meizu Pro 5 64Gb";"Телефон Meizu Pro 5 64Gb";"Телефон Meizu Pro 5 64Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040718-02.jpg;telefon-meizu-pro-5-m576-64-gb;;"168 г";;;;;;;;;Pro;"5.7""";"Super AMOLED Full HD";"1080x1920 пикс.";"Android 5.1";"8 (Cortex A-57 + Cortex A-53)";"Samsung Exynos 7";"4 х 2.1 ГГц + 4 х 1.5 ГГц";"4 ГБ";"64 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"21.16 Мп";"5 Мп";Есть;4.1;Есть;Есть;"802.11 a/b/g/n";Есть;"microUSB. Аудио 3.5 мм";"3050 мАч";"156.7 х 78.0 х 7.5 мм";"Серый. серебряный. золотой";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu Pro 5 32Gb";"Под заказ";;297.00;;4;0.00;;;1;0;"Телефон Meizu Pro 5 32Gb";"Телефон Meizu Pro 5 32Gb";"Телефон Meizu Pro 5 32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040719-01.jpg;telefon-meizu-pro-5-m576-32-gb;;"168 г";;;;;;;;;Pro;"5.7""";"Super AMOLED Full HD";"1080x1920 пикс.";"Android 5.1";"8 (Cortex A-57 + Cortex A-53)";"Samsung Exynos 7";"4 х 2.1 ГГц + 4 х 1.5 ГГц";"3 ГБ";"32 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. nano-SIM (microSD)";"900 / 1800 / 1900 МГц";"900 / 2100 МГц";TD-LTE;"900 / 2100 МГц";"21.16 Мп";"5 Мп";Есть;4.1;Есть;Есть;"802.11 a/b/g/n";Есть;"microUSB. Аудио 3.5 мм";"3050 мАч";"156.7 х 78.0 х 7.5 мм";"Серый. серебряный. золотой";Есть;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu M3S 2+16Gb";"В наличии";;122.00;;4;0.00;0;;1;0;"Телефон Meizu M3S 2+16Gb";"Телефон Meizu M3S 2+16Gb";"Телефон Meizu M3S 2+16Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040709-01.jpg;telefon-meizu-m3s-216-gb;;"138 г";;;;;;;;;M3;"5.0""";"IPS HD";"1280x720 пикс.";"Android 6.0";"8 (Cortex A-53)";"MediaTek MT6750";"1.5 ГГц";"2 ГБ";"16 ГБ";;"2 sim";nano-SIM;"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n/ac";;"microUSB. Аудио 3.5 мм";"3020 мАч";"141.9 x 69.0 x 8.3 мм";"Темно-серый. золотой. серый. Rose-gold";;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu Pro 6 32Gb";;;305.00;;4;0.00;0;;1;0;"Телефон Meizu Pro 6 32Gb";"Телефон Meizu Pro 6 32Gb";"Телефон Meizu Pro 6 32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040722-01.jpg;telefon-meizu-pro-6-m570q-32-gb;;"160 г";;;;;;;;;Pro;"5.2""";"Super AMOLED Full HD";"1080x1920 пикс.";"Android 6.0";10;"MediaTek MT6797T";"2 x 2.5 ГГц Cortex-A72 + 4 x 2.0 ГГц Cortex-A53 + 4 x 1.4 ГГц Cortex-A53";"4 ГБ";"32 ГБ";;"2 sim";nano-SIM;"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"21 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n/ac";;"microUSB. Аудио 3.5 мм";"2560 мАч";"147.7 х 70.8 х 7.3 мм";"Темно-серый. золотой. серый";;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu Pro 6 64Gb";;;342.15;;4;0.00;0;;1;0;"Телефон Meizu Pro 6 64Gb";"Телефон Meizu Pro 6 64Gb";"Телефон Meizu Pro 6 64Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040721-02.jpg;telefon-meizu-pro-6-m570q-64-gb;;"160 г";;;;;;;;;Pro;"5.2""";"Super AMOLED Full HD";"1080x1920 пикс.";"Android 6.0";10;"MediaTek MT6797T";"2 x 2.5 ГГц Cortex-A72 + 4 x 2.0 ГГц Cortex-A53 + 4 x 1.4 ГГц Cortex-A53";"4 ГБ";"64 ГБ";;"2 sim";nano-SIM;"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"21 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n/ac";;"microUSB. Аудио 3.5 мм";"2560 мАч";"147.7 х 70.8 х 7.3 мм";"Темно-серый. золотой. серый";;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu M3 Note 32Gb";;;169.10;;4;0.00;0;;1;0;"Телефон Meizu M3 Note 32Gb";"Телефон Meizu M3 Note 32Gb";"Телефон Meizu M3 Note 32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040703-02.jpg;telefon-meizu-m3-note-32-gb;;"163 г";;;;;;;;;M3;"5.5""";"IPS Full HD";"1080x1920 пикс.";"Android 5.1";"8 (Cortex A-53)";"MediaTek MT6755";"4 x 1.8 Ггц + 4 x 1.0 ГГц";"3 ГБ";"32 ГБ";"до 128 ГБ";"2 sim";nano-SIM;"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n";;"microUSB. Аудио 3.5 мм";"4100 мАч";"153.6 х 75.5 х 8.2 мм";"Белый. серый. золотой";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu M2 Note 16GB";"В наличии";;145.00;;4;0.00;0;;1;0;"Телефон Meizu M2 Note 16GB";"Телефон Meizu M2 Note 16GB";"Телефон Meizu M2 Note 16GB";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s010785-01.jpg;telefon-meizu-m2-note-16-gb;;"149 г";;;;;;;;;M2;"5.5""";"IGZO Full HD";"1080x1920 пикс.";"Android 5.1";"8 (Cortex A-53)";"MediaTek MT6753";"1.7 ГГц";"2 ГБ";"16 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n";;"microUSB. Аудио 3.5 мм";"3100 мАч";"150.9 х 75.2 х 8.7 мм";"Белый. синий. серый. розовый";Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu M3S 3+32Gb";;;128.44;;4;0.00;0;;1;0;"Телефон Meizu M3S 3+32Gb";"Телефон Meizu M3S 3+32Gb";"Телефон Meizu M3S 3+32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";s040708-02.jpg;telefon-meizu-m3s-332-gb;;"138 г";;;;;;;;;M3;"5.0""";"IPS HD";"1280x720 пикс.";"Android 6.0";"8 (Cortex A-53)";"MediaTek MT6750";"1.5 ГГц";"3 ГБ";"32 ГБ";;"2 sim";nano-SIM;"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n/ac";;"microUSB. Аудио 3.5 мм";"3020 мАч (съемный)";"141.9 x 69.0 x 8.3 мм";"Темно-серый. золотой. серый. Rose-gold";;Есть;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu M3 mini 32Gb";;;133.54;;4;0.00;10;;1;0;"Телефон Meizu M3 mini 32Gb";"Телефон Meizu M3 mini 32Gb";"Телефон Meizu M3 mini 32Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";271011-01.jpg;telefon-meizu-m3-mini-m688u-32-gb;;"132 г";;;;;;;;;M3;"5.0""";"IPS HD";"1280x720 пикс.";"Android 5.1";"8 (Cortex A-53)";"MediaTek MT6750";"4 х 1 ГГц + 4 х 1.5 ГГц";"3 ГБ";"32 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n";Есть;"microUSB. Аудио 3.5 мм";"2870 мАч";"141.5 х 69.5 х 8.3 мм";"Белый. золотой. синий. серый. розовый";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu MX6";;;242.54;;4;0.00;5;;1;0;"Телефон Meizu MX6";"Телефон Meizu MX6";"Телефон Meizu MX6";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";171101-01.jpg;telefon-meizu-mx6;;"155 г";;;;;;;;;MX;"5.5""";"IPS (Sharp)";"1080x1920 пикс.";"Android 6.0 + Flyme";"10 (8- Cortex-A53 / 2- A72)";"MediaTek MT6797";"2 x 2.3GHz Cortex-A72 + 4 x 2.0GHz Cortex-A53 + 4 x 1.4GHz Cortex-A53";"3 ГБ";"32 ГБ";;"2 sim";"nano-SIM. nano-SIM";"850 / 900 / 1800 / 1900 МГц";"900 / 2100 МГц";"1800 / 1900 / 2100 / 2300 / 2500 / 2600 МГц";"900 / 2100 МГц";"12 Мп";"5 Мп";Есть;4.1;Есть;Есть;"802.11 a/b/g/n/ac";Есть;"USB Type-C. OTG. Аудио 3.5 мм";"3060 мАч";"153.6 х 75.2 х 7.25 мм";Silver;Есть;"Есть (mTouch)";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu M3 mini M688U 16 Gb";Красный;;3000.00;;4;0.00;5;;1;1;"Телефон Meizu M3 mini M688U 16 Gb";"Телефон Meizu M3 mini M688U 16 Gb";"Телефон Meizu M3 mini M688U 16 Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";271012-01.jpg;telefon-meizu-m3-mini-m688u-16-gb;;"132 г";;;;;;;;;M3;"5.0""";"IPS HD";"1280x720 пикс.";"Android 5.1";"8 (Cortex A-53)";"MediaTek MT6750";"4 х 1 ГГц + 4 х 1.5 ГГц";"2 ГБ";"16 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n";Есть;"microUSB. Аудио 3.5 мм";"2870 мАч";"141.5 х 69.5 х 8.3 мм";"Белый. золотой. синий. серый. розовый";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -Smartphones/Meizu;;"Телефон Meizu M3 mini M688U 16 Gb";Серый;;3100.00;;4;0.00;19;;1;1;"Телефон Meizu M3 mini M688U 16 Gb";"Телефон Meizu M3 mini M688U 16 Gb";"Телефон Meizu M3 mini M688U 16 Gb";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";271012-01.jpg;telefon-meizu-m3-mini-m688u-16-gb;;"132 г";;;;;;;;;M3;"5.0""";"IPS HD";"1280x720 пикс.";"Android 5.1";"8 (Cortex A-53)";"MediaTek MT6750";"4 х 1 ГГц + 4 х 1.5 ГГц";"2 ГБ";"16 ГБ";"до 128 ГБ";"2 sim";"nano-SIM. nano-SIM (microSD)";"850 / 900 / 1800 / 1900 МГц";"850 / 900 / 1900 / 2100 МГц";TD-LTE;"850 / 900 / 1900 / 2100 МГц";"13 Мп";"5 Мп";Есть;4.0;Есть;Есть;"802.11 a/b/g/n";Есть;"microUSB. Аудио 3.5 мм";"2870 мАч";"141.5 х 69.5 х 8.3 мм";"Белый. золотой. синий. серый. розовый";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Cats";;;2899.00;;4;0.00;3;;1;0;"Кресло мешок груша L Cats";"Кресло мешок груша L Cats";"Кресло мешок груша L Cats";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";kreslo_meshok_cats_2.jpg;Kreslo_Meshok_L_Cats;;"до 4 кг";"110 см";"80 см";велюр;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L iPhone";;;2899.00;;4;0.00;6;;1;;"Кресло мешок груша L iPhone";"Кресло мешок груша L iPhone";"Кресло мешок груша L iPhone";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";kreslo_meshok_iphone_2.jpg;kreslo-meshok-grusha-l-iphone;;"до 4 кг";"110 см";"80 см";жаккард;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Mini Cooper";;;2899.00;;4;0.00;8;;1;0;"Кресло мешок груша L Mini Cooper";"Кресло мешок груша L Mini Cooper";"Кресло мешок груша L Mini Cooper";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";kreslo_meshok_mini_cooper_2.jpg;Kreslo_Meshok_L_Mini_Cooper;;"до 4 кг";"110 см";"80 см";жаккард;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Graffity Orange";;;2899.00;3199.00;4;0.00;9;;1;0;"Кресло мешок груша L Graffity Orange";"Кресло мешок груша L Graffity Orange";"Кресло мешок груша L Graffity Orange";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";kreslo_meshok_graffity_orange_2.jpg;Kreslo_Meshok_L_Graffity_Orange;;"до 4 кг";"110 см";"80 см";жаккард;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло-мешок L Balloons";;;2899.00;3199.01;4;0.00;4;;1;0;"Кресло-мешок L Balloons";"Кресло-мешок L Balloons";"Кресло-мешок L Balloons";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";kreslo_meshok_balloons_2.jpg;Kreslo_Meshok_L_Balloons;;"до 4 кг";"110 см";"80 см";жаккард;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Beatrice";;;2899.00;3199.02;4;0.00;14;;1;0;"Кресло мешок груша L Beatrice";"Кресло мешок груша L Beatrice";"Кресло мешок груша L Beatrice";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";kreslo_meshok_biatrice_2.jpg;Kreslo_Meshok_L_Jungle;;"до 4 кг";"110 см";"80 см";жаккард;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Lime Velvet";;;3499.00;;4;0.00;17;;1;0;"Кресло мешок груша L Lime Velvet";"Кресло мешок груша L Lime Velvet";"Кресло мешок груша L Lime Velvet";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"kreslo_meshok_velvet_lime.jpg, kreslo_meshok_velvet_lime.jpg";kreslo-meshok-grusha-l-lime-velvet;;"до 4 кг";"110 см";"80 см";микро-вельвет;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Pink Velvet";;;3499.00;;4;0.00;3;;1;0;"Кресло мешок груша L Pink Velvet";"Кресло мешок груша L Pink Velvet";"Кресло мешок груша L Pink Velvet";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_pink.jpg;Kreslo_Meshok_L_Pink_Velvet;;"до 4 кг";"110 см";"80 см";микро-вельвет;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Fuxia Velvet";;;3499.00;;4;0.00;5;;1;0;"Кресло мешок груша L Fuxia Velvet";"Кресло мешок груша L Fuxia Velvet";"Кресло мешок груша L Fuxia Velvet";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_fuxia_2.jpg;Kreslo_Meshok_L_Fuxia_Velvet;;"до 4 кг";"110 см";"80 см";микро-вельвет;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Malina Velvet";;;3499.00;;4;0.00;10;;1;0;"Кресло мешок груша L Malina Velvet";"Кресло мешок груша L Malina Velvet";"Кресло мешок груша L Malina Velvet";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";kreslo_meshol_malina_4.jpg;Kreslo_Meshok_L_Malina_Velvet;;"до 4 кг";"110 см";"80 см";микро-вельвет;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Red Velvet";;;3499.00;;4;0.00;1;;1;0;"Кресло мешок груша L Red Velvet";"Кресло мешок груша L Red Velvet";"Кресло мешок груша L Red Velvet";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_copy_28.jpg;Kreslo_Meshok_L_Red_Velvet;;"до 4 кг";"110 см";"80 см";микро-вельвет;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Turquoise Velvet";;;3499.00;;4;0.00;10;;1;0;"Кресло мешок груша L Turquoise Velvet";"Кресло мешок груша L Turquoise Velvet";"Кресло мешок груша L Turquoise Velvet";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";kreslo_meshok_grusha_turquoise_velvet.jpg;kreslo-meshok-grusha-l-turquoise-velvet;;"до 4 кг";"110 см";"80 см";микро-вельвет;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Blue Velvet";;;3499.00;;4;0.00;10;;1;0;"Кресло мешок груша L Blue Velvet";"Кресло мешок груша L Blue Velvet";"Кресло мешок груша L Blue Velvet";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_blue_velvet_1.jpg;Kreslo_Meshok_L_Blue_Velvet;;"до 4 кг";"110 см";"80 см";микро-вельвет;"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford Yellow";;;1799.00;;4;0.00;3;;1;0;"Кресло мешок груша L Oksford Yellow";"Кресло мешок груша L Oksford Yellow";"Кресло мешок груша L Oksford Yellow";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_copy_7.jpg;Kreslo_Meshok_L_Oksford_Yellow;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford Lime";;;1799.00;;4;0.00;3;;1;0;"Кресло мешок груша L Oksford Lime";"Кресло мешок груша L Oksford Lime";"Кресло мешок груша L Oksford Lime";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_copy_46_2.jpg;Kreslo_Meshok_L_Oksford_Lime;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford Orange";;;1799.00;;4;0.00;3;;1;0;"Кресло мешок груша L Oksford Orange";"Кресло мешок груша L Oksford Orange";"Кресло мешок груша L Oksford Orange";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_copy_8_9.jpg;Kreslo_Meshok_L_Oksford_Orange;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford Red";;;1799.00;;4;0.00;10;;1;0;"Кресло мешок груша L Oksford Red";"Кресло мешок груша L Oksford Red";"Кресло мешок груша L Oksford Red";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_copy_3.jpg;Kreslo_Meshok_L_Oksford_Red;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford bordo";;;1799.00;;4;0.00;45;;1;0;"Кресло мешок груша L Oksford bordo";"Кресло мешок груша L Oksford bordo";"Кресло мешок груша L Oksford bordo";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_Oksford_Bordo_4.jpg;Kerslo_Meshok_L_Oksford_Bordo;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford Green";;;1799.00;;4;0.00;44;;1;0;"Кресло мешок груша L Oksford Green";"Кресло мешок груша L Oksford Green";"Кресло мешок груша L Oksford Green";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_copy_14.jpg;Kreslo_Meshok_L_Oksford_Green;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford Blue";;;1799.00;;4;0.00;6;;1;0;"Кресло мешок груша L Oksford Blue";"Кресло мешок груша L Oksford Blue";"Кресло мешок груша L Oksford Blue";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_copy_12.jpg;Kreslo_Meshok_L_Oksford_Blue;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford Dark Blue";;;1799.00;;4;0.00;3;;1;1;"Кресло мешок груша L Oksford Dark Blue";"Кресло мешок груша L Oksford Dark Blue";"Кресло мешок груша L Oksford Dark Blue";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";beskarkasnoe_kreslo_meshok_grusha_copy_19.jpg;Kreslo_Meshok_L_Oksford_Dark_Blue;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -"Home furniture/Cтолы/Bag chair";Puff;"Кресло мешок груша L Oksford Black";;;1799.00;;4;0.00;6;;1;1;"Кресло мешок груша L Oksford Black";"Кресло мешок груша L Oksford Black";"Кресло мешок груша L Oksford Black";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"beskarkasnoe_kreslo_meshok_grusha_copy_9.jpg, beskarkasnoe_kreslo_meshok_grusha_copy_459.jpg";Kreslo_Meshok_L_Oksford_Black;;"до 4 кг";"110 см";"80 см";"оксфорд (плащевая ткань)";"для детей до 12 лет";"пенополистирол 1-2 мм";"200 литров";"0.25 м3";Латвия;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/files/export/export_stat.csv b/backend/files/export/export_stat.csv deleted file mode 100644 index 5f2ca189c..000000000 --- a/backend/files/export/export_stat.csv +++ /dev/null @@ -1,15 +0,0 @@ -;; -" ";0;0 -;0;0 -" ";0;0 -;0;0 -"/ ";0;0 -"/ ";0;0 -" ";0;0 -" / ";0;0 -" / / ";0;0 -" / / / ";0;0 -" / / / / ";0;0 -" ";0;0 -" / ";65;1 -;1;65 diff --git a/backend/files/export/export_stat_products.csv b/backend/files/export/export_stat_products.csv deleted file mode 100644 index f7a728b01..000000000 --- a/backend/files/export/export_stat_products.csv +++ /dev/null @@ -1,13 +0,0 @@ -;" ";; -;" Panasonic KX-FC258RU";91573.95;1 -;" Dect Panasonic KX-TCD815";69593.55;1 -;" Remeza 4270.670";54509.00;1 -;" ASUS MyPal A639";53148.55;1 -;" STIHL BT 360";25350.00;1 -;" VS-244";4333.55;1 -;" Pacme EL-ED1700";564.85;1 -;"IP- D-link DPH-400S";269.75;1 -;" BERTONI ARENA 2+ ";261.95;1 -;" Motorola TLKR-T4";86.45;1 -;" LG GS-475 RUS WA";76.70;1 -;" ";299768.3;11 diff --git a/backend/files/import/example.csv b/backend/files/import/example.csv index 207041e48..d379e1dfa 100644 --- a/backend/files/import/example.csv +++ b/backend/files/import/example.csv @@ -1,12 +1,12 @@ -Category;Brand;Product;Variant;SKU;Price;"Old price";"Currency ID";Weight;Stock;Units;Visible;Featured;"Meta title";"Meta keywords";"Meta description";Annotation;Description;Images;URL;;;;;;;" ";" ";" ";;;" ";" ";;" ";;" ";" ";" ";" SD ";Sim-;" SIM";"GSM (2G)";3G;"LTE (4G)";WCDMA;" ";" ";;Bluetooth;GPS;A-GPS;Wi-Fi;" ";;;;;"Wi-Fi Hotspot";" ";"3G (UMTS)";;;;;" ";" ";" ";" ";" ";" ";" ";" ";"- ";" ";" ";" ";" ";" ";;" ";;" ()";;" ";" ";;" ";" ";" ()";" (3)";" ... (.)";". " -" // ";Vaillant;" Vaillant MAG OE 11-0/0 XZ C+ H ()";;311187;195.00;212.00;1;0.00;;;1;1;" Vaillant MAG OE 11-0/0 XZ C+ H ()";" Vaillant MAG OE 11-0/0 XZ C+ H ()";" Vaillant MAG OE 11-0/0 XZ C+ H ()";"
Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum.
";"

Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum.

";mag_pro_370x500.png;gazovaya-kolonka-vaillant-mag-oe-11-00-xz-c-h-pezorozzhig;10.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;19.2.;;;;11.;" ";;;; -" // ";Vaillant;" Vaillant MAG mini OE 11-0/0 RXZ H ()";;311292;260.00;289.00;1;0.00;0;;1;;" Vaillant MAG mini OE 11-0/0 RXZ H ()";" Vaillant MAG mini OE 11-0/0 RXZ H ()";" Vaillant MAG mini OE 11-0/0 RXZ H ()";"
Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum.
";"

Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum.

";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.png";gazovaya-kolonka-vaillant-mag-mini-oe-11-00-rxz-h-pezorozzhig;12.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;19.2.;;;;11.;" ";;;; -" // ";Vaillant;" Vaillant MAG mini OE 11-0/0 RXI H ( )";;311260;280.00;311.00;1;0.00;0;;1;0;" Vaillant MAG mini OE 11-0/0 RXI H ( )";" Vaillant MAG mini OE 11-0/0 RXI H ( )";" Vaillant MAG mini OE 11-0/0 RXI H ( )";"
-Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum. -
";"Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.png";gazovaya-kolonka-vaillant-mag-mini-oe-11-00-rxi-h-elektro-rozzhig-;12.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;18.6.;;" ()";;11.;" ";;;; -" // ";Vaillant;" Vaillant MAG OE 14-0/0 RXZ H ()";;311392;312.00;347.00;1;0.00;0;;1;0;" Vaillant MAG OE 14-0/0 RXZ H ()";" Vaillant MAG OE 14-0/0 RXZ H ()";" Vaillant MAG OE 14-0/0 RXZ H ()";"
-Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum. -
";"Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.png";gazovaya-kolonka-vaillant-mag-oe-14-00-rxz-h-pezorozzhig;12.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;24.4.;;;;14.;" ";;;; -" // ";Vaillant;" Vaillant MAG OE 14-0/0 RXI H ( )";;311591;355.00;395.00;1;0.00;0;;1;1;" Vaillant MAG OE 14-0/0 RXI H ( )";" Vaillant MAG OE 14-0/0 RXI H ( )";" Vaillant MAG OE 14-0/0 RXI H ( )";"
-Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum. -
";"Lorem Ipsum - -"""", -. Lorem Ipsum """" XVI . , Lorem Ipsum . Lorem Ipsum , . Letraset Lorem Ipsum 60- , , Aldus PageMaker, Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.png";gazovaya-kolonka-vaillant-mag-oe-14-00-rxi-h-elektro-rozzhig;14.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;24.4.;;" ()";;14.;" ";;;; +Category;Brand;Product;Variant;SKU;Price;"Old price";"Currency ID";Weight;Stock;Units;Visible;Featured;"Meta title";"Meta keywords";"Meta description";Annotation;Description;Images;URL;Вес;Высота;Диаметр;Ткань;Предназначение;Наполнитель;"Количество наполнителя";"Объемный вес для транспортных компаний";"Производство ";Серия;Диагональ;"Тип экрана";"Разрешение экрана";Система;"Количество ядер";Процессор;"Частота процессора";"Оперативная память";"Встроенная память";"Поддержка SD карт";Sim-карт;"Размеры SIM";"GSM (2G)";3G;"LTE (4G)";WCDMA;"Основная камера";"Фронтальная камера";Вспышка;Bluetooth;GPS;A-GPS;Wi-Fi;"ИК порт";Разъемы;Аккумулятор;Размеры;Цвет;"Wi-Fi Hotspot";"Сканер отпечатков пальцев";"3G (UMTS)";Возраст;Пол;Комплектация;Материал;"Размеры упаковки";"Размер игрушки";"Страна производитель";"Тепловая мощность";"Максимальная температура теплоносителя";"Максимальное рабочее давление";"Конструкция радиатора";"Способ подключения";"Кол-во секций в радиаторе";"Объем воды в радиаторе";"Опрессовочное давление";"Способ установки";"Номинальный объем водонагревателя";"Положение установки";Форма;"Мощность ТЭНа";Гарантия;"Тип нагревательного элемента (ТЭНа)";Мощность;"Отвод газов";"Тип розжига";Производительность;"Производительность ГВС";"Модуляция мощности";"Вес (кг)";"Объем (м3)";"Габариты В.Ш.Г. (мм.)";"Мин. давление газа" +"Техника для дома/Водонагреватели/Газовые колонки";Vaillant;"Газовая колонка Vaillant MAG OE 11-0/0 XZ C+ H (пьезорозжиг)";;311187;195.00;212.00;1;0.00;;;1;1;"Газовая колонка Vaillant MAG OE 11-0/0 XZ C+ H (пьезорозжиг)";"Газовая колонка Vaillant MAG OE 11-0/0 XZ C+ H (пьезорозжиг)";"Газовая колонка Vaillant MAG OE 11-0/0 XZ C+ H (пьезорозжиг)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"

Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.

";mag_pro_370x500.png;gazovaya-kolonka-vaillant-mag-oe-11-00-xz-c-h-pezorozzhig;10кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;19.2кВт.;дымоход;пьезорозжиг;;11л.;"Две ступени";;;; +"Техника для дома/Водонагреватели/Газовые колонки";Vaillant;"Газовая колонка Vaillant MAG mini OE 11-0/0 RXZ H (пьезорозжиг)";;311292;260.00;289.00;1;0.00;0;;1;;"Газовая колонка Vaillant MAG mini OE 11-0/0 RXZ H (пьезорозжиг)";"Газовая колонка Vaillant MAG mini OE 11-0/0 RXZ H (пьезорозжиг)";"Газовая колонка Vaillant MAG mini OE 11-0/0 RXZ H (пьезорозжиг)";"
Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.
";"

Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.

";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.png";gazovaya-kolonka-vaillant-mag-mini-oe-11-00-rxz-h-pezorozzhig;12кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;19.2кВт.;дымоход;пьезорозжиг;;11л.;"Десять ступеней";;;; +"Техника для дома/Водонагреватели/Газовые колонки";Vaillant;"Газовая колонка Vaillant MAG mini OE 11-0/0 RXI H (электро розжиг)";;311260;280.00;311.00;1;0.00;0;;1;0;"Газовая колонка Vaillant MAG mini OE 11-0/0 RXI H (электро розжиг)";"Газовая колонка Vaillant MAG mini OE 11-0/0 RXI H (электро розжиг)";"Газовая колонка Vaillant MAG mini OE 11-0/0 RXI H (электро розжиг)";"
+Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum. +
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.png";gazovaya-kolonka-vaillant-mag-mini-oe-11-00-rxi-h-elektro-rozzhig-;12кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;18.6кВт.;дымоход;"от батареек (электро)";;11л.;"Десять ступеней";;;; +"Техника для дома/Водонагреватели/Газовые колонки";Vaillant;"Газовая колонка Vaillant MAG OE 14-0/0 RXZ H (пьезорозжиг)";;311392;312.00;347.00;1;0.00;0;;1;0;"Газовая колонка Vaillant MAG OE 14-0/0 RXZ H (пьезорозжиг)";"Газовая колонка Vaillant MAG OE 14-0/0 RXZ H (пьезорозжиг)";"Газовая колонка Vaillant MAG OE 14-0/0 RXZ H (пьезорозжиг)";"
+Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum. +
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.png";gazovaya-kolonka-vaillant-mag-oe-14-00-rxz-h-pezorozzhig;12кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;24.4кВт.;дымоход;пьезорозжиг;;14л.;"Десять ступеней";;;; +"Техника для дома/Водонагреватели/Газовые колонки";Vaillant;"Газовая колонка Vaillant MAG OE 14-0/0 RXI H (электро розжиг)";;311591;355.00;395.00;1;0.00;0;;1;1;"Газовая колонка Vaillant MAG OE 14-0/0 RXI H (электро розжиг)";"Газовая колонка Vaillant MAG OE 14-0/0 RXI H (электро розжиг)";"Газовая колонка Vaillant MAG OE 14-0/0 RXI H (электро розжиг)";"
+Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum. +
";"Lorem Ipsum - это текст-""рыба"", часто используемый в печати и вэб-дизайне. Lorem Ipsum является стандартной ""рыбой"" для текстов на латинице с начала XVI века. В то время некий безымянный печатник создал большую коллекцию размеров и форм шрифтов, используя Lorem Ipsum для распечатки образцов. Lorem Ipsum не только успешно пережил без заметных изменений пять веков, но и перешагнул в электронный дизайн. Его популяризации в новое время послужили публикация листов Letraset с образцами Lorem Ipsum в 60-х годах и, в более недавнее время, программы электронной вёрстки типа Aldus PageMaker, в шаблонах которых используется Lorem Ipsum.";"mag_370x500-kopiya.png, _06_atmomag_gwh03_1014_04-kopiya.png";gazovaya-kolonka-vaillant-mag-oe-14-00-rxi-h-elektro-rozzhig;14кг.;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;Испания;;;;;;;;;;;;;;;;24.4кВт.;дымоход;"от батареек (электро)";;14л.;"Десять ступеней";;;; diff --git a/backend/files/index.php b/backend/files/index.php index 096350ee5..69cd666a1 100644 --- a/backend/files/index.php +++ b/backend/files/index.php @@ -3,8 +3,11 @@ use Okay\Core\Response; use Okay\Core\EntityFactory; use Okay\Core\Managers; +use Okay\Core\Config; +use Okay\Core\Request; use Okay\Core\Security\BackendFileDownloadPolicy; use Okay\Core\Security\AdminSession; +use Okay\Core\Security\TrustedRequestContext; use Okay\Entities\ManagersEntity; use Okay\Core\Modules\Modules; @@ -12,12 +15,23 @@ require_once('vendor/autoload.php'); +/** @var \Okay\Core\OkayContainer\OkayContainer $DI */ +$DI = include 'Okay/Core/config/container.php'; + +/** @var Config $config */ +$config = $DI->get(Config::class); +$trustedProxyIps = $config->get('trusted_proxy_ips'); +$trustedClientIpHeader = $config->get('trusted_proxy_client_ip_header'); +Request::setTrustedRequestContext(TrustedRequestContext::fromServer( + $_SERVER, + is_string($trustedProxyIps) ? $trustedProxyIps : null, + is_string($trustedClientIpHeader) ? $trustedClientIpHeader : 'X-Forwarded-For' +)); + session_name(AdminSession::SESSION_NAME); AdminSession::configureCookieParams($_SERVER); session_start(); -$DI = include 'Okay/Core/config/container.php'; - /** @var Modules $modules */ $modules = $DI->get(Modules::class); $modules->startEnabledModules(); diff --git a/backend/index.php b/backend/index.php index e7ecc8e80..3dfdf7887 100644 --- a/backend/index.php +++ b/backend/index.php @@ -15,7 +15,9 @@ use Okay\Core\Languages; use Okay\Admin\Helpers\BackendModulesHelper; use Okay\Admin\Bootstrap\BackendControllerMethodParams; +use Okay\Core\Security\AdminCsrfToken; use Okay\Core\Security\AdminSession; +use Okay\Core\Security\TrustedRequestContext; ini_set('display_errors', 'off'); @@ -31,6 +33,13 @@ * @var Config $config */ $config = $DI->get(Config::class); +$trustedProxyIps = $config->get('trusted_proxy_ips'); +$trustedClientIpHeader = $config->get('trusted_proxy_client_ip_header'); +Request::setTrustedRequestContext(TrustedRequestContext::fromServer( + $_SERVER, + is_string($trustedProxyIps) ? $trustedProxyIps : null, + is_string($trustedClientIpHeader) ? $trustedClientIpHeader : 'X-Forwarded-For' +)); // Засекаем время $time_start = microtime(true); @@ -39,7 +48,7 @@ ini_set('session.cookie_lifetime', '0'); // 0 - пока браузер не закрыт AdminSession::configureCookieParams($_SERVER); session_start(); -$_SESSION['id'] = session_id(); +$_SESSION['id'] = AdminCsrfToken::get(); if ($config->get('debug_mode') == true) { ini_set('display_errors', 'on'); diff --git a/backend/lang/en.php b/backend/lang/en.php index 6f8d4f053..5a640d48c 100644 --- a/backend/lang/en.php +++ b/backend/lang/en.php @@ -152,6 +152,19 @@ $lang['general_design_message'] = 'In this section, you can edit the files responsible for displaying the site.'; $lang['general_design_message2'] = 'Before editing, create a copy of your template.'; $lang['general_design_message3'] = 'This section allows you to view message templates in debug mode.'; +$lang['theme_editor_disabled'] = 'The theme editor is disabled by the secure default. Enable it only for trusted administrators.'; +$lang['theme_editor_unauthorized'] = 'You are not authorized to edit this theme file.'; +$lang['theme_editor_method_not_allowed'] = 'Theme changes must be submitted with POST.'; +$lang['theme_editor_csrf_invalid'] = 'Your security token is invalid or expired. Reload the page and try again.'; +$lang['theme_editor_invalid_path'] = 'The selected theme file is invalid or outside the theme directory.'; +$lang['theme_editor_invalid_extension'] = 'The selected file type is not supported by this editor.'; +$lang['theme_editor_locked'] = 'The current theme is locked and cannot be changed.'; +$lang['theme_editor_write_failed'] = 'The theme file could not be saved. Check its permissions and try again.'; +$lang['theme_editor_saved'] = 'Theme file saved.'; +$lang['theme_editor_saving'] = 'Saving theme file...'; +$lang['theme_editor_dirty'] = 'Unsaved changes.'; +$lang['theme_editor_retry'] = 'Your draft is still open. Try saving again.'; +$lang['theme_editor_save_error'] = 'The theme file could not be saved.'; $lang['general_translation_attention'] = 'This section contains translations of all elements of the site. For editing - just go to the desired translation and enter the value you need.'; $lang['general_discount'] = 'Discount'; $lang['general_do_disable'] = 'Disable'; @@ -1152,6 +1165,13 @@ $lang['learning_hint_next_done'] = 'Перейти'; $lang['learning_hint_meta_data'] = '

Мета-данные - это данные необходимые для поискового продвижения
Meta-title - заголовок страницы, который отображается во вкладке браузера
Meta-keywords - список ключевых слов
Meta-description - описание страницы для отображения страницы в поиске

'; $lang['learning_hint_success'] = '

Остался последний шаг - это сохранить все настройки

Нажмите на кнопку Применить

'; +$lang['learning_hint_saving'] = 'Saving the lesson status...'; +$lang['learning_hint_save_error'] = 'The lesson could not be completed.'; +$lang['learning_hint_retry'] = 'Retry'; +$lang['lesson_forbidden'] = 'You are not authorized to complete lessons.'; +$lang['lesson_method_not_allowed'] = 'Lesson completion requires POST.'; +$lang['lesson_csrf_invalid'] = 'Your security token is invalid or expired. Reload the page and try again.'; +$lang['lesson_invalid'] = 'The selected lesson is invalid.'; $lang['learning_hint_categories_intro'] = '

Категории - важная часть для построения структуры сайта и основа всего интернет-магазина.

В данном уроке мы разберем все моменты, которые касаются создания и редактирования категорий товара.

'; $lang['learning_hint_categories_intro2'] = '

Мы сейчас находимся на странице вывода категорий.

Отображаются они в виде списка, в котором можно легко ими управлять.

'; diff --git a/backend/lang/languages_list.php b/backend/lang/languages_list.php index 6f2f21c6f..da210e0c4 100644 --- a/backend/lang/languages_list.php +++ b/backend/lang/languages_list.php @@ -1,4 +1,5 @@ 'Armenian', 'label' => 'am', 'href_lang' => 'hy'); $langs['az'] = (object)array('name' => 'Azerbaijani', 'label' => 'az', 'href_lang' => 'az'); @@ -29,4 +30,4 @@ $langs['se'] = (object)array('name' => 'Swedish', 'label' => 'se', 'href_lang' => 'sv'); $langs['tr'] = (object)array('name' => 'Turkish', 'label' => 'tr', 'href_lang' => 'tr'); $langs['ua'] = (object)array('name' => 'Ukrainian', 'label' => 'ua', 'href_lang' => 'uk'); -$langs['uz'] = (object)array('name' => 'Uzbek', 'label' => 'uz', 'href_lang' => 'uz'); \ No newline at end of file +$langs['uz'] = (object)array('name' => 'Uzbek', 'label' => 'uz', 'href_lang' => 'uz'); diff --git a/backend/lang/ru.php b/backend/lang/ru.php index bd59d81ca..e5f54ddb3 100644 --- a/backend/lang/ru.php +++ b/backend/lang/ru.php @@ -152,6 +152,19 @@ $lang['general_design_message'] = 'В данном разделе вы можете редактировать файлы, отвечающие за отображение сайта.'; $lang['general_design_message2'] = 'Перед началом редактирования создайте копию вашего шаблона.'; $lang['general_design_message3'] = 'Данный раздел позволяет, просматривать шаблоны писем в режиме отладки.'; +$lang['theme_editor_disabled'] = 'Редактор шаблона отключен безопасными настройками. Включайте его только для доверенных администраторов.'; +$lang['theme_editor_unauthorized'] = 'У вас нет прав на редактирование этого файла шаблона.'; +$lang['theme_editor_method_not_allowed'] = 'Изменения шаблона должны отправляться методом POST.'; +$lang['theme_editor_csrf_invalid'] = 'Токен безопасности недействителен или устарел. Обновите страницу и повторите попытку.'; +$lang['theme_editor_invalid_path'] = 'Выбран недопустимый файл шаблона или путь за пределами каталога шаблона.'; +$lang['theme_editor_invalid_extension'] = 'Выбранный тип файла не поддерживается этим редактором.'; +$lang['theme_editor_locked'] = 'Текущий шаблон защищен от изменений.'; +$lang['theme_editor_write_failed'] = 'Не удалось сохранить файл шаблона. Проверьте права и повторите попытку.'; +$lang['theme_editor_saved'] = 'Файл шаблона сохранен.'; +$lang['theme_editor_saving'] = 'Сохранение файла шаблона...'; +$lang['theme_editor_dirty'] = 'Есть несохраненные изменения.'; +$lang['theme_editor_retry'] = 'Ваш черновик сохранен в редакторе. Повторите сохранение.'; +$lang['theme_editor_save_error'] = 'Не удалось сохранить файл шаблона.'; $lang['general_translation_attention'] = 'В этом разделе собраны переводы всех элементов сайта. Для редактирования - просто перейдите в нужный перевод и впишите нужное вам значение.'; $lang['general_discount'] = 'Скидка'; $lang['general_do_disable'] = 'Выключить'; @@ -1152,6 +1165,13 @@ $lang['learning_hint_next_done'] = 'Перейти'; $lang['learning_hint_meta_data'] = '

Мета-данные - это данные необходимые для поискового продвижения
Meta-title - заголовок страницы, который отображается во вкладке браузера
Meta-keywords - список ключевых слов
Meta-description - описание страницы для отображения страницы в поиске

'; $lang['learning_hint_success'] = '

Остался последний шаг - это сохранить все настройки

Нажмите на кнопку Применить

'; +$lang['learning_hint_saving'] = 'Сохранение статуса урока...'; +$lang['learning_hint_save_error'] = 'Не удалось завершить урок.'; +$lang['learning_hint_retry'] = 'Повторить'; +$lang['lesson_forbidden'] = 'У вас нет прав для завершения уроков.'; +$lang['lesson_method_not_allowed'] = 'Для завершения урока требуется POST-запрос.'; +$lang['lesson_csrf_invalid'] = 'Токен безопасности недействителен или устарел. Обновите страницу и повторите попытку.'; +$lang['lesson_invalid'] = 'Выбран недопустимый урок.'; $lang['learning_hint_categories_intro'] = '

Категории - важная часть для построения структуры сайта и основа всего интернет-магазина.

В данном уроке мы разберем все моменты, которые касаются создания и редактирования категорий товара.

'; $lang['learning_hint_categories_intro2'] = '

Мы сейчас находимся на странице вывода категорий.

Отображаются они в виде списка, в котором можно легко ими управлять.

'; diff --git a/backend/lang/ua.php b/backend/lang/ua.php index e7713b0a9..7a0f35dec 100644 --- a/backend/lang/ua.php +++ b/backend/lang/ua.php @@ -152,6 +152,19 @@ $lang['general_design_message'] = 'В цьому розділі ви можете редагувати файли, що відповідають за відображення сайту.'; $lang['general_design_message2'] = 'Перед початком редагування створіть копію вашого шаблону.'; $lang['general_design_message3'] = 'Даний розділ дозволяє, переглядати шаблони листів в режимі налагодження.'; +$lang['theme_editor_disabled'] = 'Редактор шаблону вимкнено безпечними налаштуваннями. Увімкніть його лише для довірених адміністраторів.'; +$lang['theme_editor_unauthorized'] = 'У вас немає прав на редагування цього файлу шаблону.'; +$lang['theme_editor_method_not_allowed'] = 'Зміни шаблону потрібно надсилати методом POST.'; +$lang['theme_editor_csrf_invalid'] = 'Токен безпеки недійсний або застарів. Оновіть сторінку та повторіть спробу.'; +$lang['theme_editor_invalid_path'] = 'Вибрано недійсний файл шаблону або шлях за межами каталогу шаблону.'; +$lang['theme_editor_invalid_extension'] = 'Вибраний тип файлу не підтримується цим редактором.'; +$lang['theme_editor_locked'] = 'Поточний шаблон захищений від змін.'; +$lang['theme_editor_write_failed'] = 'Не вдалося зберегти файл шаблону. Перевірте права та повторіть спробу.'; +$lang['theme_editor_saved'] = 'Файл шаблону збережено.'; +$lang['theme_editor_saving'] = 'Збереження файлу шаблону...'; +$lang['theme_editor_dirty'] = 'Є незбережені зміни.'; +$lang['theme_editor_retry'] = 'Ваш чернетковий текст залишився у редакторі. Повторіть збереження.'; +$lang['theme_editor_save_error'] = 'Не вдалося зберегти файл шаблону.'; $lang['general_translation_attention'] = 'У цьому розділі зібрані переклади всіх елементів сайту. Для редагування - просто перейдіть в потрібний переклад і впишіть потрібне вам значення.'; $lang['general_discount'] = 'Знижка'; $lang['general_do_disable'] = 'Вимкнути'; @@ -1152,6 +1165,13 @@ $lang['learning_hint_next_done'] = 'Перейти'; $lang['learning_hint_meta_data'] = '

Мета-данные - это данные необходимые для поискового продвижения
Meta-title - заголовок страницы, который отображается во вкладке браузера
Meta-keywords - список ключевых слов
Meta-description - описание страницы для отображения страницы в поиске

'; $lang['learning_hint_success'] = '

Остался последний шаг - это сохранить все настройки

Нажмите на кнопку Применить

'; +$lang['learning_hint_saving'] = 'Збереження статусу уроку...'; +$lang['learning_hint_save_error'] = 'Не вдалося завершити урок.'; +$lang['learning_hint_retry'] = 'Повторити'; +$lang['lesson_forbidden'] = 'У вас немає прав для завершення уроків.'; +$lang['lesson_method_not_allowed'] = 'Для завершення уроку потрібен POST-запит.'; +$lang['lesson_csrf_invalid'] = 'Токен безпеки недійсний або застарів. Оновіть сторінку та повторіть спробу.'; +$lang['lesson_invalid'] = 'Вибрано недійсний урок.'; $lang['learning_hint_categories_intro'] = '

Категории - важная часть для построения структуры сайта и основа всего интернет-магазина.

В данном уроке мы разберем все моменты, которые касаются создания и редактирования категорий товара.

'; $lang['learning_hint_categories_intro2'] = '

Мы сейчас находимся на странице вывода категорий.

Отображаются они в виде списка, в котором можно легко ими управлять.

'; diff --git a/composer.json b/composer.json index 4da4077b7..34d33fcab 100644 --- a/composer.json +++ b/composer.json @@ -49,10 +49,9 @@ "dragonmantank/cron-expression": "^3.1", "symfony/process": "^8.0", "symfony/lock": "^8.0", - "chillerlan/php-qrcode": "^6.0", "axy/sourcemap": "^1.1", "intervention/image": "^4.0", - "openai-php/client": "^0.19", + "openai-php/client": "^0.20", "guzzlehttp/guzzle": "^7.10", "wikimedia/minify": "^2.10" }, diff --git a/composer.lock b/composer.lock index c13ed3866..71160b6fa 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "27ea423074760ba4239a43dcd98e8006", + "content-hash": "c2cc6e10d572580f598369989fb81d8b", "packages": [ { "name": "aura/sql", @@ -394,172 +394,6 @@ }, "time": "2021-11-18T19:24:07+00:00" }, - { - "name": "chillerlan/php-qrcode", - "version": "6.0.1", - "source": { - "type": "git", - "url": "https://github.com/chillerlan/php-qrcode.git", - "reference": "49006e34bd5328f163e80329e7312f34dceea59b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-qrcode/zipball/49006e34bd5328f163e80329e7312f34dceea59b", - "reference": "49006e34bd5328f163e80329e7312f34dceea59b", - "shasum": "" - }, - "require": { - "chillerlan/php-settings-container": "^3.2.1", - "ext-mbstring": "*", - "php": "^8.2" - }, - "require-dev": { - "chillerlan/php-authenticator": "^5.3", - "ext-fileinfo": "*", - "intervention/image": "^3.11", - "phan/phan": "^6.0.1", - "phpbench/phpbench": "^1.4", - "phpmd/phpmd": "^2.15", - "phpstan/phpstan": "^2.1.40", - "phpstan/phpstan-deprecation-rules": "^2.0.4", - "phpunit/phpunit": "^11.5", - "setasign/fpdf": "^1.8.6", - "slevomat/coding-standard": "^8.28", - "squizlabs/php_codesniffer": "^4.0" - }, - "suggest": { - "chillerlan/php-authenticator": "Yet another Google authenticator! Also creates URIs for mobile apps.", - "intervention/image": "More advanced GD and ImageMagick output.", - "setasign/fpdf": "Required to use the QR FPDF output.", - "simple-icons/simple-icons": "SVG icons that you can use to embed as logos in the QR Code" - }, - "type": "library", - "autoload": { - "psr-4": { - "chillerlan\\QRCode\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT", - "Apache-2.0" - ], - "authors": [ - { - "name": "Kazuhiko Arase", - "homepage": "https://github.com/kazuhikoarase/qrcode-generator" - }, - { - "name": "ZXing Authors", - "homepage": "https://github.com/zxing/zxing" - }, - { - "name": "Ashot Khanamiryan", - "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder" - }, - { - "name": "Smiley", - "email": "smiley@chillerlan.net", - "homepage": "https://github.com/codemasher" - }, - { - "name": "Contributors", - "homepage": "https://github.com/chillerlan/php-qrcode/graphs/contributors" - } - ], - "description": "A QR Code generator and reader with a user-friendly API. PHP 8.2+", - "homepage": "https://github.com/chillerlan/php-qrcode", - "keywords": [ - "phpqrcode", - "qr", - "qr code", - "qr-reader", - "qrcode", - "qrcode-generator", - "qrcode-reader" - ], - "support": { - "docs": "https://php-qrcode.readthedocs.io", - "issues": "https://github.com/chillerlan/php-qrcode/issues", - "source": "https://github.com/chillerlan/php-qrcode" - }, - "funding": [ - { - "url": "https://ko-fi.com/codemasher", - "type": "Ko-Fi" - } - ], - "time": "2026-03-18T21:21:07+00:00" - }, - { - "name": "chillerlan/php-settings-container", - "version": "3.3.0", - "source": { - "type": "git", - "url": "https://github.com/chillerlan/php-settings-container.git", - "reference": "a0a487cbf5344f721eb504bf0f59bada40c381b7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/chillerlan/php-settings-container/zipball/a0a487cbf5344f721eb504bf0f59bada40c381b7", - "reference": "a0a487cbf5344f721eb504bf0f59bada40c381b7", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^8.1" - }, - "require-dev": { - "phan/phan": "^5.5.2", - "phpmd/phpmd": "^2.15", - "phpstan/phpstan": "^2.1.31", - "phpstan/phpstan-deprecation-rules": "^2.0.3", - "phpunit/phpunit": "^10.5", - "slevomat/coding-standard": "^8.22", - "squizlabs/php_codesniffer": "^4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "chillerlan\\Settings\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Smiley", - "email": "smiley@chillerlan.net", - "homepage": "https://github.com/codemasher" - } - ], - "description": "A container class for immutable settings objects. Not a DI container.", - "homepage": "https://github.com/chillerlan/php-settings-container", - "keywords": [ - "Settings", - "configuration", - "container", - "helper", - "property hook" - ], - "support": { - "issues": "https://github.com/chillerlan/php-settings-container/issues", - "source": "https://github.com/chillerlan/php-settings-container" - }, - "funding": [ - { - "url": "https://www.paypal.com/donate?hosted_button_id=WLYUNAT9ZTJZ4", - "type": "custom" - }, - { - "url": "https://ko-fi.com/codemasher", - "type": "ko_fi" - } - ], - "time": "2026-03-20T21:10:52+00:00" - }, { "name": "dragonmantank/cron-expression", "version": "v3.6.0", @@ -626,16 +460,16 @@ }, { "name": "giggsey/libphonenumber-for-php", - "version": "9.0.34", + "version": "9.0.35", "source": { "type": "git", "url": "https://github.com/giggsey/libphonenumber-for-php.git", - "reference": "4dfd6bc0eb0ed3166e77595a905bd83c8fe29f3e" + "reference": "4e7e15a26477b65090fdc59cd56d35ca747cb126" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/4dfd6bc0eb0ed3166e77595a905bd83c8fe29f3e", - "reference": "4dfd6bc0eb0ed3166e77595a905bd83c8fe29f3e", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/4e7e15a26477b65090fdc59cd56d35ca747cb126", + "reference": "4e7e15a26477b65090fdc59cd56d35ca747cb126", "shasum": "" }, "require": { @@ -698,7 +532,7 @@ "issues": "https://github.com/giggsey/libphonenumber-for-php/issues", "source": "https://github.com/giggsey/libphonenumber-for-php" }, - "time": "2026-07-03T10:22:37+00:00" + "time": "2026-07-17T13:12:34+00:00" }, { "name": "giggsey/locale", @@ -760,22 +594,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.14.0", + "version": "7.15.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "aef242412e13128b5049864867bb49fc37dd39de" + "reference": "744101956d78b7c1384d0cbf379db13e859167bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aef242412e13128b5049864867bb49fc37dd39de", - "reference": "aef242412e13128b5049864867bb49fc37dd39de", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/744101956d78b7c1384d0cbf379db13e859167bf", + "reference": "744101956d78b7c1384d0cbf379db13e859167bf", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^2.5.1", - "guzzlehttp/psr7": "^2.12.4", + "guzzlehttp/psr7": "^2.13", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", @@ -788,7 +622,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.3", - "guzzlehttp/test-server": "^0.6", + "guzzlehttp/test-server": "^0.7", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -868,7 +702,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.14.0" + "source": "https://github.com/guzzle/guzzle/tree/7.15.2" }, "funding": [ { @@ -884,7 +718,7 @@ "type": "tidelift" } ], - "time": "2026-07-08T22:54:09+00:00" + "time": "2026-07-26T23:23:20+00:00" }, { "name": "guzzlehttp/promises", @@ -972,16 +806,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.12.4", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c" + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", - "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4", + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4", "shasum": "" }, "require": { @@ -1071,7 +905,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.12.4" + "source": "https://github.com/guzzle/psr7/tree/2.13.0" }, "funding": [ { @@ -1087,7 +921,7 @@ "type": "tidelift" } ], - "time": "2026-07-08T15:56:20+00:00" + "time": "2026-07-16T22:23:49+00:00" }, { "name": "intervention/gif", @@ -1402,16 +1236,16 @@ }, { "name": "openai-php/client", - "version": "v0.19.2", + "version": "v0.20.1", "source": { "type": "git", "url": "https://github.com/openai-php/client.git", - "reference": "12e3513527e22d5657f4f9809796f8fe254fd0a9" + "reference": "de32ca459d5810d3c80989bc16fb0412f5095ae4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/openai-php/client/zipball/12e3513527e22d5657f4f9809796f8fe254fd0a9", - "reference": "12e3513527e22d5657f4f9809796f8fe254fd0a9", + "url": "https://api.github.com/repos/openai-php/client/zipball/de32ca459d5810d3c80989bc16fb0412f5095ae4", + "reference": "de32ca459d5810d3c80989bc16fb0412f5095ae4", "shasum": "" }, "require": { @@ -1473,7 +1307,7 @@ ], "support": { "issues": "https://github.com/openai-php/client/issues", - "source": "https://github.com/openai-php/client/tree/v0.19.2" + "source": "https://github.com/openai-php/client/tree/v0.20.1" }, "funding": [ { @@ -1489,7 +1323,7 @@ "type": "github" } ], - "time": "2026-04-19T20:35:01+00:00" + "time": "2026-07-20T11:03:21+00:00" }, { "name": "pear/net_url2", @@ -2292,16 +2126,16 @@ }, { "name": "symfony/console", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d" + "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", - "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", + "url": "https://api.github.com/repos/symfony/console/zipball/535e18a1b8925f6c01a55b171d157ab66c2ace15", + "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15", "shasum": "" }, "require": { @@ -2368,7 +2202,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.1.1" + "source": "https://github.com/symfony/console/tree/v8.1.2" }, "funding": [ { @@ -2388,7 +2222,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T12:55:20+00:00" + "time": "2026-07-27T13:58:19+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2628,16 +2462,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { @@ -2686,7 +2520,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -2706,7 +2540,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T05:58:03+00:00" + "time": "2026-07-28T08:25:59+00:00" }, { "name": "symfony/polyfill-intl-normalizer", @@ -2964,16 +2798,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { @@ -3020,7 +2854,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { @@ -3040,7 +2874,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T02:25:22+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/process", @@ -3196,16 +3030,16 @@ }, { "name": "symfony/string", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", - "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", "shasum": "" }, "require": { @@ -3262,7 +3096,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.1.0" + "source": "https://github.com/symfony/string/tree/v8.1.2" }, "funding": [ { @@ -3282,7 +3116,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-28T07:35:25+00:00" }, { "name": "thecodingmachine/safe", @@ -3562,16 +3396,16 @@ }, { "name": "laravel/pao", - "version": "v1.1.2", + "version": "v1.1.3", "source": { "type": "git", "url": "https://github.com/laravel/pao.git", - "reference": "41b3c61ebeddce52a446afe6d21e0b02983fb2f6" + "reference": "322f22095f3639551b64db9a13dfd8ab09c8206b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pao/zipball/41b3c61ebeddce52a446afe6d21e0b02983fb2f6", - "reference": "41b3c61ebeddce52a446afe6d21e0b02983fb2f6", + "url": "https://api.github.com/repos/laravel/pao/zipball/322f22095f3639551b64db9a13dfd8ab09c8206b", + "reference": "322f22095f3639551b64db9a13dfd8ab09c8206b", "shasum": "" }, "require": { @@ -3585,15 +3419,15 @@ "phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0" }, "require-dev": { - "brianium/paratest": "^7.20.0", - "laravel/pint": "^1.29.1", + "brianium/paratest": "^7.23.0", + "laravel/pint": "^1.30.0", "orchestra/testbench": "^10.11.0 || ^11.1.0", - "pestphp/pest": "^4.7.2 || ^5.0.0", + "pestphp/pest": "^4.7.2 || ^5.0.1", "pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.0", - "phpstan/phpstan": "^2.2.2", - "rector/rector": "^2.4.5", + "phpstan/phpstan": "^2.2.7", + "rector/rector": "^2.5.8", "symfony/process": "^7.4.8 || ^8.1.0", - "symfony/var-dumper": "^7.4.8 || ^8.1.0" + "symfony/var-dumper": "^7.4.8 || ^8.1.2" }, "type": "library", "extra": { @@ -3643,7 +3477,7 @@ "issues": "https://github.com/laravel/pao/issues", "source": "https://github.com/laravel/pao" }, - "time": "2026-06-22T19:58:00+00:00" + "time": "2026-07-29T18:38:14+00:00" }, { "name": "myclabs/deep-copy", @@ -4049,11 +3883,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.2.1", + "version": "2.2.7", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dea9c8f2d25cc849391042b71e429c1a4bf82660", - "reference": "dea9c8f2d25cc849391042b71e429c1a4bf82660", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/692db47b9dddb0487934e5236e77d48594aef921", + "reference": "692db47b9dddb0487934e5236e77d48594aef921", "shasum": "" }, "require": { @@ -4109,7 +3943,7 @@ "type": "github" } ], - "time": "2026-05-28T14:44:12+00:00" + "time": "2026-07-29T17:39:32+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4496,16 +4330,16 @@ }, { "name": "phpunit/phpunit", - "version": "13.2.4", + "version": "13.2.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "8f5180f4627fc1978be2f61d8d9979dbe37e0c10" + "reference": "5d2afe181339a56348ef9a80fa7eb806b7eae508" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8f5180f4627fc1978be2f61d8d9979dbe37e0c10", - "reference": "8f5180f4627fc1978be2f61d8d9979dbe37e0c10", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5d2afe181339a56348ef9a80fa7eb806b7eae508", + "reference": "5d2afe181339a56348ef9a80fa7eb806b7eae508", "shasum": "" }, "require": { @@ -4528,7 +4362,7 @@ "sebastian/comparator": "^8.3.0", "sebastian/diff": "^9.0", "sebastian/environment": "^9.3.2", - "sebastian/exporter": "^8.1.0", + "sebastian/exporter": "^8.1.1", "sebastian/file-filter": "^1.0", "sebastian/git-state": "^1.0", "sebastian/global-state": "^9.0.1", @@ -4576,7 +4410,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.4" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.6" }, "funding": [ { @@ -4584,25 +4418,25 @@ "type": "other" } ], - "time": "2026-07-08T08:36:51+00:00" + "time": "2026-07-28T14:00:09+00:00" }, { "name": "rector/rector", - "version": "2.4.5", + "version": "2.5.9", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c" + "reference": "858b1fb95d94f658b54c01080bf7b6ae97274536" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", - "reference": "cbd86024be5014d3c14d9f0b3f7aae8ecbffd62c", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/858b1fb95d94f658b54c01080bf7b6ae97274536", + "reference": "858b1fb95d94f658b54c01080bf7b6ae97274536", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.56" + "phpstan/phpstan": "^2.2.6" }, "conflict": { "rector/rector-doctrine": "*", @@ -4636,7 +4470,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.4.5" + "source": "https://github.com/rectorphp/rector/tree/2.5.9" }, "funding": [ { @@ -4644,7 +4478,7 @@ "type": "github" } ], - "time": "2026-05-26T21:03:22+00:00" + "time": "2026-07-30T22:10:00+00:00" }, { "name": "sebastian/cli-parser", @@ -5034,16 +4868,16 @@ }, { "name": "sebastian/exporter", - "version": "8.1.0", + "version": "8.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "c0d29a945f8cf82f300a05e69874508e307ca4c6" + "reference": "cfaa77c750dcad6f44c9bac8f62ac486e1c82c26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c0d29a945f8cf82f300a05e69874508e307ca4c6", - "reference": "c0d29a945f8cf82f300a05e69874508e307ca4c6", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/cfaa77c750dcad6f44c9bac8f62ac486e1c82c26", + "reference": "cfaa77c750dcad6f44c9bac8f62ac486e1c82c26", "shasum": "" }, "require": { @@ -5052,7 +4886,7 @@ "sebastian/recursion-context": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^13.1.10" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { @@ -5100,7 +4934,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/8.1.0" + "source": "https://github.com/sebastianbergmann/exporter/tree/8.1.1" }, "funding": [ { @@ -5120,7 +4954,7 @@ "type": "tidelift" } ], - "time": "2026-05-21T11:50:56+00:00" + "time": "2026-07-13T11:35:11+00:00" }, { "name": "sebastian/file-filter", @@ -5886,16 +5720,16 @@ }, { "name": "symfony/var-dumper", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "40096a2515a979f3125c5c928603995b8664c62a" + "reference": "865103cf742a039f34645b971fc3ace308d6c167" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/40096a2515a979f3125c5c928603995b8664c62a", - "reference": "40096a2515a979f3125c5c928603995b8664c62a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/865103cf742a039f34645b971fc3ace308d6c167", + "reference": "865103cf742a039f34645b971fc3ace308d6c167", "shasum": "" }, "require": { @@ -5911,7 +5745,7 @@ "symfony/http-kernel": "^7.4|^8.0", "symfony/process": "^7.4|^8.0", "symfony/uid": "^7.4|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "bin": [ "Resources/bin/var-dump-server" @@ -5949,7 +5783,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.1.1" + "source": "https://github.com/symfony/var-dumper/tree/v8.1.2" }, "funding": [ { @@ -5969,7 +5803,7 @@ "type": "tidelift" } ], - "time": "2026-06-09T10:54:51+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "theseer/tokenizer", diff --git a/config/config.php b/config/config.php index 795b6d673..c98191684 100644 --- a/config/config.php +++ b/config/config.php @@ -2,32 +2,32 @@ [database] -;Сервер базы данных +;Сервер бази даних db_server = localhost -;Пользователь базы данных +;Користувач бази даних db_user = root -;Пароль к базе +;Пароль до бази даних db_password = "" -;Имя базы +;Назва бази даних db_name = okaycms-git -;Драйвер базы данных +;Драйвер бази даних db_driver = mysql -;Префикс для таблиц +;Префікс таблиць db_prefix = ok_ -;Кодировка базы данных +;Кодування бази даних db_charset = UTF8MB4 db_names = utf8mb4 ;Режим SQL db_sql_mode = "ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION" -;Смещение часового пояса +;Зміщення часового поясу ;db_timezone = +02:00 [modules] @@ -42,9 +42,21 @@ php_locale_numeric = uk_UA php_locale_time = uk_UA ;php_timezone = Europe/Kyiv -; Enable only in local overrides such as config/config.local.php. +; Увімкніть лише в локальних перевизначеннях, наприклад config/config.local.php. debug_mode = false tmp_dir = tmp/ +; Редактори JS/CSS/TPL тем на сервері вмикаються явно (безпечне значення за замовчуванням — вимкнено). +allow_theme_file_editor = false + +; Додаткове посилення заголовків відповіді (порожнє значення — не додавати). Спочатку використовуйте CSP у режимі Report-Only. +; content_security_policy_report_only = default-src 'self' +; permissions_policy = geolocation=() +; strict_transport_security = max-age=31536000; includeSubDomains + +; Обмеження IP-адрес для входу адміністратора: довіряйте переданому заголовку IP клієнта ЛИШЕ коли REMOTE_ADDR +; вказано в trusted_proxy_ips (IP-адреси або CIDR, розділені комами чи пробілами). За замовчуванням вимкнено. +; trusted_proxy_client_ip_header = X-Forwarded-For +; trusted_proxy_ips = 127.0.0.1,10.0.0.0/8 [smarty] smarty_compile_check = true @@ -53,67 +65,67 @@ smarty_debugging = false smarty_html_minify = false smarty_security = true -; Данный параметр может быть полезным при отладке модификаторов tpl файлов +; Цей параметр може бути корисним для налагодження модифікаторів TPL-файлів smarty_force_compile = false [design] debug_translation = false scripts_defer = true -;Режим разработчика, пока только в админке подписывает блоки +;Режим розробника: наразі лише позначає блоки в адмінпанелі dev_mode = false preload_head_css = false preload_head_js = false preload_footer_css = false preload_footer_js = false -;Пока TplMod находится в состоянии теста, оставим здесь возможность экстренного отключения +;Поки TplMod перебуває у стані тестування, залишаємо можливість аварійного вимкнення ;disable_tpl_mod = true [images] -;Указываем какую библиотеку использовать для нарезки изображений. Варианты: Intervention, Imagick или GD. Это имя класса адаптера +;Вкажіть бібліотеку для обробки зображень. Варіанти: Intervention, Imagick або GD. Це назва класу адаптера resize_adapter = Intervention -;Директория общих изображений дизайна (лого, фавикон...) +;Каталог спільних зображень дизайну (логотип, favicon тощо) design_images = files/images/ -;Файл изображения с водяным знаком +;Файл зображення з водяним знаком watermark_file = backend/files/watermark/watermark.png -;Промо изображения +;Промозображення special_images_dir = files/special/ -;Директория оригиналов и нарезок фоток товаров +;Каталоги оригіналів і нарізок зображень товарів original_images_dir = files/originals/products/ resized_images_dir = files/resized/products/ -;Изображения оригиналов и нарезок фоток блога +;Зображення оригіналів і нарізок для блогу original_blog_dir = files/originals/blog/ resized_blog_dir = files/resized/blog/ -;Изображения оригиналов и нарезок фоток брендов +;Зображення оригіналів і нарізок брендів original_brands_dir = files/originals/brands/ resized_brands_dir = files/resized/brands/ -;Изображения оригиналов и нарезок фоток категории +;Зображення оригіналів і нарізок категорій original_categories_dir = files/originals/categories/ resized_categories_dir = files/resized/categories/ -;Изображения оригиналов и нарезок фоток категории блога +;Зображення оригіналів і нарізок категорій блогу original_blog_categories_dir = files/originals/blog_categories/ resized_blog_categories_dir = files/resized/blog_categories/ -;Изображения оригиналов и нарезок фоток авторов +;Зображення оригіналів і нарізок авторів original_authors_dir = files/originals/authors/ resized_authors_dir = files/resized/authors/ -;Изображения оригиналов и нарезок фоток доставки +;Зображення оригіналів і нарізок способів доставки original_deliveries_dir = files/originals/deliveries/ resized_deliveries_dir = files/resized/deliveries/ -;Изображения оригиналов и нарезок фоток способов оплаты +;Зображення оригіналів і нарізок способів оплати original_payments_dir = files/originals/payments/ resized_payments_dir = files/resized/payments/ -; Папка изображений языков +; Каталог зображень мов lang_images_dir = files/originals/lang/ lang_resized_dir = files/resized/lang/ diff --git a/design/okay_shop/css.php b/design/okay_shop/css.php index 37b52cc3f..c3d125de3 100644 --- a/design/okay_shop/css.php +++ b/design/okay_shop/css.php @@ -20,4 +20,3 @@ (new Css('media.css')), (new Css('mobile_menu.css')) ]; - diff --git a/design/okay_shop/html/brands.tpl b/design/okay_shop/html/brands.tpl index ae77d4e33..728218ff5 100644 --- a/design/okay_shop/html/brands.tpl +++ b/design/okay_shop/html/brands.tpl @@ -6,52 +6,10 @@

{$h1|escape}

- -
- {* Mobile button filters *} -
- {include file="svg.tpl" svgId="filter_icon"} - {$lang->filters} -
-
- {* Sidebar with filters *} + {* Sidebar with browsed products *}
- - diff --git a/design/okay_shop/html/head.tpl b/design/okay_shop/html/head.tpl index 4c27c61be..eb18dc585 100644 --- a/design/okay_shop/html/head.tpl +++ b/design/okay_shop/html/head.tpl @@ -76,7 +76,7 @@ { "@context": "https://schema.org/", "@type": "WebSite", - "name": "{/literal}{$settings->site_name}{literal}", + "name": "{/literal}{$settings->site_name|json_ld_text}{literal}", "url": "{/literal}{url_generator route='main' absolute=1}{literal}", "potentialAction": { "@type": "SearchAction", @@ -93,7 +93,7 @@ { "@context": "https://schema.org", "@type": "Organization", - "name": "{/literal}{$settings->site_name}{literal}", + "name": "{/literal}{$settings->site_name|json_ld_text}{literal}", "url": "{/literal}{url_generator route='main' absolute=1}{literal}", "logo": "{/literal}{$rootUrl}/{$config->design_images}{$settings->site_logo}{literal}"{/literal}{if $site_social}{literal}, "sameAs": [ diff --git a/design/okay_shop/html/order.tpl b/design/okay_shop/html/order.tpl index df2529f2f..13d418886 100644 --- a/design/okay_shop/html/order.tpl +++ b/design/okay_shop/html/order.tpl @@ -187,7 +187,13 @@ {$lang->order_payment_details}
+ {if $error == 'csrf'} +
+ {$lang->form_error_csrf} +
+ {/if}
+
{foreach $payment_methods as $payment_method}
@@ -244,6 +250,7 @@ {$payment_method->name|escape}
+ {if $payment_method->description} diff --git a/design/okay_shop/html/post.tpl b/design/okay_shop/html/post.tpl index 22c8032c4..1e71fa0a9 100644 --- a/design/okay_shop/html/post.tpl +++ b/design/okay_shop/html/post.tpl @@ -297,6 +297,7 @@
{* Comment form *}
+ {if $settings->captcha_type == "v3"} {/if} @@ -313,6 +314,8 @@
{if $error=='captcha'} {$lang->form_error_captcha} + {elseif $error=='csrf'} + {$lang->form_error_csrf} {elseif $error=='empty_name'} {$lang->form_enter_name} {elseif $error=='empty_comment'} @@ -337,7 +340,7 @@ {* User's comment *}
- + {$lang->form_enter_comment}*
diff --git a/design/okay_shop/html/product.tpl b/design/okay_shop/html/product.tpl index 8a882ba76..07e2e5fd5 100644 --- a/design/okay_shop/html/product.tpl +++ b/design/okay_shop/html/product.tpl @@ -456,6 +456,7 @@
{* Comment form *} + {if $settings->captcha_type == "v3"} @@ -472,6 +473,8 @@
{if $error=='captcha'} {$lang->form_error_captcha} + {elseif $error=='csrf'} + {$lang->form_error_csrf} {elseif $error=='empty_name'} {$lang->form_enter_name} {elseif $error=='empty_comment'} @@ -497,7 +500,7 @@ {* User's comment *}
- + {$lang->form_enter_comment}*
diff --git a/design/okay_shop/html/scripts.tpl b/design/okay_shop/html/scripts.tpl index 5401a3cb8..ad1b4f372 100644 --- a/design/okay_shop/html/scripts.tpl +++ b/design/okay_shop/html/scripts.tpl @@ -7,7 +7,7 @@ okay.max_order_amount = {$settings->max_order_amount|escape}; /*Сброс фильтра*/ - {if in_array($controller, ['ProductsController', 'BrandController', 'BrandsController', 'CategoryController'])} + {if in_array($controller, ['ProductsController', 'BrandController', 'CategoryController'])} $(document).on('click', '.fn_filter_reset', function () { var date = new Date(0); document.cookie = "price_filter=; path=/; expires=" + date.toUTCString(); diff --git a/dev/README.md b/dev/README.md index fe71988a4..8c64f3184 100644 --- a/dev/README.md +++ b/dev/README.md @@ -1,4 +1,5 @@ # Docker compose for OkayCMS + ## Типова конфігурація Типова конфігурація містить Nginx в якості web сервера та інтерпретатор php-fpm. diff --git a/dev/config/nginx/templates/okay.conf.template b/dev/config/nginx/templates/okay.conf.template index 38dab5ef1..e0bfde126 100644 --- a/dev/config/nginx/templates/okay.conf.template +++ b/dev/config/nginx/templates/okay.conf.template @@ -5,19 +5,19 @@ server { listen 80; server_name ${VIRTUAL_HOST}; - + #add_header Strict-Transport-Security "max-age=31536000;"; - + set $root_path /var/www/html; root $root_path; - + charset off; index index.php index.html; disable_symlinks if_not_owner from=$root_path; - + access_log /var/www/html/dev/logs/application.access.log; error_log /var/www/html/dev/logs/application.error.log; - + # Gzip compression gzip on; gzip_comp_level 9; @@ -29,38 +29,35 @@ server { brotli on; brotli_comp_level 6; brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; + client_max_body_size 100M; - if ($host ~* ^www\.(.*)$) { - return 301 https://$server_name$request_uri; - } - if ($request_uri ~* "^(.*/)index\.php$") { return 301 $1; } - + if ($request_uri ~* "^(.*/)index\.html") { return 301 $1; } - + location / { location ~* \.(tpl.?)$ { return 404; } - + location ~* \.(htaccess|htpasswd|git|svn) { return 404; } - + location ~* ^/admin$ { rewrite /admin /backend/ redirect; } - + location ~* backend/design/js/admintooltip/admintooltip.js { rewrite ^(/backend/design/js/admintooltip)/admintooltip.js$ $1/admintooltip.php break; try_files /no_file @php; } - + location ~* backend/files/ { rewrite ^(/backend/files)/export/(.+)\.csv$ $1/index.php?file=$2&folder=export&ext=csv break; rewrite ^(/backend/files)/export_users/(.+)\.csv$ $1/index.php?file=$2&folder=export_users&ext=csv break; @@ -69,32 +66,30 @@ server { rewrite ^(\w+)$ /backend/files/index.php?file= break; try_files /does_not_exists @php; } - + location ~* files/originals { return 404; } - + location ~* docs/ { return 404; } - + location ~* /(\w+/)?(\w+/)?(.+\.(jpg|jpeg|gif|png|webp|svg|js|css|mp3|ogg|mpe?g|avi|zip|gz|bz2?|rar|swf|woff|woff2|ttf|xls|xlsx|doc|docx|pdf))$ { try_files $uri $uri/ /$2$3 /$3 /index.php?$args; allow all; access_log off; expires max; - add_header Cache-Control public; - add_header Access-Control-Allow-Origin *; } - + location ~* Okay/ { return 404; } - + location ~* backend/design/js/filemanager/config { return 404; } - + location ~* backend/lang { return 404; } @@ -102,30 +97,30 @@ server { location ~* dev/ { return 404; } - + location ~* config/ { return 404; } - + location ~* ^design/.+/(.+\.(phtml|php|php3|php4|php5|php6|phps|cgi|exe|pl|asp|aspx|shtml|shtm|fcgi|fpl|jsp|htm|html|wml)) { return 404; } - + location ~* ^files/(.+\.(phtml|php|php3|php4|php5|php6|phps|cgi|exe|pl|asp|aspx|shtml|shtm|fcgi|fpl|jsp|htm|html|wml))$ { return 404; } - + location ~ [^/]\.ph(p\d*|tml)$ { try_files /does_not_exists @php; } - + try_files $uri $uri/ /index.php?$args; - + location ~* ^.+\.(jpg|jpeg|gif|png|webp|svg|js|css|mp3|ogg|mpe?g|avi|zip|gz|bz2?|rar|swf)$ { expires max; } } - + location @php { try_files $uri $uri/ /$2$3 /$3 /index.php =404; fastcgi_pass ${FASTCGI}:9000; diff --git a/dev/scripts/build-install-package.php b/dev/scripts/build-install-package.php new file mode 100644 index 000000000..ea278256d --- /dev/null +++ b/dev/scripts/build-install-package.php @@ -0,0 +1,482 @@ + $content) { + writeGeneratedFile("{$tempDir}/{$packageDirName}/{$path}", $content); + } + + createZip($tempDir, $packageDirName, $zipPath); +} finally { + removeDirectory($tempDir); +} + +echo "Created {$zipPath}\n"; +echo "Production files: " . count($productionFiles) . "\n"; +echo "Generated package files: " . count($generatedFiles) . "\n"; + +/** + * @return array + */ +function parseOptions(array $args): array +{ + $options = []; + foreach ($args as $arg) { + if (!str_starts_with($arg, '--')) { + throw new InvalidArgumentException("Unknown positional argument: {$arg}"); + } + + $option = substr($arg, 2); + if (str_contains($option, '=')) { + [$key, $value] = explode('=', $option, 2); + $options[$key] = $value; + continue; + } + + $options[$option] = true; + } + + return $options; +} + +function printUsage(): void +{ + echo "Usage: php dev/scripts/build-install-package.php [options]\n"; + echo "\n"; + echo "Options:\n"; + echo " --root= Project root. Default: current git repository root.\n"; + echo " --ref= Build from a git ref. Default: tracked files in the current worktree.\n"; + echo " --version= Version label used in folder and archive names.\n"; + echo " --output-dir= Output directory. Default: var/release-packages.\n"; + echo " --dry-run Print included paths without creating a zip.\n"; +} + +function detectGitRoot(): string +{ + [$root] = runCommand(['git', 'rev-parse', '--show-toplevel']); + $root = trim($root); + if ($root === '') { + throw new RuntimeException('Unable to detect git repository root.'); + } + + return $root; +} + +function detectVersionLabel(string $root, ?string $ref): string +{ + if ($ref !== null) { + try { + [$tag] = runCommand(['git', '-C', $root, 'describe', '--tags', '--exact-match', $ref]); + return trim($tag); + } catch (RuntimeException) { + [$hash] = runCommand(['git', '-C', $root, 'rev-parse', '--short', $ref]); + return trim($hash); + } + } + + $readmePath = "{$root}/README.md"; + if (is_file($readmePath)) { + $readme = file($readmePath, FILE_IGNORE_NEW_LINES); + $firstLine = is_array($readme) ? ($readme[0] ?? '') : ''; + if (preg_match('/OkayCMS\s+v?([0-9]+(?:\.[0-9]+)+)/', $firstLine, $matches)) { + return $matches[1]; + } + } + + [$hash] = runCommand(['git', '-C', $root, 'rev-parse', '--short', 'HEAD']); + + return trim($hash); +} + +function normalizeVersionLabel(string $version): string +{ + $label = preg_replace('/^refs\/tags\//', '', $version) ?? $version; + $label = preg_replace('/^v(?=\d)/', '', $label) ?? $label; + $label = preg_replace('/[^A-Za-z0-9._-]+/', '_', $label) ?? $label; + + return trim($label, '_') ?: 'snapshot'; +} + +function validateRef(string $root, string $ref): void +{ + runCommand(['git', '-C', $root, 'rev-parse', '--verify', "{$ref}^{commit}"]); +} + +/** + * @return list + */ +function collectProductionFiles(string $root, ?string $ref): array +{ + $files = $ref === null ? collectTrackedWorktreeFiles($root) : collectRefFiles($root, $ref); + $productionFiles = []; + + foreach ($files as $path) { + if (isProductionPath($path)) { + $productionFiles[] = $path; + } + } + + $productionFiles = array_values(array_unique($productionFiles)); + sort($productionFiles); + + return $productionFiles; +} + +/** + * @return list + */ +function collectTrackedWorktreeFiles(string $root): array +{ + [$output] = runCommand(['git', '-C', $root, 'ls-files', '-z']); + + return splitNullSeparatedPaths($output); +} + +/** + * @return list + */ +function collectRefFiles(string $root, string $ref): array +{ + [$output] = runCommand(['git', '-C', $root, 'ls-tree', '-r', '-z', '--name-only', $ref]); + + return splitNullSeparatedPaths($output); +} + +/** + * @return list + */ +function splitNullSeparatedPaths(string $output): array +{ + if ($output === '') { + return []; + } + + return array_values(array_filter(explode("\0", rtrim($output, "\0")), static fn (string $path): bool => $path !== '')); +} + +function isProductionPath(string $path): bool +{ + $path = ltrim($path, '/'); + + if (isNonRuntimeFile($path)) { + return false; + } + + if (isGeneratedRuntimePath($path)) { + return false; + } + + if (in_array($path, productionScripts(), true)) { + return true; + } + + if (isExcludedFromProductionPackage($path)) { + return false; + } + + $topLevelDirectory = strtok($path, '/'); + if ($topLevelDirectory !== false && in_array($topLevelDirectory, productionDirectories(), true)) { + return true; + } + + return in_array($path, productionRootFiles(), true); +} + +/** + * @return list + */ +function productionDirectories(): array +{ + return [ + '1DB_changes', + 'Okay', + 'backend', + 'config', + 'design', + 'files', + 'js_libraries', + 'patches', + ]; +} + +/** + * @return list + */ +function productionRootFiles(): array +{ + return [ + '.htaccess', + '.keep_folder', + 'LICENSE.md', + 'composer.json', + 'composer.lock', + 'index.php', + 'ok', + 'robots.txt', + ]; +} + +/** + * @return list + */ +function productionScripts(): array +{ + return [ + 'scripts/apply-patches.php', + 'scripts/clear-runtime-cache.php', + 'scripts/upgrade-apply.php', + 'scripts/upgrade-inspect.php', + ]; +} + +function isNonRuntimeFile(string $path): bool +{ + $basename = basename($path); + + if (in_array($basename, ['AGENTS.md', 'CLAUDE.md', 'README.md'], true)) { + return true; + } + + return str_ends_with($path, '_example.html'); +} + +function isGeneratedRuntimePath(string $path): bool +{ + return str_starts_with($path, 'backend/files/export/') + && str_ends_with($path, '.csv'); +} + +/** + * @return array + */ +function collectGeneratedPackageFiles(string $root, ?string $ref): array +{ + return [ + 'INSTALL.md' => readSourceFile($root, $ref, 'docs/install-production.md'), + ]; +} + +function readSourceFile(string $root, ?string $ref, string $path): string +{ + if ($ref === null) { + $sourcePath = "{$root}/{$path}"; + if (!is_file($sourcePath)) { + throw new RuntimeException("Required source file not found: {$sourcePath}"); + } + + return (string) file_get_contents($sourcePath); + } + + [$content] = runCommand(['git', '-C', $root, 'show', "{$ref}:{$path}"]); + + return $content; +} + +function writePackageFile(string $root, ?string $ref, string $path, string $targetPath): void +{ + $content = readSourceFile($root, $ref, $path); + writeGeneratedFile($targetPath, $content); + + $mode = $ref === null ? worktreeFileMode($root, $path) : gitFileMode($root, $ref, $path); + if ($mode === '100755') { + chmod($targetPath, 0755); + } +} + +function writeGeneratedFile(string $targetPath, string $content): void +{ + $targetDir = dirname($targetPath); + if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) { + throw new RuntimeException("Unable to create directory: {$targetDir}"); + } + + file_put_contents($targetPath, $content); +} + +function worktreeFileMode(string $root, string $path): string +{ + return is_executable("{$root}/{$path}") ? '100755' : '100644'; +} + +function gitFileMode(string $root, string $ref, string $path): string +{ + [$output] = runCommand(['git', '-C', $root, 'ls-tree', $ref, '--', $path]); + if (!preg_match('/^([0-9]{6})\s+/', $output, $matches)) { + return '100644'; + } + + return $matches[1]; +} + +function createTempDir(string $packageDirName): string +{ + $tempBase = sys_get_temp_dir() . '/okaycms-install-' . bin2hex(random_bytes(6)); + $packageRoot = "{$tempBase}/{$packageDirName}"; + if (!mkdir($packageRoot, 0775, true) && !is_dir($packageRoot)) { + throw new RuntimeException("Unable to create temporary directory: {$packageRoot}"); + } + + return $tempBase; +} + +function createZip(string $tempDir, string $packageDirName, string $zipPath): void +{ + $zip = new ZipArchive(); + if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + throw new RuntimeException("Unable to create zip archive: {$zipPath}"); + } + + $basePath = "{$tempDir}/{$packageDirName}"; + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($basePath, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $fileInfo) { + $absolutePath = $fileInfo->getPathname(); + $relativePath = $packageDirName . '/' . substr($absolutePath, strlen($basePath) + 1); + + if ($fileInfo->isDir()) { + $zip->addEmptyDir($relativePath); + continue; + } + + $zip->addFile($absolutePath, $relativePath); + $mode = $fileInfo->isExecutable() ? 0100755 : 0100644; + $zip->setExternalAttributesName($relativePath, ZipArchive::OPSYS_UNIX, $mode << 16); + } + + $zip->close(); +} + +function removeDirectory(string $dir): void +{ + if (!is_dir($dir)) { + return; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $fileInfo) { + if ($fileInfo->isDir()) { + rmdir($fileInfo->getPathname()); + continue; + } + + unlink($fileInfo->getPathname()); + } + + rmdir($dir); +} + +function makeAbsolutePath(string $root, string $path): string +{ + if (str_starts_with($path, '/')) { + return $path; + } + + return rtrim($root, '/') . "/{$path}"; +} + +/** + * @return array{0: string, 1: string} + */ +function runCommand(array $command): array +{ + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start command: ' . implode(' ', $command)); + } + + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + + $exitCode = proc_close($process); + if ($exitCode !== 0) { + throw new RuntimeException( + "Command failed ({$exitCode}): " . implode(' ', $command) . "\n" . trim((string) $stderr) + ); + } + + return [(string) $stdout, (string) $stderr]; +} diff --git a/dev/scripts/build-upgrade-package.php b/dev/scripts/build-upgrade-package.php new file mode 100644 index 000000000..c196e5c85 --- /dev/null +++ b/dev/scripts/build-upgrade-package.php @@ -0,0 +1,438 @@ + ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes, $cwd); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start command: ' . implode(' ', $command)); + } + + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + + $exitCode = proc_close($process); + if ($exitCode !== 0) { + throw new RuntimeException( + "Command failed ({$exitCode}): " . implode(' ', $command) . "\n" . trim((string) $stderr) + ); + } + + return [(string) $stdout, (string) $stderr]; +} + +function printUsage(): void +{ + echo "Usage: php dev/scripts/build-upgrade-package.php [options]\n"; + echo "\n"; + echo "Options:\n"; + echo " --from-version= Label used in diff folder name instead of .\n"; + echo " --to-version= Label used in diff folder name instead of .\n"; + echo " --output-dir= Output directory. Default: var/release-packages.\n"; + echo " --dry-run Print included/deleted paths without creating a zip.\n"; +} + +/** + * @return array + */ +function parseOptions(array $args): array +{ + $options = []; + foreach ($args as $arg) { + if (!str_starts_with($arg, '--')) { + throw new InvalidArgumentException("Unknown positional argument: {$arg}"); + } + + $option = substr($arg, 2); + if (str_contains($option, '=')) { + [$key, $value] = explode('=', $option, 2); + $options[$key] = $value; + } else { + $options[$option] = true; + } + } + + return $options; +} + +function normalizeVersionLabel(string $ref): string +{ + $label = preg_replace('/^refs\/tags\//', '', $ref) ?? $ref; + $label = preg_replace('/^v(?=\d)/', '', $label) ?? $label; + + return preg_replace('/[^A-Za-z0-9._-]+/', '_', $label) ?? $label; +} + +function validateRef(string $ref): void +{ + runCommand(['git', 'rev-parse', '--verify', "{$ref}^{commit}"]); +} + +/** + * @return list + */ +function collectDiffEntries(string $root, string $fromRef, string $toRef): array +{ + [$output] = runCommand(['git', '-C', $root, 'diff', '--name-status', '-z', '--find-renames', $fromRef, $toRef]); + if ($output === '') { + return []; + } + + $parts = explode("\0", rtrim($output, "\0")); + $entries = []; + $index = 0; + + while ($index < count($parts)) { + $status = $parts[$index++]; + if ($status === '') { + continue; + } + + $code = $status[0]; + if ($code === 'R') { + $oldPath = $parts[$index++] ?? null; + $newPath = $parts[$index++] ?? null; + if ($oldPath === null || $newPath === null) { + throw new RuntimeException('Malformed rename entry in git diff output.'); + } + + $entries[] = ['include' => $newPath, 'delete' => $oldPath]; + continue; + } + + if ($code === 'C') { + $index++; + $newPath = $parts[$index++] ?? null; + if ($newPath === null) { + throw new RuntimeException('Malformed copy entry in git diff output.'); + } + + $entries[] = ['include' => $newPath, 'delete' => null]; + continue; + } + + $path = $parts[$index++] ?? null; + if ($path === null) { + throw new RuntimeException('Malformed entry in git diff output.'); + } + + $entries[] = [ + 'include' => $code === 'D' ? null : $path, + 'delete' => $code === 'D' ? $path : null, + ]; + } + + return $entries; +} + +function isProductionPath(string $path): bool +{ + $path = ltrim($path, '/'); + + if (isNonRuntimeFile($path)) { + return false; + } + + if (isGeneratedRuntimePath($path)) { + return false; + } + + if (in_array($path, productionScripts(), true)) { + return true; + } + + if (isExcludedFromProductionPackage($path)) { + return false; + } + + $topLevelDirectory = strtok($path, '/'); + if ($topLevelDirectory !== false && in_array($topLevelDirectory, productionDirectories(), true)) { + return true; + } + + if (in_array($path, productionRootFiles(), true)) { + return true; + } + + return false; +} + +/** + * @return list + */ +function productionDirectories(): array +{ + return [ + '1DB_changes', + 'Okay', + 'backend', + 'config', + 'design', + 'files', + 'js_libraries', + 'patches', + ]; +} + +/** + * @return list + */ +function productionRootFiles(): array +{ + return [ + '.htaccess', + '.keep_folder', + 'composer.json', + 'composer.lock', + 'index.php', + 'ok', + 'robots.txt', + ]; +} + +/** + * @return list + */ +function productionScripts(): array +{ + return [ + 'scripts/apply-patches.php', + 'scripts/clear-runtime-cache.php', + 'scripts/upgrade-apply.php', + 'scripts/upgrade-inspect.php', + ]; +} + +function isNonRuntimeFile(string $path): bool +{ + $basename = basename($path); + + if (in_array($basename, ['AGENTS.md', 'CLAUDE.md', 'README.md'], true)) { + return true; + } + + return str_ends_with($path, '_example.html'); +} + +function isGeneratedRuntimePath(string $path): bool +{ + return str_starts_with($path, 'backend/files/export/') + && str_ends_with($path, '.csv'); +} + +function makeAbsolutePath(string $root, string $path): string +{ + if (str_starts_with($path, '/')) { + return $path; + } + + return "{$root}/{$path}"; +} + +function createTempDir(string $packageDirName): string +{ + $tempBase = sys_get_temp_dir() . '/okaycms-upgrade-' . bin2hex(random_bytes(6)); + $packageRoot = "{$tempBase}/{$packageDirName}"; + if (!mkdir($packageRoot, 0775, true) && !is_dir($packageRoot)) { + throw new RuntimeException("Unable to create temporary directory: {$packageRoot}"); + } + + return $tempBase; +} + +function writeGitBlob(string $root, string $ref, string $path, string $targetPath): void +{ + [$content] = runCommand(['git', '-C', $root, 'show', "{$ref}:{$path}"]); + $targetDir = dirname($targetPath); + if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true) && !is_dir($targetDir)) { + throw new RuntimeException("Unable to create directory: {$targetDir}"); + } + + file_put_contents($targetPath, $content); + + $mode = gitFileMode($root, $ref, $path); + if ($mode === '100755') { + chmod($targetPath, 0755); + } +} + +function gitFileMode(string $root, string $ref, string $path): string +{ + [$output] = runCommand(['git', '-C', $root, 'ls-tree', $ref, '--', $path]); + if (!preg_match('/^([0-9]{6})\s+/', $output, $matches)) { + return '100644'; + } + + return $matches[1]; +} + +function createZip(string $tempDir, string $packageDirName, string $zipPath): void +{ + $zip = new ZipArchive(); + if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { + throw new RuntimeException("Unable to create zip archive: {$zipPath}"); + } + + $basePath = "{$tempDir}/{$packageDirName}"; + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($basePath, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::SELF_FIRST + ); + + foreach ($iterator as $fileInfo) { + $absolutePath = $fileInfo->getPathname(); + $relativePath = $packageDirName . '/' . substr($absolutePath, strlen($basePath) + 1); + + if ($fileInfo->isDir()) { + $zip->addEmptyDir($relativePath); + continue; + } + + $zip->addFile($absolutePath, $relativePath); + $mode = $fileInfo->isExecutable() ? 0100755 : 0100644; + $zip->setExternalAttributesName($relativePath, ZipArchive::OPSYS_UNIX, $mode << 16); + } + + $zip->close(); +} + +function removeDirectory(string $dir): void +{ + if (!is_dir($dir)) { + return; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $fileInfo) { + if ($fileInfo->isDir()) { + rmdir($fileInfo->getPathname()); + } else { + unlink($fileInfo->getPathname()); + } + } + + rmdir($dir); +} diff --git a/dev/scripts/changelog-draft.php b/dev/scripts/changelog-draft.php new file mode 100644 index 000000000..1d46c68a0 --- /dev/null +++ b/dev/scripts/changelog-draft.php @@ -0,0 +1,389 @@ +getMessage() . "\n"); + exit(1); +} + +function printUsage(): void +{ + echo "Usage: php dev/scripts/changelog-draft.php [options]\n"; + echo "\n"; + echo "Options:\n"; + echo " --root= Project root. Default: current git repository root.\n"; + echo " --from= Base tag/ref. Default: latest reachable git tag.\n"; + echo " --to= Head ref. Default: HEAD.\n"; + echo " --check Check whether [Невипущено] likely covers candidate commits.\n"; +} + +/** + * @param list $args + * @return array + */ +function parseOptions(array $args): array +{ + $options = []; + foreach ($args as $arg) { + if (!str_starts_with($arg, '--')) { + throw new InvalidArgumentException("Unknown positional argument: {$arg}"); + } + + $option = substr($arg, 2); + if (str_contains($option, '=')) { + [$key, $value] = explode('=', $option, 2); + $options[$key] = $value; + continue; + } + + $options[$option] = true; + } + + return $options; +} + +function detectGitRoot(): string +{ + [$root] = runCommand(['git', 'rev-parse', '--show-toplevel']); + $root = trim($root); + if ($root === '') { + throw new RuntimeException('Unable to detect git repository root.'); + } + + return $root; +} + +function detectLatestReachableTag(string $root): string +{ + [$tag] = runCommand(['git', '-C', $root, 'describe', '--tags', '--abbrev=0']); + $tag = trim($tag); + if ($tag === '') { + throw new RuntimeException('Unable to detect latest reachable git tag.'); + } + + return $tag; +} + +/** + * @return list + */ +function collectChangelogCommits(string $root, string $from, string $to): array +{ + [$log] = runCommand([ + 'git', + '-C', + $root, + 'log', + '--reverse', + '--format=%h%x00%s%x00', + "{$from}..{$to}", + ]); + + $parts = explode("\0", rtrim($log, "\0")); + $commits = []; + + for ($index = 0; $index + 1 < count($parts); $index += 2) { + $subject = trim($parts[$index + 1]); + $parsed = parseConventionalSubject($subject); + if ($parsed === null || shouldIgnoreCommit($parsed['type'], $parsed['scope'], $parsed['description'])) { + continue; + } + + $commits[] = [ + 'hash' => trim($parts[$index]), + 'subject' => $subject, + 'type' => $parsed['type'], + 'scope' => $parsed['scope'], + 'description' => $parsed['description'], + 'category' => categorizeCommit($parsed['type'], $parsed['scope'], $parsed['description']), + ]; + } + + return $commits; +} + +/** + * @return array{type: string, scope: string, description: string}|null + */ +function parseConventionalSubject(string $subject): ?array +{ + if (!preg_match('/^(?[a-z]+)(?:\((?[^)]+)\))?!?:\s+(?.+)$/', $subject, $matches)) { + return null; + } + + return [ + 'type' => strtolower($matches['type']), + 'scope' => strtolower($matches['scope'] ?? ''), + 'description' => trim($matches['description']), + ]; +} + +function shouldIgnoreCommit(string $type, string $scope, string $description): bool +{ + $text = strtolower("{$scope} {$description}"); + + return ($type === 'chore' && str_contains($text, 'release')) + || ($type === 'docs' && str_contains($text, 'changelog')) + || ($type === 'docs' && str_contains($text, 'release')); +} + +function categorizeCommit(string $type, string $scope, string $description): string +{ + $text = strtolower("{$scope} {$description}"); + if (preg_match('/\b(security|csrf|xss|auth|password|token|upload|svg|sanitize|sanitized)\b/', $text)) { + return 'Безпека'; + } + + return match ($type) { + 'feat' => 'Додано', + 'fix' => 'Виправлено', + 'perf', 'refactor', 'build', 'ci', 'chore', 'test', 'docs' => 'Змінено', + default => 'Інше', + }; +} + +/** + * @param list $commits + */ +function printDraft(array $commits, string $from, string $to): void +{ + echo "Changelog draft for {$from}..{$to}\n"; + echo "Review and rewrite these bullets for the Ukrainian [Невипущено] section.\n\n"; + + if ($commits === []) { + echo "No conventional commits found in range.\n"; + return; + } + + foreach (['Додано', 'Змінено', 'Виправлено', 'Безпека', 'Інше'] as $category) { + $categoryCommits = array_values(array_filter( + $commits, + static fn (array $commit): bool => $commit['category'] === $category + )); + + if ($categoryCommits === []) { + continue; + } + + echo "### {$category}\n\n"; + foreach ($categoryCommits as $commit) { + echo "- TODO: {$commit['subject']} ({$commit['hash']})\n"; + } + echo "\n"; + } +} + +/** + * @param list $commits + */ +function runCheck(string $root, array $commits, string $from, string $to): void +{ + if ($commits === []) { + echo "No conventional commits found in {$from}..{$to}.\n"; + return; + } + + $unreleased = extractUnreleasedSection(readProjectFile($root, 'CHANGELOG.md')); + $normalizedUnreleased = normalizeText($unreleased); + $missing = []; + + foreach ($commits as $commit) { + if (isCommitCovered($commit, $normalizedUnreleased)) { + continue; + } + + $missing[] = $commit; + } + + if ($missing === []) { + echo "CHANGELOG.md [Невипущено] likely covers {$from}..{$to}.\n"; + return; + } + + fwrite(STDERR, "CHANGELOG.md [Невипущено] may be missing entries for {$from}..{$to}:\n"); + foreach ($missing as $commit) { + fwrite(STDERR, " - {$commit['hash']} {$commit['subject']}\n"); + } + + fwrite(STDERR, "\nRun composer release:changelog:draft -- --from={$from} --to={$to}\n"); + exit(1); +} + +function extractUnreleasedSection(string $changelog): string +{ + if (!preg_match('/^## \[Невипущено]\R(?
.*?)(?=^## \[)/msu', $changelog, $matches)) { + throw new RuntimeException('Unable to find [Невипущено] section in CHANGELOG.md.'); + } + + return $matches['section']; +} + +/** + * @param array{hash: string, scope: string, description: string} $commit + */ +function isCommitCovered(array $commit, string $normalizedUnreleased): bool +{ + if (str_contains($normalizedUnreleased, strtolower($commit['hash']))) { + return true; + } + + $keywords = commitKeywords($commit['scope'], $commit['description']); + if ($keywords === []) { + return true; + } + + $matched = 0; + foreach ($keywords as $keyword) { + if (keywordMatches($keyword, $normalizedUnreleased)) { + $matched++; + } + } + + return $matched >= min(2, count($keywords)); +} + +function keywordMatches(string $keyword, string $normalizedUnreleased): bool +{ + foreach (keywordVariants($keyword) as $variant) { + if (str_contains($normalizedUnreleased, $variant)) { + return true; + } + } + + return false; +} + +/** + * @return non-empty-list + */ +function keywordVariants(string $keyword): array +{ + $variants = [ + 'cart' => ['cart', 'кошик', 'кошика'], + 'database' => ['database', 'база', 'бази', 'даних'], + 'optimization' => ['optimization', 'оптимізація', 'оптимізації'], + 'purchases' => ['purchases', 'товарів', 'товари'], + 'removing' => ['removing', 'видалення', 'видалено'], + ]; + + return $variants[$keyword] ?? [$keyword]; +} + +/** + * @return list + */ +function commitKeywords(string $scope, string $description): array +{ + $words = preg_split('/[^a-z0-9.]+/', normalizeText("{$scope} {$description}")) ?: []; + $keywords = []; + $stopWords = [ + 'add', + 'allow', + 'from', + 'into', + 'load', + 'mode', + 'release', + 'respect', + 'the', + 'update', + 'with', + ]; + + foreach ($words as $word) { + if (strlen($word) < 3 || in_array($word, $stopWords, true)) { + continue; + } + + $keywords[] = $word; + } + + return array_slice(array_values(array_unique($keywords)), 0, 5); +} + +function normalizeText(string $text): string +{ + return strtolower($text); +} + +function readProjectFile(string $root, string $path): string +{ + $filePath = "{$root}/{$path}"; + if (!is_file($filePath)) { + throw new RuntimeException("Required file not found: {$filePath}"); + } + + return (string) file_get_contents($filePath); +} + +function makeAbsolutePath(string $root, string $path): string +{ + if (str_starts_with($path, '/')) { + return $path; + } + + return rtrim($root, '/') . "/{$path}"; +} + +/** + * @param list $command + * @return array{0: string, 1: string} + */ +function runCommand(array $command): array +{ + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $process = proc_open($command, $descriptorSpec, $pipes); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start command: ' . implode(' ', $command)); + } + + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + + $exitCode = proc_close($process); + if ($exitCode !== 0) { + throw new RuntimeException( + "Command failed ({$exitCode}): " . implode(' ', $command) . "\n" . trim((string) $stderr) + ); + } + + return [(string) $stdout, (string) $stderr]; +} diff --git a/dev/scripts/lib/package-prod-ignore.php b/dev/scripts/lib/package-prod-ignore.php new file mode 100644 index 000000000..f5e524751 --- /dev/null +++ b/dev/scripts/lib/package-prod-ignore.php @@ -0,0 +1,22 @@ +_install.zip`. +- Домен або тестовий піддомен, на який буде відкриватися магазин. +- Хостинг з PHP 8.5 або новішим у межах PHP 8.5. +- MySQL або MariaDB. +- Доступ до панелі хостингу, файлового менеджера або FTP. +- Дані нової бази даних: сервер, назва бази, користувач і пароль. +- Доступ до Composer на хостингу. Якщо Composer недоступний, попросіть + підтримку хостингу виконати команду встановлення залежностей. + +## 1. Створіть базу даних + +1. Відкрийте панель керування хостингом. +2. Знайдіть розділ "Бази даних", "MySQL" або схожий за назвою. +3. Створіть нову базу даних. +4. Створіть користувача бази даних і задайте пароль. +5. Дайте цьому користувачу повні права на створену базу. +6. Запишіть сервер бази, назву бази, користувача і пароль. Вони знадобляться + під час встановлення. + +Якщо хостинг просить вибрати кодування, оберіть `utf8mb4`. + +## 2. Завантажте файли магазину + +1. Розпакуйте архів `okaycms__install.zip` на комп'ютері або прямо на + хостингу. +2. Всередині буде папка `okaycms_`. +3. Завантажте вміст цієї папки у кореневу папку сайту на хостингу. Часто вона + називається `public_html`, `www`, `htdocs` або за назвою домену. +4. Перевірте, що файл `index.php` лежить саме у кореневій папці сайту, а не в + додатковій вкладеній папці. + +## 3. Встановіть залежності Composer + +У кореневій папці сайту виконайте: + +```bash +composer install --no-dev --optimize-autoloader +``` + +Ця команда завантажує бібліотеки, які потрібні магазину для роботи. Якщо ви не +бачите, де виконати команду, зверніться до підтримки хостингу і передайте їм цю +команду. + +## 4. Налаштуйте доступ до бази + +У production не створюйте і не завантажуйте `config/config.local.php`. Це +ігнорований файл локальних перевизначень для development-середовища. У +production налаштування бази задаються у `config/config.php`. + +Відкрийте `config/config.php` і вкажіть дані production-бази: + +```ini +db_server = ваш_сервер_бази +db_user = ваш_користувач +db_password = "ваш_пароль" +db_name = ваша_назва_бази +``` + +Залиште `db_driver = mysql`, якщо хостинг не дав інших вказівок. + +## 5. Завантажте початкову базу магазину + +Найпростіший спосіб - виконати команду: + +```bash +php ok database:deploy +``` + +Команда поставить кілька питань. Підтвердьте встановлення чистої бази. Якщо +команда запропонує змінити доступи до бази, введіть ті самі дані, які створили +на хостингу. + +Якщо CLI-команди недоступні, імпортуйте файл `1DB_changes/okay_clean.sql` через +phpMyAdmin або інший інструмент у панелі хостингу. + +## 6. Перевірте права на папки + +Магазин повинен мати право запису у службові папки. Зазвичай хостинг уже +налаштований правильно. Якщо після відкриття сайту з'являються помилки про +запис файлів, перевірте права на такі папки: + +- `cache` +- `compiled` +- `files` +- `backend/files` +- `backend/design/compiled` +- `Okay/xml/compiled` + +Не ставте права `777`, якщо підтримка хостингу прямо цього не просить. Краще +попросіть хостинг налаштувати власника файлів для PHP. + +## 7. Відкрийте сайт і адмін-панель + +1. Відкрийте домен магазину в браузері. +2. Перевірте, що головна сторінка відкривається без PHP-помилок. +3. Відкрийте адмін-панель за адресою `/backend/`. +4. Увійдіть під даними, які є у чистій базі, або змініть їх одразу після + першого входу. + +Після першого входу змініть пароль адміністратора і перевірте контактний email. + +## 8. Увімкніть cron, якщо він потрібен + +Якщо ваш магазин використовує автоматичні задачі, додайте у планувальник +хостингу команду: + +```bash +php /path/to/site/ok scheduler:run +``` + +Замість `/path/to/site` вкажіть реальний шлях до папки сайту на хостингу. +Інтервал запуску залежить від потреб магазину. Якщо не впевнені, почніть з +одного запуску кожні 5 хвилин. + +## Якщо щось не працює + +- Перевірте, що PHP на хостингу має версію 8.5. +- Перевірте дані бази у `config/config.php` і переконайтеся, що + `config/config.local.php` не містить локальних налаштувань production-сайту. +- Переконайтеся, що Composer-команда виконалась без помилок. +- Перевірте, що `index.php` лежить у корені сайту. +- Перегляньте журнал помилок у панелі хостингу. +- Якщо бачите помилку бази даних, перевірте, чи імпортовано + `1DB_changes/okay_clean.sql`. diff --git a/docs/modules/common-errors.md b/docs/modules/common-errors.md index a0bda3bee..54932a890 100644 --- a/docs/modules/common-errors.md +++ b/docs/modules/common-errors.md @@ -10,13 +10,13 @@ | Валідація запускається не в той момент | Кнопка додавання в кошик не працює | Перевірте POST-параметр `checkout` | [Сумісність модулів](compatibility.md) | | JS-файли лежать не в тій директорії | `File not found` | Для `addFrontBlock()` використовуйте `design/html/` | [Швидкий старт](quick_start.md) | | Некоректний namespace контролера | `Class not exists` | Використовуйте `::class`, а не запис через крапки | [Контролери](../controllers.md) | -| Нестандартний backend UI | Сторінка адмін-панелі не узгоджується з системною | Дотримуйтеся стандартної структури | [Структура проєкту](../../../docs/project-structure.md) | +| Нестандартний backend UI | Сторінка адмін-панелі не узгоджується з системною | Дотримуйтеся стандартної структури | [Сумісність модулів](compatibility.md) | | Неправильна мова адмін-панелі | Переклади не оновлюються | Використовуйте `BackendTranslations::getLangLabel()` | [Сумісність модулів](compatibility.md) | | Неправильна мова AJAX-запиту | Завжди використовується одна мова | Визначайте мову з referrer | [Сумісність модулів](compatibility.md) | | Валюта не відповідає мові | Знак валюти завжди один | Встановіть мову перед отриманням валюти | [Сумісність модулів](compatibility.md) | | Тексти налаштувань збережені у мовних файлах | Типові повідомлення не змінюються | Створіть Entity для повідомлень | [Сумісність модулів](compatibility.md) | -| Позиційні placeholders в Aura SQL | `Undefined array key` | Використовуйте named placeholders | [Патерни Aura SQL](../../../docs/patterns/aura-sql.md) | -| `bindValues` після `joinSubSelect` | `Undefined array key` | Викликайте `bindValues` перед `joinSubSelect` | [Патерни Aura SQL](../../../docs/patterns/aura-sql.md) | +| Позиційні placeholders в Aura SQL | `Undefined array key` | Використовуйте named placeholders | Цей документ | +| `bindValues` після `joinSubSelect` | `Undefined array key` | Викликайте `bindValues` перед `joinSubSelect` | Цей документ | | Поле Entity не зареєстроване | Поле відсутнє в SELECT-запитах | Викликайте `registerEntityField()` в `init()` | [Сумісність модулів](compatibility.md) | ## Пояснення @@ -136,8 +136,6 @@ $this->select->where("field = :{$placeholder}") ->bindValue($placeholder, (int)$id); ``` -**Детальніше:** [Патерни Aura SQL](../../../docs/patterns/aura-sql.md). - ### Поле Entity не зареєстроване **Помилка:** поле відсутнє в SELECT-запитах, `get()` повертає `null`. diff --git a/docs/modules/compatibility.md b/docs/modules/compatibility.md index 5c69b3b47..844c2d735 100644 --- a/docs/modules/compatibility.md +++ b/docs/modules/compatibility.md @@ -57,16 +57,14 @@ Extender callback першим аргументом отримує резуль - [Типові помилки модулів](common-errors.md) - [Ініціалізація модуля](init.md) - [Оновлення таблиць БД модулів](table_migrate.md) -- [Патерни Aura SQL](../../../docs/patterns/aura-sql.md) -- [Заборонені патерни](../../../docs/patterns/forbidden.md) ## Перевірка Для модулів використовуйте ті самі перевірки, що й для проєкту: ```bash -ddev composer analyze:static -ddev composer test:unit +ddev co analyze:static +ddev co test:unit ``` Додавайте вузькі runtime smoke-перевірки, якщо модуль змінює storefront, diff --git a/index.php b/index.php index fde50b8c7..8942a3d3d 100644 --- a/index.php +++ b/index.php @@ -10,21 +10,38 @@ use Okay\Core\Modules\Modules; use Okay\Core\OkayContainer\OkayContainer; use Okay\Core\Security\AdminSession; +use Okay\Core\Security\TrustedRequestContext; use Psr\Log\LoggerInterface; ini_set('display_errors', 'off'); require_once('vendor/autoload.php'); -session_name('okay_sid'); -session_start(); -AdminSession::syncFrontendAdmin($_COOKIE, $_SERVER); - /** @var OkayContainer $DI */ $DI = include 'Okay/Core/config/container.php'; -/** @var Config $config Конфигурируем в конструкторе сервиса параметры системы */ +/** @var Config $config */ $config = $DI->get(Config::class); +$trustedProxyIps = $config->get('trusted_proxy_ips'); +$trustedClientIpHeader = $config->get('trusted_proxy_client_ip_header'); +$trustedRequestContext = TrustedRequestContext::fromServer( + $_SERVER, + is_string($trustedProxyIps) ? $trustedProxyIps : null, + is_string($trustedClientIpHeader) ? $trustedClientIpHeader : 'X-Forwarded-For' +); +Request::setTrustedRequestContext($trustedRequestContext); + +session_name('okay_sid'); +session_set_cookie_params([ + 'lifetime' => 0, + 'path' => '/', + 'domain' => '', + 'secure' => $trustedRequestContext->isSecure(), + 'httponly' => true, + 'samesite' => 'Lax', +]); +session_start(); +AdminSession::syncFrontendAdmin($_COOKIE, $_SERVER); /** Инициализируем панель отладки, если включен режим отладки */ if ($config->get('debug_mode') == true) { @@ -65,7 +82,7 @@ setcookie('admin_login', '', [ 'expires' => time() - 100, 'path' => '/', - 'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off', + 'secure' => $trustedRequestContext->isSecure(), 'httponly' => true, 'samesite' => 'Lax', ]); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 42de56e38..7502eee25 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -36,6 +36,12 @@ parameters: count: 1 path: Okay/Controllers/BrandController.php + - + message: '#^Parameter \#3 \$featuresFilter of method Okay\\Helpers\\MetaRobotsHelper\:\:getCatalogRobots\(\) expects array\\>, 0\|0\.0\|''''\|''0''\|array\|false given\.$#' + identifier: argument.type + count: 1 + path: Okay/Controllers/BrandController.php + - message: '#^Method Okay\\Controllers\\BrandsController\:\:getFilter\(\) has no return type specified\.$#' identifier: missingType.return @@ -84,6 +90,12 @@ parameters: count: 1 path: Okay/Controllers/CategoryController.php + - + message: '#^Parameter \#3 \$featuresFilter of method Okay\\Helpers\\MetaRobotsHelper\:\:getCatalogRobots\(\) expects array\\>, 0\|0\.0\|''''\|''0''\|array\|false given\.$#' + identifier: argument.type + count: 1 + path: Okay/Controllers/CategoryController.php + - message: '#^Method Okay\\Controllers\\ComparisonController\:\:ajaxUpdate\(\) has no return type specified\.$#' identifier: missingType.return @@ -186,6 +198,12 @@ parameters: count: 1 path: Okay/Controllers/ProductsController.php + - + message: '#^Parameter \#3 \$featuresFilter of method Okay\\Helpers\\MetaRobotsHelper\:\:getCatalogRobots\(\) expects array\\>, 0\|0\.0\|''''\|''0''\|array\|false given\.$#' + identifier: argument.type + count: 1 + path: Okay/Controllers/ProductsController.php + - message: '#^Method Okay\\Controllers\\ResizeController\:\:resize\(\) has no return type specified\.$#' identifier: missingType.return @@ -20340,24 +20358,6 @@ parameters: count: 1 path: Okay/Modules/OkayCMS/RozetkaPay/Backend/Controllers/RefundAdmin.php - - - message: '#^Method Okay\\Modules\\OkayCMS\\RozetkaPay\\Controllers\\CallbackController\:\:getPaymentDetails\(\) has parameter \$id with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Okay/Modules/OkayCMS/RozetkaPay/Controllers/CallbackController.php - - - - message: '#^Method Okay\\Modules\\OkayCMS\\RozetkaPay\\Controllers\\CallbackController\:\:getPaymentDetails\(\) has parameter \$queryFactory with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Okay/Modules/OkayCMS/RozetkaPay/Controllers/CallbackController.php - - - - message: '#^Method Okay\\Modules\\OkayCMS\\RozetkaPay\\Controllers\\CallbackController\:\:getPaymentDetails\(\) has parameter \$table with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Okay/Modules/OkayCMS/RozetkaPay/Controllers/CallbackController.php - - message: '#^Method Okay\\Modules\\OkayCMS\\RozetkaPay\\Controllers\\CallbackController\:\:payOrder\(\) has no return type specified\.$#' identifier: missingType.return @@ -20622,12 +20622,6 @@ parameters: count: 1 path: Okay/Modules/OkayCMS/WayForPay/Controllers/CallbackController.php - - - message: '#^Parameter \#2 \$array of function array_key_exists expects array, object given\.$#' - identifier: argument.type - count: 1 - path: Okay/Modules/OkayCMS/WayForPay/Controllers/CallbackController.php - - message: '#^Method Okay\\Modules\\OkayCMS\\WayForPay\\Init\\Init\:\:init\(\) has no return type specified\.$#' identifier: missingType.return @@ -21060,12 +21054,6 @@ parameters: count: 1 path: backend/Controllers/ImportLogAdmin.php - - - message: '#^Variable \$page might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/Controllers/ImportLogAdmin.php - - message: '#^Method Okay\\Admin\\Controllers\\IndexAdmin\:\:__construct\(\) has parameter \$backendController with no type specified\.$#' identifier: missingType.parameter @@ -28599,30 +28587,6 @@ parameters: count: 1 path: backend/ajax/import.php - - - message: '#^Variable \$entityFactory might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/lesson_done.php - - - - message: '#^Variable \$manager might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/lesson_done.php - - - - message: '#^Variable \$managers might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/lesson_done.php - - - - message: '#^Variable \$request might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/lesson_done.php - - message: '#^Variable \$entityFactory might not be defined\.$#' identifier: variable.undefined @@ -28713,96 +28677,6 @@ parameters: count: 2 path: backend/ajax/options_autocomplete.php - - - message: '#^Variable \$config might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: backend/ajax/save_script.php - - - - message: '#^Variable \$manager might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/save_script.php - - - - message: '#^Variable \$managers might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/save_script.php - - - - message: '#^Variable \$request might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: backend/ajax/save_script.php - - - - message: '#^Variable \$response might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: backend/ajax/save_script.php - - - - message: '#^Variable \$config might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: backend/ajax/save_style.php - - - - message: '#^Variable \$manager might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/save_style.php - - - - message: '#^Variable \$managers might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/save_style.php - - - - message: '#^Variable \$request might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: backend/ajax/save_style.php - - - - message: '#^Variable \$response might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: backend/ajax/save_style.php - - - - message: '#^Variable \$config might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: backend/ajax/save_template.php - - - - message: '#^Variable \$manager might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/save_template.php - - - - message: '#^Variable \$managers might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/ajax/save_template.php - - - - message: '#^Variable \$request might not be defined\.$#' - identifier: variable.undefined - count: 5 - path: backend/ajax/save_template.php - - - - message: '#^Variable \$response might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: backend/ajax/save_template.php - - message: '#^Variable \$DI might not be defined\.$#' identifier: variable.undefined @@ -28965,12 +28839,6 @@ parameters: count: 2 path: backend/ajax/update_order.php - - - message: '#^Comparison operation "\>\=" between 1 and 0 is always true\.$#' - identifier: greaterOrEqual.alwaysTrue - count: 1 - path: backend/design/js/filemanager/UploadHandler.php - - message: '#^Method UploadHandler\:\:__construct\(\) has parameter \$error_messages with no type specified\.$#' identifier: missingType.parameter @@ -30294,7 +30162,7 @@ parameters: - message: '#^Variable \$path_thumb might not be defined\.$#' identifier: variable.undefined - count: 16 + count: 14 path: backend/design/js/filemanager/execute.php - @@ -30574,1912 +30442,232 @@ parameters: path: backend/design/js/filemanager/include/mime_type_lib.php - - message: '#^Access to property \$infoArray on an unknown class PhpPsdReader\.$#' - identifier: class.notFound + message: '#^Argument of an invalid type string supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Binary operation "/" between string and string results in an error\.$#' + message: '#^Binary operation "\*" between string and 1024 results in an error\.$#' identifier: binaryOp.invalid - count: 3 - path: backend/design/js/filemanager/include/php_image_magician.php + count: 2 + path: backend/design/js/filemanager/include/utils.php - - message: '#^Call to method getImage\(\) on an unknown class PhpPsdReader\.$#' + message: '#^Call to method send\(\) on an unknown class Illuminate\\Http\\Response\.$#' identifier: class.notFound count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Comparison operation "\>\=" between 1 and 0 is always true\.$#' - identifier: greaterOrEqual.alwaysTrue - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + message: '#^Cannot use array destructuring on int\.$#' + identifier: offsetAccess.nonArray + count: 2 + path: backend/design/js/filemanager/include/utils.php - - message: '#^Instantiated class PhpPsdReader not found\.$#' - identifier: class.notFound - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + message: '#^Function AddErrorLocation\(\) should return bool but returns string\.$#' + identifier: return.type + count: 2 + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:GD2BMPstring\(\) has no return type specified\.$#' + message: '#^Function checkRelativePathPartial\(\) has no return type specified\.$#' identifier: missingType.return count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:GD2BMPstring\(\) has parameter \$gd_image with no type specified\.$#' + message: '#^Function checkRelativePathPartial\(\) has parameter \$path with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php + + - + message: '#^Function check_file_extension invoked with 1 parameter, 2 required\.$#' + identifier: arguments.count + count: 1 + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:GetPixelColor\(\) has no return type specified\.$#' + message: '#^Function check_file_extension\(\) has no return type specified\.$#' identifier: missingType.return count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:GetPixelColor\(\) has parameter \$img with no type specified\.$#' - identifier: missingType.parameter + message: '#^Function check_files_extensions_on_path\(\) has no return type specified\.$#' + identifier: missingType.return count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:GetPixelColor\(\) has parameter \$x with no type specified\.$#' - identifier: missingType.parameter + message: '#^Function check_files_extensions_on_phar\(\) has no return type specified\.$#' + identifier: missingType.return count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:GetPixelColor\(\) has parameter \$y with no type specified\.$#' + message: '#^Function check_files_extensions_on_phar\(\) has parameter \$config with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:ImageCreateFromBMP\(\) has no return type specified\.$#' - identifier: missingType.return + message: '#^Function config_loading\(\) has parameter \$current_path with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:ImageCreateFromBMP\(\) has parameter \$filename with no type specified\.$#' + message: '#^Function config_loading\(\) has parameter \$fld with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:LittleEndian2String\(\) has no return type specified\.$#' + message: '#^Function create_folder\(\) has no return type specified\.$#' identifier: missingType.return count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:LittleEndian2String\(\) has parameter \$minbytes with no type specified\.$#' + message: '#^Function create_folder\(\) has parameter \$config with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:LittleEndian2String\(\) has parameter \$number with no type specified\.$#' + message: '#^Function create_folder\(\) has parameter \$ftp with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:__construct\(\) has parameter \$fileName with no type specified\.$#' + message: '#^Function create_img\(\) has parameter \$config with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addBorder\(\) has no return type specified\.$#' - identifier: missingType.return + message: '#^Function create_img\(\) should return bool but returns string\.$#' + identifier: return.type count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addBorder\(\) has parameter \$rgbArray with no type specified\.$#' - identifier: missingType.parameter + message: '#^Function debugger\(\) has no return type specified\.$#' + identifier: missingType.return count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addBorder\(\) has parameter \$thickness with no type specified\.$#' + message: '#^Function deleteDir\(\) has parameter \$config with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addCaptionBox\(\) has no return type specified\.$#' - identifier: missingType.return + message: '#^Function deleteDir\(\) has parameter \$ftp with no type specified\.$#' + identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addCaptionBox\(\) has parameter \$bgColor with no type specified\.$#' - identifier: missingType.parameter + message: '#^Function deleteDir\(\) should return bool but returns null\.$#' + identifier: return.type count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addCaptionBox\(\) has parameter \$padding with no type specified\.$#' - identifier: missingType.parameter + message: '#^Function deleteFile\(\) should return null but empty return statement found\.$#' + identifier: return.empty count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addCaptionBox\(\) has parameter \$side with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + message: '#^Function deleteFile\(\) should return null but return statement is missing\.$#' + identifier: return.missing + count: 2 + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addCaptionBox\(\) has parameter \$thickness with no type specified\.$#' + message: '#^Function duplicate_file\(\) has parameter \$config with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addCaptionBox\(\) has parameter \$transaprencyAmount with no type specified\.$#' + message: '#^Function duplicate_file\(\) has parameter \$ftp with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addReflection\(\) has no return type specified\.$#' - identifier: missingType.return + message: '#^Function duplicate_file\(\) should return bool but return statement is missing\.$#' + identifier: return.missing count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addReflection\(\) has parameter \$bgColor with no type specified\.$#' - identifier: missingType.parameter + message: '#^Function duplicate_file\(\) should return bool but returns null\.$#' + identifier: return.type count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addReflection\(\) has parameter \$divider with no type specified\.$#' - identifier: missingType.parameter + message: '#^Function ends_with\(\) has no return type specified\.$#' + identifier: missingType.return count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addReflection\(\) has parameter \$inside with no type specified\.$#' + message: '#^Function ends_with\(\) has parameter \$haystack with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addReflection\(\) has parameter \$reflectionHeight with no type specified\.$#' + message: '#^Function ends_with\(\) has parameter \$needle with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addReflection\(\) has parameter \$startingTransparency with no type specified\.$#' + message: '#^Function filescount\(\) has parameter \$count_hidden with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addReflection\(\) has parameter \$stretch with no type specified\.$#' + message: '#^Function fix_filename\(\) has parameter \$config with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addShadow\(\) has no return type specified\.$#' + message: '#^Function fix_path\(\) has no return type specified\.$#' identifier: missingType.return count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addShadow\(\) has parameter \$bgColor with no type specified\.$#' + message: '#^Function fix_path\(\) has parameter \$config with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addShadow\(\) has parameter \$blur with no type specified\.$#' + message: '#^Function fix_path\(\) has parameter \$path with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php + path: backend/design/js/filemanager/include/utils.php - - message: '#^Method imageLib\:\:addShadow\(\) has parameter \$shadowAngle with no type specified\.$#' + message: '#^Function folder_info\(\) has parameter \$count_hidden with no type specified\.$#' identifier: missingType.parameter count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addText\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addText\(\) has parameter \$angle with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addText\(\) has parameter \$font with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addText\(\) has parameter \$fontColor with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addText\(\) has parameter \$fontSize with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addText\(\) has parameter \$padding with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addText\(\) has parameter \$pos with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addText\(\) has parameter \$text with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addTextToCaptionBox\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addTextToCaptionBox\(\) has parameter \$angle with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addTextToCaptionBox\(\) has parameter \$font with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addTextToCaptionBox\(\) has parameter \$fontColor with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addTextToCaptionBox\(\) has parameter \$fontSize with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addTextToCaptionBox\(\) has parameter \$text with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addWatermark\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addWatermark\(\) has parameter \$opacity with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addWatermark\(\) has parameter \$padding with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addWatermark\(\) has parameter \$pos with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:addWatermark\(\) has parameter \$watermarkImage with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:blackAndWhite\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:borderPreset\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:borderPreset\(\) has parameter \$preset with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculateCaptionBoxPosition\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculateCaptionBoxPosition\(\) has parameter \$padding with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculateCaptionBoxPosition\(\) has parameter \$side with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculateCaptionBoxPosition\(\) has parameter \$thickness with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculatePosition\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculatePosition\(\) has parameter \$assetHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculatePosition\(\) has parameter \$assetWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculatePosition\(\) has parameter \$padding with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculatePosition\(\) has parameter \$pos with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:calculatePosition\(\) has parameter \$upperLeft with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:checkInterlaceImage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:checkInterlaceImage\(\) has parameter \$isEnabled with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:checkStringStartsWith\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:checkStringStartsWith\(\) has parameter \$haystack with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:checkStringStartsWith\(\) has parameter \$needle with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:createImageColor\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:createImageColor\(\) has parameter \$colorArray with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:createImageColor\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:crop\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:crop\(\) has parameter \$cropPos with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:crop\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:crop\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:crop\(\) has parameter \$optimalHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:crop\(\) has parameter \$optimalWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:cropImage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:cropImage\(\) has parameter \$cropPos with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:cropImage\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:cropImage\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:displayImage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:displayImage\(\) has parameter \$fileType with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:displayImage\(\) has parameter \$imageQuality with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:filterOpacity\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:filterOpacity\(\) has parameter \$img with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:filterOpacity\(\) has parameter \$opacity with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:findSharp\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:findSharp\(\) has parameter \$final with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:findSharp\(\) has parameter \$orig with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:findUnusedBlue\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:findUnusedBlue\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:findUnusedGreen\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:formatColor\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:formatColor\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:gd_apply_overlay\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:gd_apply_overlay\(\) has parameter \$amount with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:gd_apply_overlay\(\) has parameter \$im with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:gd_apply_overlay\(\) has parameter \$type with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:gd_filter_monopin\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:gd_filter_vintage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getCropPlacing\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getCropPlacing\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getCropPlacing\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getCropPlacing\(\) has parameter \$optimalHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getCropPlacing\(\) has parameter \$optimalWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getCropPlacing\(\) has parameter \$pos with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getDimensions\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getDimensions\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getDimensions\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getDimensions\(\) has parameter \$option with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getErrors\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getExif\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getExif\(\) has parameter \$debug with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getFileName\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getHeight\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getOptimalCrop\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getOptimalCrop\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getOptimalCrop\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getOriginalHeight\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getOriginalWidth\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByAuto\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByAuto\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByAuto\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByFixedHeight\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByFixedHeight\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByFixedHeight\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByFixedWidth\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByFixedWidth\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getSizeByFixedWidth\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getTextFont\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getTextFont\(\) has parameter \$font with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getTextSize\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getTextSize\(\) has parameter \$angle with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getTextSize\(\) has parameter \$font with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getTextSize\(\) has parameter \$fontSize with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getTextSize\(\) has parameter \$text with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:getWidth\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:greyScale\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:greyScaleDramatic\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:greyScaleEnhanced\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:hex2dec\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:hex2dec\(\) has parameter \$hex with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:image_colorize\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:image_colorize\(\) has parameter \$rgb with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:imagecreatefrompsd\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:imagecreatefrompsd\(\) has parameter \$fileName with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:initialise\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:invertTransparency\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:invertTransparency\(\) has parameter \$invert with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:invertTransparency\(\) has parameter \$originalMax with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:invertTransparency\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:iptc_maketag\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:iptc_maketag\(\) has parameter \$dat with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:iptc_maketag\(\) has parameter \$rec with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:iptc_maketag\(\) has parameter \$val with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:keepTransparancy\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:keepTransparancy\(\) has parameter \$height with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:keepTransparancy\(\) has parameter \$im with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:keepTransparancy\(\) has parameter \$width with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:negative\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:openImage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:openImage\(\) has parameter \$file with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:prepOption\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:prepOption\(\) has parameter \$option with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:reset\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resizeImage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resizeImage\(\) has parameter \$autoRotate with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resizeImage\(\) has parameter \$newHeight with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resizeImage\(\) has parameter \$newWidth with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resizeImage\(\) has parameter \$option with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resizeImage\(\) has parameter \$sharpen with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resolveExposureProgram\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resolveExposureProgram\(\) has parameter \$ep with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resolveFlash\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resolveFlash\(\) has parameter \$flash with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resolveMeteringMode\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:resolveMeteringMode\(\) has parameter \$mm with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:rotate\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:rotate\(\) has parameter \$bgColor with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:rotate\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:roundCorners\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:roundCorners\(\) has parameter \$bgColor with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:roundCorners\(\) has parameter \$radius with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:saveImage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:saveImage\(\) has parameter \$imageQuality with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:saveImage\(\) has parameter \$savePath with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:sepia\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:sepia2\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setCropFromTop\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setCropFromTop\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setFile\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setFile\(\) has parameter \$fileName with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setFillColor\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setFillColor\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setForceStretch\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setForceStretch\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setTransparency\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:setTransparency\(\) has parameter \$bool with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:sharpen\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:sharpen\(\) invoked with 1 parameter, 0 required\.$#' - identifier: arguments.count - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:sharpen2\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:sharpen2\(\) has parameter \$level with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:sharpen2\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:testColorExists\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:testColorExists\(\) has parameter \$colorArray with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:testEXIFInstalled\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:testFunct\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:testGDInstalled\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:testIsImage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:testIsImage\(\) has parameter \$image with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:transparentImage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:transparentImage\(\) has parameter \$src with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:vintage\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:writeIPTC\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:writeIPTC\(\) has parameter \$dat with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:writeIPTC\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:writeIPTCcaption\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:writeIPTCcaption\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:writeIPTCwriter\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Method imageLib\:\:writeIPTCwriter\(\) has parameter \$value with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#1 \$image of function imagecolorat expects GdImage, resource given\.$#' - identifier: argument.type - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#1 \$image of function imagecolorsforindex expects GdImage, resource given\.$#' - identifier: argument.type - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#1 \$image of function imagegif expects GdImage, resource given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#1 \$image of function imagejpeg expects GdImage, resource given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#1 \$image of function imagepng expects GdImage, resource given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#2 \$offset of function substr expects int, float given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#2 \$red of function imagecolorallocate expects int\<0, 255\>, float given\.$#' - identifier: argument.type - count: 3 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#2 \$x1 of function imagefilledrectangle expects int, float given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#3 \$dst_x of function imagecopymerge expects int, float given\.$#' - identifier: argument.type - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#3 \$green of function imagecolorallocate expects int\<0, 255\>, float given\.$#' - identifier: argument.type - count: 3 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#3 \$quality of function imagepng expects int, float given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#3 \$red of function imagecolorset expects int\<0, 255\>, float given\.$#' - identifier: argument.type - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#3 \$y1 of function imagefilledrectangle expects int, float given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#4 \$blue of function imagecolorallocate expects int\<0, 255\>, float given\.$#' - identifier: argument.type - count: 3 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#4 \$dst_y of function imagecopymerge expects int, float given\.$#' - identifier: argument.type - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#4 \$green of function imagecolorset expects int\<0, 255\>, float given\.$#' - identifier: argument.type - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#4 \$width of function imagefilledarc expects int, float given\.$#' - identifier: argument.type - count: 4 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#4 \$x2 of function imagefilledrectangle expects int, float given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#5 \$alpha of function imagecolorallocatealpha expects int\<0, 127\>, float given\.$#' - identifier: argument.type - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#5 \$blue of function imagecolorset expects int\<0, 255\>, float given\.$#' - identifier: argument.type - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#5 \$height of function imagefilledarc expects int, float given\.$#' - identifier: argument.type - count: 4 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Parameter \#5 \$y2 of function imagefilledrectangle expects int, float given\.$#' - identifier: argument.type - count: 2 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$aggresiveSharpening has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$captionBoxPositionArray has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$cropFromTopPercent has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$debug has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$errorArray has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$fileExtension has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$fileName has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$fillColorArray has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$filterOverlayPath has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$fontDir has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$forceStretch has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$height has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$heightOriginal has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$image has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$imageResized has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$imageSize has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$imageSize is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$isInterlace has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$keepTransparency has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$psdReaderPath has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$sharpenArray has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$transparentArray has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$width has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Property imageLib\:\:\$widthOriginal has no type specified\.$#' - identifier: missingType.property - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Variable \$a might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Variable \$b0 might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Variable \$degrees might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Variable \$g0 might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Variable \$optimalHeight might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Variable \$optimalWidth might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Variable \$r0 might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: backend/design/js/filemanager/include/php_image_magician.php - - - - message: '#^Argument of an invalid type string supplied for foreach, only iterables are supported\.$#' - identifier: foreach.nonIterable - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Binary operation "\*" between string and 1024 results in an error\.$#' - identifier: binaryOp.invalid - count: 2 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Call to method send\(\) on an unknown class Illuminate\\Http\\Response\.$#' - identifier: class.notFound - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Cannot use array destructuring on int\.$#' - identifier: offsetAccess.nonArray - count: 2 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function AddErrorLocation\(\) should return bool but returns string\.$#' - identifier: return.type - count: 2 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function checkRelativePathPartial\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function checkRelativePathPartial\(\) has parameter \$path with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function check_file_extension invoked with 1 parameter, 2 required\.$#' - identifier: arguments.count - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function check_file_extension\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function check_files_extensions_on_path\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function check_files_extensions_on_phar\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function check_files_extensions_on_phar\(\) has parameter \$config with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function config_loading\(\) has parameter \$current_path with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function config_loading\(\) has parameter \$fld with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function create_folder\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function create_folder\(\) has parameter \$config with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function create_folder\(\) has parameter \$ftp with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function create_img\(\) has parameter \$config with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function create_img\(\) should return bool but returns string\.$#' - identifier: return.type - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function debugger\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function deleteDir\(\) has parameter \$config with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function deleteDir\(\) has parameter \$ftp with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function deleteDir\(\) should return bool but returns null\.$#' - identifier: return.type - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function deleteFile\(\) should return null but empty return statement found\.$#' - identifier: return.empty - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function deleteFile\(\) should return null but return statement is missing\.$#' - identifier: return.missing - count: 2 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function duplicate_file\(\) has parameter \$config with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function duplicate_file\(\) has parameter \$ftp with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function duplicate_file\(\) should return bool but return statement is missing\.$#' - identifier: return.missing - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function duplicate_file\(\) should return bool but returns null\.$#' - identifier: return.type - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function ends_with\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function ends_with\(\) has parameter \$haystack with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function ends_with\(\) has parameter \$needle with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function filescount\(\) has parameter \$count_hidden with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function fix_filename\(\) has parameter \$config with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function fix_path\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function fix_path\(\) has parameter \$config with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function fix_path\(\) has parameter \$path with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php - - - - message: '#^Function folder_info\(\) has parameter \$count_hidden with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: backend/design/js/filemanager/include/utils.php + path: backend/design/js/filemanager/include/utils.php - message: '#^Function folder_info\(\) should return int but returns array\\.$#' @@ -32724,7 +30912,7 @@ parameters: - message: '#^Path in include\(\) "lang/languages\.php" is not a file or it does not exist\.$#' identifier: include.fileNotFound - count: 2 + count: 1 path: backend/design/js/filemanager/include/utils.php - diff --git a/tests/Core/LanguagesEntityRegistryTest.php b/tests/Core/LanguagesEntityRegistryTest.php index 6e941323c..668376325 100644 --- a/tests/Core/LanguagesEntityRegistryTest.php +++ b/tests/Core/LanguagesEntityRegistryTest.php @@ -72,6 +72,15 @@ public function testEntityLangInfoKeepsExistingContractShape(): void ); } + public function testLanguageAccessorsReturnFalseWhenNoLanguagesExist(): void + { + $languages = $this->languagesWithoutDatabase(); + + self::assertFalse($languages->getLangLabel()); + self::assertFalse($languages->getHrefLang()); + self::assertFalse($languages->getLangLink()); + } + private function languagesWithoutDatabase(): Languages { $reflection = new ReflectionClass(Languages::class); diff --git a/tests/Core/Modules/LicenseModulesTemplatesTest.php b/tests/Core/Modules/LicenseModulesTemplatesTest.php new file mode 100644 index 000000000..98f33f8ae --- /dev/null +++ b/tests/Core/Modules/LicenseModulesTemplatesTest.php @@ -0,0 +1,38 @@ +newInstanceWithoutConstructor(); + $method = new ReflectionMethod(LicenseModulesTemplates::class, 'getTplFiles'); + + self::assertSame([], $method->invoke($templates, '/path/that/does/not/exist')); + } + + public function testClearingRequestRetryInitializesMissingSessionState(): void + { + unset($_SESSION); + $reflection = new ReflectionClass(LicenseModulesTemplates::class); + $templates = $reflection->newInstanceWithoutConstructor(); + + $templates->clearRequestRetry(); + + self::assertSame([], $_SESSION); + } +} diff --git a/tests/Fixtures/Integration1C/doctype_internal_entity.xml b/tests/Fixtures/Integration1C/doctype_internal_entity.xml new file mode 100644 index 000000000..8f6cdc60b --- /dev/null +++ b/tests/Fixtures/Integration1C/doctype_internal_entity.xml @@ -0,0 +1,3 @@ + +]> +<Товар><Ид>&hostile;<Наименование>must not import diff --git a/tests/Fixtures/Integration1C/external_entity.xml b/tests/Fixtures/Integration1C/external_entity.xml new file mode 100644 index 000000000..632495939 --- /dev/null +++ b/tests/Fixtures/Integration1C/external_entity.xml @@ -0,0 +1,3 @@ + +]> +<Товар><Ид>&hostile;<Наименование>must not import diff --git a/tests/Fixtures/Integration1C/malformed_truncated.xml b/tests/Fixtures/Integration1C/malformed_truncated.xml new file mode 100644 index 000000000..ed3c6836a --- /dev/null +++ b/tests/Fixtures/Integration1C/malformed_truncated.xml @@ -0,0 +1,2 @@ + +<КоммерческаяИнформация><Каталог><Товары><Товар><Ид>truncated diff --git a/tests/Fixtures/Integration1C/missing_collection.xml b/tests/Fixtures/Integration1C/missing_collection.xml new file mode 100644 index 000000000..eece4ac73 --- /dev/null +++ b/tests/Fixtures/Integration1C/missing_collection.xml @@ -0,0 +1,2 @@ + +<КоммерческаяИнформация><Каталог><Товары/> diff --git a/tests/Fixtures/Integration1C/over_deep.xml b/tests/Fixtures/Integration1C/over_deep.xml new file mode 100644 index 000000000..1480023f4 --- /dev/null +++ b/tests/Fixtures/Integration1C/over_deep.xml @@ -0,0 +1,2 @@ + +<Товар><Ид>deep<Наименование>too deep diff --git a/tests/Fixtures/Integration1C/over_nodes.xml b/tests/Fixtures/Integration1C/over_nodes.xml new file mode 100644 index 000000000..2013831eb --- /dev/null +++ b/tests/Fixtures/Integration1C/over_nodes.xml @@ -0,0 +1,2 @@ + +<КоммерческаяИнформация><Каталог><Товары><Товар><Ид>nodes<Наименование>too many nodes diff --git a/tests/Fixtures/Integration1C/oversized.xml b/tests/Fixtures/Integration1C/oversized.xml new file mode 100644 index 000000000..63ae856ae --- /dev/null +++ b/tests/Fixtures/Integration1C/oversized.xml @@ -0,0 +1,2 @@ + +<КоммерческаяИнформация><Каталог><Товары><Товар><Ид>oversized<Наименование>resource budget fixture with enough bytes diff --git a/tests/Fixtures/Integration1C/unreadable.xml b/tests/Fixtures/Integration1C/unreadable.xml new file mode 100644 index 000000000..3d736151f --- /dev/null +++ b/tests/Fixtures/Integration1C/unreadable.xml @@ -0,0 +1,2 @@ + +<КоммерческаяИнформация><Каталог><Товары><Товар><Ид>unreadable<Наименование>not readable diff --git a/tests/Fixtures/Integration1C/valid_catalog.xml b/tests/Fixtures/Integration1C/valid_catalog.xml new file mode 100644 index 000000000..dc583925c --- /dev/null +++ b/tests/Fixtures/Integration1C/valid_catalog.xml @@ -0,0 +1,17 @@ + +<КоммерческаяИнформация> + <Каталог> + <Товары> + <Товар> + <Ид>product-1 + <Наименование>Valid product one + <Артикул>SKU-1 + + <Товар> + <Ид>product-2 + <Наименование>Valid product two + <Артикул>SKU-2 + + + + diff --git a/tests/Fixtures/Integration1C/valid_offers.xml b/tests/Fixtures/Integration1C/valid_offers.xml new file mode 100644 index 000000000..8888d7525 --- /dev/null +++ b/tests/Fixtures/Integration1C/valid_offers.xml @@ -0,0 +1,15 @@ + +<КоммерческаяИнформация> + <ПакетПредложений> + <Предложения> + <Предложение> + <Ид>product-1 + <Количество>2 + + <Предложение> + <Ид>product-2#variant-2 + <Количество>3 + + + + diff --git a/tests/Performance/BrandsPageFilterScopeTest.php b/tests/Performance/BrandsPageFilterScopeTest.php new file mode 100644 index 000000000..20e9873d8 --- /dev/null +++ b/tests/Performance/BrandsPageFilterScopeTest.php @@ -0,0 +1,127 @@ +root = dirname(__DIR__, 2); + } + + public function testAllBrandsPageDoesNotBuildProductFeatureFilters(): void + { + $controller = $this->read('Okay/Controllers/BrandsController.php'); + $helperMethod = $this->readMethod(BrandsHelper::class, 'assignBrandsFilterProcedure'); + + self::assertStringNotContainsString('getCatalogFeatures()', $controller); + self::assertStringNotContainsString('setFeaturesValuesFilter', $controller); + self::assertStringNotContainsString('getBaseFeaturesValues', $controller); + self::assertStringContainsString("getProductsFilter(null, '')", $controller); + self::assertStringContainsString("changeLangUrls('')", $controller); + self::assertStringContainsString("\$productsFilter['other_filter']", $controller); + self::assertStringContainsString("\$productsFilter['price']", $controller); + self::assertStringContainsString("assign('catalog_features', [])", $helperMethod); + self::assertStringContainsString('ExtenderFacade::execute', $helperMethod); + } + + public function testAllBrandsFilterProcedureAssignsEmptyStateWithoutCatalogQueries(): void + { + $entityFactory = $this->createMock(EntityFactory::class); + $catalogHelper = $this->createMock(CatalogHelper::class); + $settings = $this->createMock(Settings::class); + $filterHelper = $this->createMock(FilterHelper::class); + $design = $this->createMock(Design::class); + + $entities = [ + BrandsEntity::class => $this->createMock(BrandsEntity::class), + CategoriesEntity::class => $this->createMock(CategoriesEntity::class), + PagesEntity::class => $this->createMock(PagesEntity::class), + ]; + $entityFactory->expects(self::exactly(3)) + ->method('get') + ->willReturnCallback(static fn (string $class): object => $entities[$class]); + + $catalogHelper->expects(self::never())->method('assignCatalogDataProcedure'); + + $assigned = []; + $design->expects(self::exactly(9)) + ->method('assign') + ->willReturnCallback(static function (string $name, mixed $value) use (&$assigned): void { + $assigned[$name] = $value; + }); + + $helper = new BrandsHelper($entityFactory, $catalogHelper, $settings, $filterHelper, $design); + $helper->assignBrandsFilterProcedure(['features' => [1 => [2]]], []); + + self::assertSame([], $assigned['catalog_categories']); + self::assertSame([], $assigned['catalog_brands']); + self::assertSame([], $assigned['catalog_other_filters']); + self::assertSame([], $assigned['catalog_features']); + self::assertEquals((object) ['min' => null, 'max' => null], $assigned['catalog_prices']); + self::assertSame([], $assigned['selected_catalog_features']); + self::assertSame([], $assigned['selected_catalog_brands_ids']); + self::assertSame([], $assigned['selected_catalog_other_filters']); + self::assertSame([], $assigned['selected_catalog_prices']); + } + + public function testSpecificBrandPageKeepsProductFeatureFilters(): void + { + $controller = $this->read('Okay/Controllers/BrandController.php'); + + self::assertStringContainsString('getCatalogFeatures($brand)', $controller); + self::assertStringContainsString("setFeaturesValuesFilter(['brand_id' => \$brand->id])", $controller); + self::assertStringContainsString('assignBrandFilterProcedure', $controller); + } + + public function testDefaultThemeDoesNotOfferFiltersOnAllBrandsPage(): void + { + $template = $this->read('design/okay_shop/html/brands.tpl'); + $scripts = $this->read('design/okay_shop/html/scripts.tpl'); + + self::assertStringNotContainsString('fn_switch_mobile_filter', $template); + self::assertStringNotContainsString("include file='features.tpl'", $template); + self::assertStringNotContainsString("include file='selected_features.tpl'", $template); + self::assertStringNotContainsString("'BrandsController'", $scripts); + self::assertStringContainsString("'BrandController'", $scripts); + } + + private function read(string $path): string + { + $source = file_get_contents($this->root . '/' . $path); + + self::assertIsString($source); + return $source; + } + + private function readMethod(string $class, string $method): string + { + $reflection = new ReflectionMethod($class, $method); + $fileName = $reflection->getFileName(); + + self::assertIsString($fileName); + $source = file($fileName); + self::assertIsArray($source); + return implode('', array_slice( + $source, + $reflection->getStartLine() - 1, + $reflection->getEndLine() - $reflection->getStartLine() + 1 + )); + } +} diff --git a/tests/Scripts/BuildInstallPackageScriptTest.php b/tests/Scripts/BuildInstallPackageScriptTest.php index 04bd75b86..4ac10cad7 100644 --- a/tests/Scripts/BuildInstallPackageScriptTest.php +++ b/tests/Scripts/BuildInstallPackageScriptTest.php @@ -56,12 +56,14 @@ public function testBuildsProductionInstallArchiveWithGeneratedInstallGuide(): v self::assertContains('okaycms_9.9.9/scripts/apply-patches.php', $entries); self::assertContains('okaycms_9.9.9/INSTALL.md', $entries); - self::assertNotContains('okaycms_9.9.9/docs/workflow/install-production.md', $entries); + self::assertNotContains('okaycms_9.9.9/docs/install-production.md', $entries); self::assertNotContains('okaycms_9.9.9/dev/scripts/dev-only.php', $entries); self::assertNotContains('okaycms_9.9.9/tests/FixtureTest.php', $entries); self::assertNotContains('okaycms_9.9.9/.github/workflows/test.yml', $entries); self::assertNotContains('okaycms_9.9.9/vendor/package/file.php', $entries); self::assertNotContains('okaycms_9.9.9/README.md', $entries); + self::assertNotContains('okaycms_9.9.9/Okay/Modules/' . 'AGENTS' . '.md', $entries); + self::assertNotContains('okaycms_9.9.9/backend/' . 'CLAUDE' . '.md', $entries); $zip = new ZipArchive(); self::assertTrue($zip->open($zipPath)); @@ -124,11 +126,13 @@ private function createFixtureRepository(): void $this->writeFixtureFile('backend/index.php', 'writeFixtureFile('config/config.php', 'db'); $this->writeFixtureFile('scripts/apply-patches.php', 'writeFixtureFile('docs/workflow/install-production.md', '# Fixture installation guide'); + $this->writeFixtureFile('docs/install-production.md', '# Fixture installation guide'); $this->writeFixtureFile('dev/scripts/dev-only.php', 'writeFixtureFile('tests/FixtureTest.php', 'writeFixtureFile('.github/workflows/test.yml', 'name: test'); $this->writeFixtureFile('vendor/package/file.php', 'writeFixtureFile('Okay/Modules/' . 'AGENTS' . '.md', 'private agent policy'); + $this->writeFixtureFile('backend/' . 'CLAUDE' . '.md', 'private agent policy'); $this->runGit(['init']); $this->runGit(['add', '.']); diff --git a/tests/Security/AdminAjaxSecurityContractTest.php b/tests/Security/AdminAjaxSecurityContractTest.php new file mode 100644 index 000000000..9f0430cbd --- /dev/null +++ b/tests/Security/AdminAjaxSecurityContractTest.php @@ -0,0 +1,122 @@ +root = dirname(__DIR__, 2); + } + + public function testThemeEditorResponseHasStableJsonShapeAndHttpPolicy(): void + { + self::assertSame( + [ + 'success' => true, + 'error' => null, + 'code' => 'saved', + 'message' => 'Saved', + ], + ThemeEditorResponse::payload(true, 'saved', 'Saved') + ); + self::assertSame(200, ThemeEditorResponse::statusCode(true, 'saved')); + + $failure = ThemeEditorResponse::payload(false, 'theme_locked', 'Locked'); + self::assertFalse($failure['success']); + self::assertSame( + ['code' => 'theme_locked', 'message' => 'Locked'], + $failure['error'] + ); + self::assertSame(423, ThemeEditorResponse::statusCode(false, 'theme_locked')); + self::assertSame(403, ThemeEditorResponse::statusCode(false, 'csrf_invalid')); + self::assertSame(415, ThemeEditorResponse::statusCode(false, 'invalid_extension')); + self::assertSame(400, ThemeEditorResponse::statusCode(false, 'invalid_path')); + self::assertSame(500, ThemeEditorResponse::statusCode(false, 'write_failed')); + } + + public function testLessonCompletionUsesPostCsrfAndSharedFailureState(): void + { + $endpoint = $this->read('backend/ajax/lesson_done.php'); + $hints = $this->read('backend/design/html/learning_hints.tpl'); + + self::assertStringContainsString('$request->method(\'post\')', $endpoint); + self::assertStringContainsString('$request->checkSession()', $endpoint); + self::assertStringContainsString('$request->post(\'lesson\', \'integer\')', $endpoint); + self::assertStringContainsString('$lessonsEntity->update($lessonId', $endpoint); + self::assertStringNotContainsString('$request->get(\'lesson\')', $endpoint); + + self::assertSame(12, preg_match_all('/completeLearningLesson\(\d+,/', $hints)); + self::assertSame(1, substr_count($hints, "type: 'POST'")); + self::assertSame(1, substr_count($hints, "session_id: '{\$smarty.session.id|escape:'javascript'}'")); + self::assertStringContainsString('learningCompletionPending', $hints); + self::assertStringContainsString('response.success !== true', $hints); + self::assertStringContainsString('.fail(function()', $hints); + self::assertStringContainsString('learningCompletionRetry', $hints); + } + + public function testThemeEditorsShareFailureAndRevisionContract(): void + { + foreach (['save_script.php', 'save_style.php', 'save_template.php'] as $file) { + $source = $this->read('backend/ajax/' . $file); + + self::assertStringContainsString('ThemeEditorResponse::send', $source, $file); + self::assertStringContainsString('$request->method(\'post\')', $source, $file); + self::assertStringContainsString('$request->checkSession()', $source, $file); + self::assertStringContainsString('realpath($file)', $source, $file); + self::assertStringContainsString('LOCK_EX', $source, $file); + } + + $response = $this->read('Okay/Core/Security/ThemeEditorResponse.php'); + self::assertStringContainsString('self::payload', $response); + self::assertStringContainsString('self::statusCode', $response); + + foreach (['scripts.tpl', 'styles.tpl', 'templates.tpl'] as $file) { + $source = $this->read('backend/design/html/' . $file); + + self::assertStringContainsString('data-editor-enabled=', $source, $file); + self::assertStringContainsString('aria-live="polite"', $source, $file); + self::assertStringContainsString('aria-live="assertive"', $source, $file); + self::assertStringContainsString("type: 'POST'", $source, $file); + self::assertStringContainsString('pendingRevision', $source, $file); + self::assertStringContainsString('response.success !== true', $source, $file); + self::assertStringContainsString('editor.setOption(\'readOnly\', true)', $source, $file); + } + } + + public function testThemeEditorMessagesExistInAllBackendLanguages(): void + { + foreach (['en.php', 'ru.php', 'ua.php'] as $file) { + $source = $this->read('backend/lang/' . $file); + + foreach ( + [ + 'theme_editor_disabled', + 'theme_editor_csrf_invalid', + 'theme_editor_invalid_path', + 'theme_editor_locked', + 'theme_editor_write_failed', + 'theme_editor_saved', + 'theme_editor_retry', + ] as $key + ) { + self::assertStringContainsString("['$key']", $source, $file . ':' . $key); + } + } + } + + private function read(string $relativePath): string + { + $path = $this->root . '/' . $relativePath; + self::assertFileExists($path); + + return (string) file_get_contents($path); + } +} diff --git a/tests/Security/AdminCsrfGuardTest.php b/tests/Security/AdminCsrfGuardTest.php index 46094af0c..d1b2a1170 100644 --- a/tests/Security/AdminCsrfGuardTest.php +++ b/tests/Security/AdminCsrfGuardTest.php @@ -5,12 +5,14 @@ namespace Security; use Okay\Core\Request; +use Okay\Core\Security\AdminCsrfToken; use PHPUnit\Framework\TestCase; final class AdminCsrfGuardTest extends TestCase { private array $server; private array $post; + private string $token; protected function setUp(): void { @@ -23,8 +25,11 @@ protected function setUp(): void } session_id('csrfguardtest'); session_start(); + + $this->token = AdminCsrfToken::get(); + $_SERVER['REQUEST_METHOD'] = 'POST'; - $_SERVER['HTTP_X_OKAY_SESSION_ID'] = session_id(); + $_SERVER['HTTP_X_OKAY_SESSION_ID'] = $this->token; $_POST = []; } @@ -39,13 +44,24 @@ protected function tearDown(): void parent::tearDown(); } - public function testUnsafeRequestAcceptsExplicitHeaderToken(): void + public function testUnsafeRequestAcceptsValidCsrfToken(): void { $request = new Request(); self::assertTrue($request->checkSession()); } + public function testUnsafeRequestRejectsRawSessionIdAsToken(): void + { + $_SERVER['HTTP_X_OKAY_SESSION_ID'] = session_id(); + $_POST = ['name' => 'changed']; + + $request = new Request(); + + self::assertFalse($request->checkSession()); + self::assertSame([], $_POST); + } + public function testUnsafeRequestRejectsMissingTokenBeforeMutation(): void { unset($_SERVER['HTTP_X_OKAY_SESSION_ID']); diff --git a/tests/Security/AdminCsrfTokenTest.php b/tests/Security/AdminCsrfTokenTest.php new file mode 100644 index 000000000..cb538ad8d --- /dev/null +++ b/tests/Security/AdminCsrfTokenTest.php @@ -0,0 +1,54 @@ +checkPassword($pass, $manager->password);'); + $passwordCheck = strpos($authAdmin, '$passwordIsValid = $managers->checkPassword('); $limitBlock = strpos($authAdmin, "} elseif (\$manager->cnt_try > \$limit) {"); self::assertIsInt($passwordCheck); diff --git a/tests/Security/AdminLoginThrottleConcurrencyTest.php b/tests/Security/AdminLoginThrottleConcurrencyTest.php new file mode 100644 index 000000000..ee754e0f1 --- /dev/null +++ b/tests/Security/AdminLoginThrottleConcurrencyTest.php @@ -0,0 +1,133 @@ +tmpDir = sys_get_temp_dir() . '/okay_admin_login_throttle_' . bin2hex(random_bytes(4)); + self::assertTrue(mkdir($this->tmpDir, 0775, true)); + } + + protected function tearDown(): void + { + foreach (glob($this->tmpDir . '/*') ?: [] as $path) { + if (is_dir($path)) { + @rmdir($path); + continue; + } + + @unlink($path); + } + @rmdir($this->tmpDir); + } + + public function testReservationTracksAccountAndClientSeparately(): void + { + $throttle = new AdminLoginThrottle($this->tmpDir, 3, 900); + + self::assertTrue($throttle->reserve('Admin', '203.0.113.10')); + + $stateFiles = array_values(array_filter( + glob($this->tmpDir . '/login_*.json') ?: [], + static fn (string $path): bool => str_contains(basename($path), 'login_account_') + || str_contains(basename($path), 'login_client_') + )); + self::assertCount(2, $stateFiles); + } + + public function testTemporaryFileAcceptsEquivalentCanonicalDirectory(): void + { + $filesystem = new AdminLoginThrottleFilesystem(); + + $path = $filesystem->temporaryFile($this->tmpDir); + + self::assertIsString($path); + self::assertSame(realpath($this->tmpDir), realpath(dirname($path))); + self::assertTrue(unlink($path)); + } + + public function testTwentySeparateProcessesDoNotLoseReservations(): void + { + $barrier = $this->tmpDir . '/start'; + $root = dirname(__DIR__, 2); + $worker = <<<'PHP' +require $argv[3] . '/vendor/autoload.php'; +$ready = $argv[1]; +$storage = $argv[2]; +while (!is_file($ready)) { + usleep(1000); +} +$throttle = new \Okay\Core\Security\AdminLoginThrottle($storage, 100, 900, 0, 100); +exit($throttle->reserve('same-account', '203.0.113.10') ? 0 : 1); +PHP; + + $processes = []; + for ($i = 0; $i < 20; $i++) { + $process = new Process([PHP_BINARY, '-r', $worker, $barrier, $this->tmpDir, $root]); + $process->setTimeout(10); + $process->start(); + $processes[] = $process; + } + + self::assertTrue(touch($barrier)); + foreach ($processes as $process) { + $process->wait(); + self::assertTrue( + $process->isSuccessful(), + $process->getErrorOutput() . $process->getOutput() + ); + } + + $accountState = json_decode( + (string) file_get_contents($this->tmpDir . '/login_account_' . hash('sha256', 'account:same-account') . '.json'), + true + ); + $clientState = json_decode( + (string) file_get_contents($this->tmpDir . '/login_client_' . hash('sha256', 'client:203.0.113.10') . '.json'), + true + ); + + self::assertIsArray($accountState); + self::assertIsArray($clientState); + self::assertSame(20, $accountState['count'] ?? null); + self::assertSame(20, $clientState['count'] ?? null); + } + + public function testCorruptStateFailsClosedAndIsNotReset(): void + { + $throttle = new AdminLoginThrottle($this->tmpDir, 3, 900); + self::assertTrue($throttle->reserve('same-account', '203.0.113.10')); + + $accountPath = $this->tmpDir . '/login_account_' . hash('sha256', 'account:same-account') . '.json'; + self::assertTrue(file_put_contents($accountPath, '{broken') !== false); + + $secondAttempt = new AdminLoginThrottle($this->tmpDir, 3, 900); + + self::assertFalse($secondAttempt->reserve('same-account', '203.0.113.10')); + self::assertSame('{broken', file_get_contents($accountPath)); + } + + public function testWriteFailureFailsClosed(): void + { + $filesystem = new class extends AdminLoginThrottleFilesystem { + public function write(mixed $handle, string $contents): int|false + { + return false; + } + }; + $throttle = new AdminLoginThrottle($this->tmpDir, 3, 900, 60, 50, $filesystem); + + self::assertFalse($throttle->reserve('same-account', '203.0.113.10')); + } +} diff --git a/tests/Security/AdminPasswordRecoveryTest.php b/tests/Security/AdminPasswordRecoveryTest.php index 500827b3f..e2d7d165c 100644 --- a/tests/Security/AdminPasswordRecoveryTest.php +++ b/tests/Security/AdminPasswordRecoveryTest.php @@ -55,10 +55,8 @@ public function testRecoveryAndLoginRedirectsDoNotAppendBackendToCurrentRecovery "\$response->redirectTo(\$this->request->getRootUrl() . '/backend/index.php');", $source ); - self::assertStringContainsString( - "\$_SESSION['before_auth_url'] : \$this->request->getRootUrl() . '/backend/index.php'", - $source - ); + self::assertStringContainsString('SafeRedirect::sameOriginUrl', $source); + self::assertStringContainsString("\$_SESSION['before_auth_url']", $source); self::assertStringNotContainsString( "\$this->request->getBasePathWithDomain() . '/backend/index.php'", $source diff --git a/tests/Security/AdminSecurityHelpersTest.php b/tests/Security/AdminSecurityHelpersTest.php new file mode 100644 index 000000000..7fcccd072 --- /dev/null +++ b/tests/Security/AdminSecurityHelpersTest.php @@ -0,0 +1,290 @@ +tmpDir = sys_get_temp_dir() . '/okay_admin_throttle_' . bin2hex(random_bytes(4)); + mkdir($this->tmpDir, 0775, true); + } + + protected function tearDown(): void + { + foreach (glob($this->tmpDir . '/*') ?: [] as $file) { + @unlink($file); + } + @rmdir($this->tmpDir); + } + + public function testThrottleAllowsThenBlocksAfterMaxAttempts(): void + { + $throttle = new AdminLoginThrottle($this->tmpDir, 3, 900); + + self::assertTrue($throttle->isAllowed('1.2.3.4')); + $throttle->registerFailure('1.2.3.4'); + $throttle->registerFailure('1.2.3.4'); + self::assertTrue($throttle->isAllowed('1.2.3.4')); + $throttle->registerFailure('1.2.3.4'); + self::assertFalse($throttle->isAllowed('1.2.3.4')); + + $throttle->clear('1.2.3.4'); + self::assertTrue($throttle->isAllowed('1.2.3.4')); + } + + public function testThrottleFailsClosedWhenStoreNotWritable(): void + { + $previous = ini_get('error_log'); + ini_set('error_log', '/dev/null'); + try { + $blocked = $this->tmpDir . '/not-a-directory'; + file_put_contents($blocked, 'x'); + + $throttle = new AdminLoginThrottle($blocked, 10, 900); + self::assertFalse($throttle->isAllowed('9.9.9.9')); + } finally { + ini_set('error_log', $previous === false ? '' : $previous); + } + } + + public function testSafeRedirectRejectsTraversalAndBackslash(): void + { + self::assertSame('/', SafeRedirect::sameOriginPath('//evil.example')); + self::assertSame('/', SafeRedirect::sameOriginPath('/\\evil')); + self::assertSame('/', SafeRedirect::sameOriginPath('/%2e%2e/etc/passwd')); + self::assertSame('/backend/index.php', SafeRedirect::sameOriginPath('/backend/index.php')); + self::assertSame( + '/backend/index.php?controller=ProductsAdmin', + SafeRedirect::sameOriginPath('/backend/index.php?controller=ProductsAdmin') + ); + } + + public function testSafeRedirectSameOriginUrlRejectsForeignHost(): void + { + $root = 'https://shop.example'; + $fallback = $root . '/backend/index.php'; + + self::assertSame( + $fallback, + SafeRedirect::sameOriginUrl('https://evil.example/backend/index.php', $root, $fallback) + ); + self::assertSame( + $root . '/backend/index.php?controller=OrdersAdmin', + SafeRedirect::sameOriginUrl( + 'https://shop.example/backend/index.php?controller=OrdersAdmin', + $root, + $fallback + ) + ); + } + + public function testTrustedClientIpIgnoresForwardedHeaderWithoutTrustedProxy(): void + { + $server = [ + 'REMOTE_ADDR' => '203.0.113.10', + 'HTTP_X_FORWARDED_FOR' => '198.51.100.7', + ]; + + self::assertSame( + '203.0.113.10', + TrustedClientIp::resolve($server, 'X-Forwarded-For', null) + ); + self::assertSame( + '203.0.113.10', + TrustedClientIp::resolve($server, 'X-Forwarded-For', '10.0.0.1') + ); + self::assertSame( + '198.51.100.7', + TrustedClientIp::resolve($server, 'X-Forwarded-For', '203.0.113.10') + ); + self::assertSame( + '198.51.100.7', + TrustedClientIp::resolve($server, 'X-Forwarded-For', '203.0.113.0/24') + ); + } + + #[DataProvider('trustedRequestContextProvider')] + /** @param array $server */ + public function testTrustedRequestContextResolvesOneTrustedClientAndScheme( + array $server, + ?string $trustedProxyList, + string $expectedClientIp, + string $expectedProtocol + ): void { + $context = TrustedRequestContext::fromServer($server, $trustedProxyList); + + self::assertSame($expectedClientIp, $context->getClientIp()); + self::assertSame($expectedProtocol, $context->getProtocol()); + self::assertSame($expectedProtocol === 'https', $context->isSecure()); + } + + /** + * @return iterable, ?string, string, string}> + */ + public static function trustedRequestContextProvider(): iterable + { + yield 'direct http' => [ + ['REMOTE_ADDR' => '203.0.113.10', 'SERVER_PORT' => 80], + null, + '203.0.113.10', + 'http', + ]; + yield 'direct https' => [ + ['REMOTE_ADDR' => '203.0.113.10', 'HTTPS' => 'on'], + null, + '203.0.113.10', + 'https', + ]; + yield 'untrusted forwarded headers are ignored' => [ + [ + 'REMOTE_ADDR' => '203.0.113.10', + 'HTTP_X_FORWARDED_FOR' => '198.51.100.7', + 'HTTP_X_FORWARDED_PROTO' => 'https', + ], + '10.0.0.1', + '203.0.113.10', + 'http', + ]; + yield 'trusted ipv4 chain is resolved right to left' => [ + [ + 'REMOTE_ADDR' => '10.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '198.51.100.7, 10.0.0.2, 10.0.0.3', + 'HTTP_X_FORWARDED_PROTO' => 'https', + ], + '10.0.0.0/8', + '198.51.100.7', + 'https', + ]; + yield 'trusted ipv6 chain selects first untrusted hop' => [ + [ + 'REMOTE_ADDR' => '2001:db8::10', + 'HTTP_X_FORWARDED_FOR' => '2001:db8:1::20, 2001:db8::1', + ], + '2001:db8::/64', + '2001:db8:1::20', + 'http', + ]; + yield 'malformed chain falls back to direct peer' => [ + [ + 'REMOTE_ADDR' => '10.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '198.51.100.7, not-an-ip', + 'HTTP_X_FORWARDED_PROTO' => 'https', + ], + '10.0.0.1', + '10.0.0.1', + 'https', + ]; + yield 'all trusted chain falls back to direct peer' => [ + [ + 'REMOTE_ADDR' => '10.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '10.0.0.2, 10.0.0.3', + ], + '10.0.0.0/8', + '10.0.0.1', + 'http', + ]; + } + + public function testSafeRedirectComparesSchemeAndEffectivePort(): void + { + $root = 'https://shop.example/subdir'; + $fallback = $root . '/backend/index.php'; + + self::assertSame( + 'https://shop.example:443/subdir/orders?tab=history#top', + SafeRedirect::sameOriginUrl( + 'HTTPS://SHOP.EXAMPLE:443/subdir/orders?tab=history#top', + $root, + $fallback + ) + ); + self::assertSame( + $fallback, + SafeRedirect::sameOriginUrl('https://shop.example:8443/subdir/orders', $root, $fallback) + ); + self::assertSame( + $fallback, + SafeRedirect::sameOriginUrl('http://shop.example/subdir/orders', $root, $fallback) + ); + } + + public function testSafeRedirectPreservesSafeRelativeSubdirectoryQueryAndFragment(): void + { + $root = 'https://shop.example/subdir'; + $fallback = $root . '/backend/index.php'; + + self::assertSame( + 'https://shop.example/subdir/catalog?sort=price#items', + SafeRedirect::sameOriginUrl('catalog?sort=price#items', $root, $fallback) + ); + self::assertSame( + '/subdir/catalog?sort=price#items', + SafeRedirect::sameOriginPath('/subdir/catalog?sort=price#items') + ); + } + + public function testRequestAndAdminSessionConsumeTheSameTrustedTransportDecision(): void + { + $context = TrustedRequestContext::fromServer( + [ + 'REMOTE_ADDR' => '10.0.0.1', + 'HTTP_X_FORWARDED_FOR' => '198.51.100.7', + 'HTTP_X_FORWARDED_PROTO' => 'https', + ], + '10.0.0.1' + ); + $previousContext = Request::getTrustedRequestContext(); + $previousCookieParams = session_get_cookie_params(); + + Request::setTrustedRequestContext($context); + try { + $request = new Request(); + self::assertSame('https', Request::getProtocol()); + self::assertSame('198.51.100.7', $request->getRemoteAddr()); + + AdminSession::configureCookieParams([ + 'REMOTE_ADDR' => '10.0.0.1', + 'HTTP_X_FORWARDED_PROTO' => 'http', + ]); + self::assertTrue(session_get_cookie_params()['secure']); + } finally { + session_set_cookie_params($previousCookieParams); + Request::setTrustedRequestContext($previousContext); + } + } + + #[DataProvider('unsafeRedirectProvider')] + public function testSafeRedirectRejectsAmbiguousAuthorityAndTraversal(string $url): void + { + $fallback = 'https://shop.example/subdir/backend/index.php'; + + self::assertSame($fallback, SafeRedirect::sameOriginUrl($url, 'https://shop.example/subdir', $fallback)); + } + + /** + * @return iterable + */ + public static function unsafeRedirectProvider(): iterable + { + yield 'userinfo' => ['https://user:pass@shop.example/subdir/orders']; + yield 'malformed port' => ['https://shop.example:invalid/subdir/orders']; + yield 'backslash' => ['https://shop.example\\@evil.example/subdir/orders']; + yield 'protocol relative' => ['//shop.example/subdir/orders']; + yield 'encoded traversal' => ['https://shop.example/subdir/%2e%2e/admin']; + yield 'control character' => ["https://shop.example/subdir/orders\nnext"]; + } +} diff --git a/tests/Security/CheckoutTokenTest.php b/tests/Security/CheckoutTokenTest.php index 8ac6120ac..4dff34088 100644 --- a/tests/Security/CheckoutTokenTest.php +++ b/tests/Security/CheckoutTokenTest.php @@ -39,4 +39,27 @@ public function testCheckoutFingerprintFallbackRejectsImmediateDuplicate(): void self::assertTrue(CheckoutToken::consumeFingerprint($fingerprint)); self::assertFalse(CheckoutToken::consumeFingerprint($fingerprint)); } + + public function testTokenSubmissionCannotBeReplayedThroughFingerprintFallback(): void + { + $token = CheckoutToken::get(); + $fingerprint = hash('sha256', 'same checkout payload'); + + self::assertTrue(CheckoutToken::consumeSubmission($token, $fingerprint)); + self::assertFalse(CheckoutToken::consumeSubmission(null, $fingerprint)); + } + + public function testFingerprintFallbackCannotBeReplayedWithAValidToken(): void + { + $token = CheckoutToken::get(); + $fingerprint = hash('sha256', 'same checkout payload'); + + self::assertTrue(CheckoutToken::consumeSubmission(null, $fingerprint)); + self::assertFalse(CheckoutToken::consumeSubmission($token, $fingerprint)); + } + + public function testSubmissionRejectsMalformedNonScalarToken(): void + { + self::assertFalse(CheckoutToken::consumeSubmission(['unexpected'], hash('sha256', 'payload'))); + } } diff --git a/tests/Security/CoreSecauditB1CommentSecurityTest.php b/tests/Security/CoreSecauditB1CommentSecurityTest.php new file mode 100644 index 000000000..2acbba3b5 --- /dev/null +++ b/tests/Security/CoreSecauditB1CommentSecurityTest.php @@ -0,0 +1,106 @@ +root = dirname(__DIR__, 2); + } + + public function testCommentTextareasEscapeRequestDataText(): void + { + foreach (['product.tpl', 'post.tpl'] as $template) { + $source = $this->read('design/okay_shop/html/' . $template); + + self::assertStringContainsString( + '{$request_data.text|escape}', + $source, + $template . ' must escape comment text on re-render' + ); + self::assertDoesNotMatchRegularExpression( + '/\{\$request_data\.text\}(?!\|)/', + $source, + $template . ' must not echo raw request_data.text' + ); + } + } + + public function testCommentFormsExposeCustomerCsrfToken(): void + { + foreach ( + [ + 'product.tpl' => 'id="captcha_id"', + 'post.tpl' => 'id="fn_blog_comment"', + ] as $template => $formMarker + ) { + $source = $this->read('design/okay_shop/html/' . $template); + $formPos = strpos($source, $formMarker); + self::assertIsInt($formPos, $template); + + $formEnd = strpos($source, '', $formPos); + self::assertIsInt($formEnd, $template); + $form = substr($source, $formPos, $formEnd - $formPos); + + self::assertStringContainsString( + 'name="customer_csrf_token"', + $form, + $template . ' comment form must include customer_csrf_token' + ); + self::assertStringContainsString( + '{$customer_csrf_token|escape}', + $form, + $template . ' comment form must escape customer_csrf_token' + ); + self::assertStringContainsString('{$request_data.text|escape}', $form, $template); + } + } + + public function testCommentValidationPathChecksCustomerCsrf(): void + { + $commentsHelper = $this->read('Okay/Helpers/CommentsHelper.php'); + $validateHelper = $this->read('Okay/Helpers/ValidateHelper.php'); + + $usesHelperCsrf = str_contains($commentsHelper, 'getCustomerCsrfError'); + $usesValidateCsrf = str_contains($validateHelper, 'getCommentValidateError') + && ( + str_contains($validateHelper, "getCustomerCsrfError") + || preg_match( + '/function getCommentValidateError.*?CustomerCsrfToken::check/s', + $validateHelper + ) === 1 + ); + + self::assertTrue( + $usesHelperCsrf || $usesValidateCsrf, + 'Comment path must call getCustomerCsrfError (or equivalent) before insert' + ); + + if ($usesHelperCsrf) { + $csrfPos = strpos($commentsHelper, 'getCustomerCsrfError'); + $addPos = strpos($commentsHelper, '->add($comment)'); + self::assertIsInt($csrfPos); + self::assertIsInt($addPos); + self::assertLessThan($addPos, $csrfPos); + } + } + + private function read(string $relativePath): string + { + $path = $this->root . '/' . $relativePath; + self::assertFileExists($path); + + return (string) file_get_contents($path); + } +} diff --git a/tests/Security/CoreSecauditB2PaymentIntegrityTest.php b/tests/Security/CoreSecauditB2PaymentIntegrityTest.php new file mode 100644 index 000000000..806d97a9f --- /dev/null +++ b/tests/Security/CoreSecauditB2PaymentIntegrityTest.php @@ -0,0 +1,140 @@ +root = dirname(__DIR__, 2); + } + + public function testWayForPayRequiresMerchantSignatureBeforePaid(): void + { + $source = $this->read('Okay/Modules/OkayCMS/WayForPay/Controllers/CallbackController.php'); + $helper = $this->read('Okay/Modules/OkayCMS/WayForPay/Helpers/CallbackPayload.php'); + + self::assertStringNotContainsString( + "if (!empty(\$data->merchantSignature) && \$data->merchantSignature != \$sign)", + $source, + 'Must not accept empty merchantSignature via soft gate' + ); + self::assertStringContainsString('CallbackPayload::verifySignature', $source); + self::assertStringContainsString('hash_equals', $helper); + self::assertStringContainsString("merchantSignature === ''", $helper); + + $sigCheckPos = strpos($source, 'CallbackPayload::verifySignature'); + $paidPos = strpos($source, 'markPaidIfUnpaid'); + self::assertIsInt($sigCheckPos); + self::assertIsInt($paidPos); + self::assertLessThan($paidPos, $sigCheckPos); + } + + public function testWayForPayDoesNotRawUnserializeModuleSettings(): void + { + $source = $this->read('Okay/Modules/OkayCMS/WayForPay/Controllers/CallbackController.php'); + + self::assertStringNotContainsString( + 'unserialize($method->settings)', + $source, + 'Must not raw-unserialize payment method settings' + ); + self::assertTrue( + str_contains($source, 'json_decode') + || str_contains($source, 'allowed_classes') + || str_contains($source, 'getPaymentSettings') + || str_contains($source, '->settings'), + 'Settings must use JSON / guarded decode / settings API' + ); + } + + public function testRozetkaPayEnablesTlsPeerVerification(): void + { + $source = $this->read('Okay/Modules/OkayCMS/RozetkaPay/Models/Gateway/Client/HttpCurl.php'); + + self::assertStringNotContainsString('CURLOPT_SSL_VERIFYPEER => 0', $source); + self::assertMatchesRegularExpression( + '/CURLOPT_SSL_VERIFYPEER\s*=>\s*true/', + $source + ); + self::assertMatchesRegularExpression( + '/CURLOPT_SSL_VERIFYHOST\s*=>\s*2/', + $source + ); + } + + public function testPaymentCallbacksPreferStrictMoneyCompare(): void + { + foreach ( + [ + 'Okay/Modules/OkayCMS/WayForPay/Controllers/CallbackController.php', + 'Okay/Modules/OkayCMS/RozetkaPay/Controllers/CallbackController.php', + ] as $path + ) { + $source = $this->read($path); + self::assertStringNotContainsString( + '$orderAmount != $w4pAmount', + $source, + $path . ' must not use loose amount !=' + ); + self::assertStringNotContainsString('bccomp', $source, $path); + self::assertStringContainsString('DecimalAmount::toMinorUnits', $source, $path); + } + } + + public function testRozetkaPayCallbackKeepsSecondaryIdAndAmountChecks(): void + { + $source = $this->read('Okay/Modules/OkayCMS/RozetkaPay/Controllers/CallbackController.php'); + + self::assertStringContainsString('$createDetails->id', $source); + self::assertStringContainsString('$data->id', $source); + self::assertStringContainsString('CallbackSecurity::verifySignature', $source); + self::assertStringContainsString('CallbackSecurity::amountLexeme', $source); + self::assertStringContainsString('payment_details', $source); + } + + public function testPaymentTransitionIsConditionalAndAuthenticationPrecedesOrderLookup(): void + { + $orders = $this->read('Okay/Entities/OrdersEntity.php'); + self::assertStringContainsString("where('paid=0')", $orders); + self::assertStringContainsString('markPaidIfUnpaid', $orders); + + foreach ( + [ + [ + 'Okay/Modules/OkayCMS/WayForPay/Controllers/CallbackController.php', + 'CallbackPayload::verifySignature', + ], + [ + 'Okay/Modules/OkayCMS/RozetkaPay/Controllers/CallbackController.php', + 'CallbackSecurity::verifySignature', + ], + ] as [$path, $authCall] + ) { + $source = $this->read($path); + $authPos = strpos($source, $authCall); + $orderLookupPos = strpos($source, '$ordersEntity->get'); + self::assertIsInt($authPos, $path); + self::assertIsInt($orderLookupPos, $path); + self::assertLessThan($orderLookupPos, $authPos, $path); + } + } + + private function read(string $relativePath): string + { + $path = $this->root . '/' . $relativePath; + self::assertFileExists($path); + + return (string) file_get_contents($path); + } +} diff --git a/tests/Security/CoreSecauditB3PublicCsrfTest.php b/tests/Security/CoreSecauditB3PublicCsrfTest.php new file mode 100644 index 000000000..534e3129f --- /dev/null +++ b/tests/Security/CoreSecauditB3PublicCsrfTest.php @@ -0,0 +1,94 @@ +root = dirname(__DIR__, 2); + } + + public function testOrderPaymentSwitchRequiresCustomerCsrfAndAllowList(): void + { + $source = $this->read('Okay/Controllers/OrderController.php'); + + self::assertStringContainsString('getCustomerCsrfError', $source); + self::assertStringContainsString("post('customer_csrf_token')", $source); + self::assertStringContainsString('getOrderPaymentMethodsList', $source); + + $csrfPos = strpos($source, 'getCustomerCsrfError'); + $updatePos = strpos($source, "update(\$order->id, ['payment_method_id'"); + self::assertIsInt($csrfPos); + self::assertIsInt($updatePos); + self::assertLessThan($updatePos, $csrfPos); + } + + public function testOrderThemePaymentFormExposesCustomerCsrfToken(): void + { + $source = $this->read('design/okay_shop/html/order.tpl'); + + self::assertStringContainsString('name="customer_csrf_token"', $source); + self::assertStringContainsString('{$customer_csrf_token|escape}', $source); + self::assertStringContainsString('name="payment_method_id"', $source); + self::assertStringContainsString("name='reset_payment_method'", $source); + + foreach (['payment_method_id', 'reset_payment_method'] as $field) { + $pos = strpos($source, $field === 'reset_payment_method' ? "name='reset_payment_method'" : 'name="payment_method_id"'); + self::assertIsInt($pos, $field); + $formStart = strrpos(substr($source, 0, $pos), '', $pos); + self::assertIsInt($formEnd, $field); + $form = substr($source, $formStart, $formEnd - $formStart); + self::assertStringContainsString( + 'name="customer_csrf_token"', + $form, + 'order.tpl form with ' . $field . ' must include CSRF token' + ); + } + } + + public function testFastOrderCreateRequiresCustomerCsrf(): void + { + $controller = $this->read( + 'Okay/Modules/OkayCMS/FastOrder/Controllers/FastOrderController.php' + ); + $form = $this->read('Okay/Modules/OkayCMS/FastOrder/design/html/fast_order_form.tpl'); + $js = $this->read('Okay/Modules/OkayCMS/FastOrder/design/js/fast_order.js'); + + self::assertStringContainsString('getCustomerCsrfError', $controller); + self::assertStringContainsString("post('customer_csrf_token')", $controller); + + $csrfPos = strpos($controller, 'getCustomerCsrfError'); + $addPos = strpos($controller, '->add($preparedOrder)'); + self::assertIsInt($csrfPos); + self::assertIsInt($addPos); + self::assertLessThan($addPos, $csrfPos); + + self::assertStringContainsString('name="customer_csrf_token"', $form); + self::assertTrue( + str_contains($js, 'customer_csrf_token') + || str_contains($js, 'withCustomerCsrfToken') + || str_contains($form, 'customer_csrf_token'), + 'Fast order AJAX/form must send customer_csrf_token' + ); + } + + private function read(string $relativePath): string + { + $path = $this->root . '/' . $relativePath; + self::assertFileExists($path); + + return (string) file_get_contents($path); + } +} diff --git a/tests/Security/CoreSecauditB4SessionHeadersTest.php b/tests/Security/CoreSecauditB4SessionHeadersTest.php new file mode 100644 index 000000000..ecf95a205 --- /dev/null +++ b/tests/Security/CoreSecauditB4SessionHeadersTest.php @@ -0,0 +1,97 @@ +root = dirname(__DIR__, 2); + } + + public function testFrontendSessionCookieParamsAreHardened(): void + { + $source = $this->read('index.php'); + + self::assertStringContainsString("session_name('okay_sid');", $source); + self::assertStringContainsString('TrustedRequestContext::fromServer', $source); + self::assertStringContainsString('Request::setTrustedRequestContext', $source); + self::assertStringContainsString('session_set_cookie_params', $source); + self::assertStringContainsString("'httponly' => true", $source); + self::assertStringContainsString("'samesite' => 'Lax'", $source); + self::assertStringContainsString("'secure' => \$trustedRequestContext->isSecure()", $source); + + $paramsPos = strpos($source, 'session_set_cookie_params'); + $startPos = strpos($source, 'session_start('); + self::assertIsInt($paramsPos); + self::assertIsInt($startPos); + self::assertLessThan($startPos, $paramsPos); + } + + public function testAffectedAdminBootstrapsResolveContextBeforeSessionStart(): void + { + foreach (['backend/index.php', 'backend/ajax/configure.php', 'backend/files/index.php'] as $path) { + $source = $this->read($path); + $contextPos = strpos($source, 'TrustedRequestContext::fromServer'); + $sessionPos = strpos($source, 'session_start('); + + self::assertIsInt($contextPos, $path); + self::assertIsInt($sessionPos, $path); + self::assertLessThan($sessionPos, $contextPos, $path); + self::assertStringContainsString('Request::setTrustedRequestContext', $source, $path); + self::assertStringNotContainsString('HTTP_X_FORWARDED_PROTO', $source, $path); + } + } + + public function testHtmlAdapterExposesReportOnlySecurityHeadersHooks(): void + { + $source = $this->read('Okay/Core/Adapters/Response/Html.php'); + + self::assertStringContainsString('X-Frame-Options: SAMEORIGIN', $source); + self::assertStringContainsString('X-Content-Type-Options: nosniff', $source); + self::assertStringContainsString('Referrer-Policy: strict-origin-when-cross-origin', $source); + self::assertStringContainsString("Request::getProtocol() === 'https'", $source); + self::assertStringNotContainsString("!empty(\$_SERVER['HTTPS'])", $source); + + // Report-Only CSP and optional HSTS / Permissions-Policy (config-driven). + self::assertTrue( + str_contains($source, 'Content-Security-Policy-Report-Only') + || str_contains($source, 'content_security_policy_report_only') + || str_contains($source, 'csp_report_only'), + 'Html adapter must be able to emit CSP Report-Only' + ); + } + + public function testJsonAdapterSendsNosniff(): void + { + $source = $this->read('Okay/Core/Adapters/Response/Json.php'); + + self::assertStringContainsString('X-Content-Type-Options: nosniff', $source); + } + + public function testFilemanagerUsesTrustedProtocolResolver(): void + { + $source = $this->read('backend/design/js/filemanager/UploadHandler.php'); + + self::assertStringContainsString('Request::getProtocol()', $source); + self::assertStringNotContainsString('HTTP_X_FORWARDED_PROTO', $source); + } + + private function read(string $relativePath): string + { + $path = $this->root . '/' . $relativePath; + self::assertFileExists($path); + + return (string) file_get_contents($path); + } +} diff --git a/tests/Security/CoreSecauditB5Deploy1CTest.php b/tests/Security/CoreSecauditB5Deploy1CTest.php new file mode 100644 index 000000000..7b372292f --- /dev/null +++ b/tests/Security/CoreSecauditB5Deploy1CTest.php @@ -0,0 +1,80 @@ +root = dirname(__DIR__, 2); + } + + public function testAutoDeployBuildKeyUsesHashEqualsAndCsprngInit(): void + { + $controller = $this->read( + 'Okay/Modules/OkayCMS/AutoDeploy/Controllers/BuildController.php' + ); + $admin = $this->read( + 'Okay/Modules/OkayCMS/AutoDeploy/Backend/Controllers/AutoDeployAdmin.php' + ); + + self::assertStringNotContainsString('$buildKey != $currentBuildKey', $controller); + self::assertStringContainsString('hash_equals', $controller); + self::assertStringNotContainsString('md5(microtime())', $admin); + self::assertTrue( + str_contains($admin, 'random_bytes') + || str_contains($admin, 'bin2hex(random_bytes'), + 'Deploy build key must initialize with CSPRNG' + ); + } + + public function testIntegration1CPathIsContainedAndExtensionCheckedBeforeWrite(): void + { + $source = $this->read( + 'Okay/Modules/OkayCMS/Integration1C/Integration/Integration1C.php' + ); + + self::assertStringContainsString('function getFullPath', $source); + self::assertTrue( + str_contains($source, 'realpath') + || str_contains($source, 'str_starts_with'), + 'getFullPath must canonicalize / contain under dir' + ); + self::assertDoesNotMatchRegularExpression( + "/return \\\$this->dir \\. preg_replace\\('~\\\\\\.\\\\.\\/~'/", + $source, + 'Must not rely on single-pass ../ strip alone' + ); + } + + public function testIntegration1CXmlParsingUsesLocalSafeFlags(): void + { + $source = $this->read( + 'Okay/Modules/OkayCMS/Integration1C/Integration/Integration1C.php' + ); + + self::assertStringContainsString('LIBXML_NONET', $source); + self::assertStringContainsString('LIBXML_NO_XXE', $source); + self::assertStringNotContainsString('libxml_disable_entity_loader', $source); + self::assertStringNotContainsString('libxml_set_external_entity_loader', $source); + self::assertStringNotContainsString('LIBXML_NOENT', $source); + self::assertStringNotContainsString('LIBXML_DTDLOAD', $source); + } + + private function read(string $relativePath): string + { + $path = $this->root . '/' . $relativePath; + self::assertFileExists($path); + + return (string) file_get_contents($path); + } +} diff --git a/tests/Security/CoreSecauditB6AdminBoundaryTest.php b/tests/Security/CoreSecauditB6AdminBoundaryTest.php new file mode 100644 index 000000000..8d38a1b6a --- /dev/null +++ b/tests/Security/CoreSecauditB6AdminBoundaryTest.php @@ -0,0 +1,105 @@ +root = dirname(__DIR__, 2); + } + + public function testAdminCheckSessionUsesDedicatedTokenAndHashEquals(): void + { + $request = $this->read('Okay/Core/Request.php'); + $index = $this->read('backend/index.php'); + + self::assertStringContainsString('AdminCsrfToken', $request); + self::assertStringContainsString('hash_equals', $request); + self::assertStringNotContainsString( + '$sessionId != session_id()', + $request + ); + self::assertStringContainsString('AdminCsrfToken', $index); + self::assertDoesNotMatchRegularExpression( + "/\\\$_SESSION\\['id'\\]\\s*=\\s*session_id\\(\\)/", + $index, + '$_SESSION[id] must not be the PHP session id' + ); + } + + public function testImportLogAdminUsesBoundParametersForFilters(): void + { + $source = $this->read('backend/Controllers/ImportLogAdmin.php'); + + self::assertStringNotContainsString('LIKE \'%$kw%\'', $source); + self::assertStringNotContainsString('status=\'$filterStatusParam\'', $source); + self::assertTrue( + str_contains($source, 'bindValue') + || str_contains($source, 'bindValues'), + 'ImportLogAdmin must bind filter parameters' + ); + } + + public function testAdminLoginHasIpOrGlobalThrottleSignal(): void + { + $source = $this->read('backend/Controllers/AuthAdmin.php'); + + self::assertStringContainsString('AdminLoginThrottle', $source); + self::assertTrue( + str_contains($source, 'TrustedClientIp::resolve') + || str_contains($source, 'getRemoteAddr()'), + 'Admin login must use the trusted client-IP resolver' + ); + self::assertStringContainsString('trusted_proxy_ips', $this->read('config/config.php')); + self::assertStringContainsString('SafeRedirect::sameOriginUrl', $source); + } + + public function testThemeEditorsHonorAllowThemeFileEditorKillSwitch(): void + { + $config = $this->read('config/config.php'); + self::assertMatchesRegularExpression( + '/^allow_theme_file_editor\s*=\s*false$/m', + $config + ); + + foreach ( + [ + 'backend/ajax/save_script.php', + 'backend/ajax/save_style.php', + 'backend/ajax/save_template.php', + ] as $relPath + ) { + $source = $this->read($relPath); + self::assertStringContainsString('allow_theme_file_editor', $source, $relPath); + } + } + + public function testAdminAjaxBootstrapSeedsDedicatedCsrfToken(): void + { + $configure = $this->read('backend/ajax/configure.php'); + self::assertStringContainsString('AdminCsrfToken::get()', $configure); + self::assertStringContainsString("\$_SESSION['id'] = AdminCsrfToken::get()", $configure); + + $lessonDone = $this->read('backend/ajax/lesson_done.php'); + self::assertStringNotContainsString('session_id()', $lessonDone); + self::assertStringContainsString('configure.php', $lessonDone); + } + + private function read(string $relativePath): string + { + $path = $this->root . '/' . $relativePath; + self::assertFileExists($path); + + return (string) file_get_contents($path); + } +} diff --git a/tests/Security/CoreSecauditB7HygieneTest.php b/tests/Security/CoreSecauditB7HygieneTest.php new file mode 100644 index 000000000..5a5af5061 --- /dev/null +++ b/tests/Security/CoreSecauditB7HygieneTest.php @@ -0,0 +1,118 @@ +root = dirname(__DIR__, 2); + } + + public function testConfigTokenUsesHmacSha256AndHashEqualsCheck(): void + { + $source = $this->read('Okay/Core/Config.php'); + + self::assertStringNotContainsString('return md5($text . $this->salt);', $source); + self::assertStringContainsString('hash_hmac', $source); + self::assertStringContainsString('hash_equals', $source); + } + + public function testCommentAndRozetkaPayRedirectsAreSameOriginSafe(): void + { + $comments = $this->read('Okay/Helpers/CommentsHelper.php'); + $refund = $this->read( + 'Okay/Modules/OkayCMS/RozetkaPay/Backend/Controllers/RefundAdmin.php' + ); + + self::assertStringNotContainsString( + "redirectTo(\$_SERVER['REQUEST_URI']", + $comments + ); + self::assertStringNotContainsString( + 'redirectTo($_SERVER["HTTP_REFERER"])', + $refund + ); + self::assertTrue( + str_contains($comments, 'SafeRedirect') + || str_contains($comments, 'isSafeRedirect') + || str_contains($comments, 'sameOrigin') + || preg_match('/redirectTo\\([^\\$].*comment_/s', $comments) === 1 + || str_contains($comments, 'Router::generateUrl'), + 'Comment redirect must not blindly trust REQUEST_URI' + ); + } + + public function testSvgResizeResponsesAddDispositionOrSandboxHint(): void + { + $source = $this->read('Okay/Core/Adapters/Response/ImageSvg.php'); + + self::assertTrue( + str_contains($source, 'Content-Disposition') + || str_contains($source, 'Content-Security-Policy') + || str_contains($source, 'attachment'), + 'SVG responses must not be bare image/svg+xml without sandbox/disposition' + ); + } + + public function testHeadJsonLdEscapesSiteName(): void + { + $source = $this->read('design/okay_shop/html/head.tpl'); + + self::assertStringNotContainsString( + '{$settings->site_name}{literal}', + $source, + 'JSON-LD must not raw-interpolate site_name into