From c764f4a26e61ce6d226a159749168566edef4a02 Mon Sep 17 00:00:00 2001 From: nertyog Date: Thu, 30 Jul 2026 15:11:58 +0300 Subject: [PATCH] Updated Image Optimizer [NAT] for OpenCart 2.0-2.2 (v1.2.0) --- Instruction ru-uk-en.txt | 179 + image_optimizer_nat_OC_20_22_1_1.ocmod.zip | Bin 0 -> 650358 bytes install.xml | 906 ++ readme.txt | 104 + upload/admin/controller/module/img_opti.php | 2323 +++++ .../admin/language/en-gb/module/img_opti.php | 565 ++ .../language/english/module/img_opti.php | 565 ++ .../admin/language/ru-ru/module/img_opti.php | 565 ++ .../language/russian/module/img_opti.php | 564 ++ .../admin/language/uk-ua/module/img_opti.php | 567 ++ .../language/ukrainian/module/img_opti.php | 564 ++ upload/admin/model/module/img_opti.php | 3644 ++++++++ .../admin/view/template/module/img_opti.tpl | 3709 ++++++++ .../controller/module/img_opti_cron.php | 637 ++ upload/catalog/model/module/img_opti.php | 672 ++ upload/system/library/font/Roboto-Regular.ttf | Bin 0 -> 515100 bytes .../sitecreator/elFinder/php/MySQLStorage.sql | 47 + .../sitecreator/elFinder/php/autoload.php | 55 + .../elFinder/php/connector.maximal.php-dist | 433 + .../elFinder/php/connector.minimal.php-dist | 179 + .../elFinder/php/elFinder.class.php | 5273 ++++++++++++ .../elFinder/php/elFinderConnector.class.php | 380 + .../elFinderFlysystemGoogleDriveNetmount.php | 380 + .../elFinder/php/elFinderPlugin.php | 113 + .../elFinder/php/elFinderSession.php | 337 + .../elFinder/php/elFinderSessionInterface.php | 57 + .../elFinder/php/elFinderVolumeBox.class.php | 1972 +++++ .../php/elFinderVolumeDriver.class.php | 7620 +++++++++++++++++ .../php/elFinderVolumeDropbox.class.php | 1464 ++++ .../php/elFinderVolumeDropbox2.class.php | 1516 ++++ .../elFinder/php/elFinderVolumeFTP.class.php | 1810 ++++ .../php/elFinderVolumeGoogleDrive.class.php | 2163 +++++ .../php/elFinderVolumeGroup.class.php | 315 + .../elFinderVolumeLocalFileSystem.class.php | 1442 ++++ .../php/elFinderVolumeMySQL.class.php | 1022 +++ .../php/elFinderVolumeOneDrive.class.php | 2152 +++++ .../php/elFinderVolumeTrash.class.php | 51 + .../php/elFinderVolumeTrashMySQL.class.php | 51 + .../ControllerCommonTrueFileManager.php | 134 + 39 files changed, 44530 insertions(+) create mode 100644 Instruction ru-uk-en.txt create mode 100644 image_optimizer_nat_OC_20_22_1_1.ocmod.zip create mode 100644 install.xml create mode 100644 readme.txt create mode 100644 upload/admin/controller/module/img_opti.php create mode 100644 upload/admin/language/en-gb/module/img_opti.php create mode 100644 upload/admin/language/english/module/img_opti.php create mode 100644 upload/admin/language/ru-ru/module/img_opti.php create mode 100644 upload/admin/language/russian/module/img_opti.php create mode 100644 upload/admin/language/uk-ua/module/img_opti.php create mode 100644 upload/admin/language/ukrainian/module/img_opti.php create mode 100644 upload/admin/model/module/img_opti.php create mode 100644 upload/admin/view/template/module/img_opti.tpl create mode 100644 upload/catalog/controller/module/img_opti_cron.php create mode 100644 upload/catalog/model/module/img_opti.php create mode 100644 upload/system/library/font/Roboto-Regular.ttf create mode 100644 upload/system/library/sitecreator/elFinder/php/MySQLStorage.sql create mode 100644 upload/system/library/sitecreator/elFinder/php/autoload.php create mode 100644 upload/system/library/sitecreator/elFinder/php/connector.maximal.php-dist create mode 100644 upload/system/library/sitecreator/elFinder/php/connector.minimal.php-dist create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinder.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderConnector.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderFlysystemGoogleDriveNetmount.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderPlugin.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderSession.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderSessionInterface.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeBox.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeDriver.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeDropbox.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeDropbox2.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeFTP.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeGoogleDrive.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeGroup.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeLocalFileSystem.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeMySQL.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeOneDrive.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeTrash.class.php create mode 100644 upload/system/library/sitecreator/elFinder/php/elFinderVolumeTrashMySQL.class.php create mode 100644 upload/system/library/sitecreator/trueFileManager/ControllerCommonTrueFileManager.php diff --git a/Instruction ru-uk-en.txt b/Instruction ru-uk-en.txt new file mode 100644 index 0000000..2dac7d0 --- /dev/null +++ b/Instruction ru-uk-en.txt @@ -0,0 +1,179 @@ +========================================================= +Image Optimizer [NAT]: Compress originals & remove unused images v1.2.0 — Инструкция / Instructions / Інструкція +========================================================= + +--------------------------------------------------------- +🇷🇺 РУССКИЙ ЯЗЫК +--------------------------------------------------------- +УСТАНОВКА МОДУЛЯ: +1. Распакуйте архив. +2. Загрузите содержимое папки upload/ в корень вашего сайта по FTP (системные файлы не перезаписываются). +3. Перейдите в админ-панели в "Система" -> "Пользователи" -> "Группы пользователей", выберите вашу группу (Administrator) и установите галочки прав на просмотр и изменение для: extension/module/img_opti +4. Перейдите в "Модули / Расширения" -> "Модули", найдите "Image Optimizer [NAT]", нажмите "Активировать" (зеленый плюс), затем "Редактировать". + +ОБНОВЛЕНИЕ: +Загрузите новые файлы поверх старых по FTP. + +УДАЛЕНИЕ: +Деактивируйте модуль в списке расширений. Удалите файлы модуля: +- admin/controller/extension/module/img_opti.php +- admin/language/*/extension/module/img_opti.php +- admin/view/template/extension/module/img_opti.twig + +КАК РАБОТАТЬ С МОДУЛЕМ: +ВНИМАНИЕ! Перед любыми манипуляциями ОБЯЗАТЕЛЬНО сделайте бэкап папки /image/catalog/ на сервере! + +Вкладка 1 (Инфо и статистика): +- График диска: Анализирует размер папок (картинки, кэш, корзина). Можно задать лимит тарифа вручную и включить/выключить автообновление графика при входе. +- Статистика оптимизации: Отображает общее число обработанных файлов и сэкономленный дисковый объем. + +Вкладка 2 (Сжатие и оптимизация): +1. Введите лицензионный токен. +2. Выберите папки для сжатия, укажите лимиты по ширине/высоте и качеству (WebP/JPG/PNG). +3. Выберите режим (Безопасное копирование в новую папку или Замена оригиналов) и нажмите "СТАРТ ОПТИМИЗАЦИИ". + +Вкладка 3 (Очистка неиспользуемых): +1. Выберите папки для поиска мусора. +2. Нажмите "НАЙТИ МУСОР". Модуль сверит физические файлы с базой данных и шаблонами. +3. Проверьте найденные файлы в логе и отправьте их в "Карантин" (безопасно) или удалите навсегда. + +Вкладка 4 (Водяные знаки): +- Настройка правил наложения водяных знаков (изображений или текста) для товаров, категорий, брендов и баннеров "на лету" с фильтрацией по категориям, регулировкой угла наклона, прозрачности и скругления кутов. + +Вкладка 5 (Заглушки): +- Автоподстановка картинок-заглушек для товаров, категорий и производителей, у которых изображение не задано в каталоге. + +Вкладка 6 (Форматы изображений 🧪 BETA): +- WebP "на лету", поддержка SVG, прогрессивный Lazy Load (эффект плавного размытия blur-up при прокрутке страницы) и переопределение глобальной заглушки для битых ссылок. + +Вкладка 7 (Инструменты (pro) 🧪 BETA): +- Санитайзер имен: Очистка имен файлов и папок от кириллицы, пробелов и спецсимволов. +- Битые ссылки в БД: Поиск и очистка записей БД с отсутствующими файлами. +- Битые файлы на сервере: Поиск и удаление испорченных картинок и файлов по 0 байт. +- Поиск тяжелых файлов: Быстрое нахождение самых объемных файлов для ручного сжатия. +- Конвертер CMYK: Перекодирование изображений из CMYK в sRGB для корректного показа на iPhone/Safari. + +Вкладка 8 (Крон 🧪 BETA): +- Настройка расписания автоматического фонового сжатия папок через системный планировщик задач. + + + +--------------------------------------------------------- +🇬🇧 ENGLISH +--------------------------------------------------------- +INSTALLATION: +1. Extract the archive. +2. Upload the contents of the upload/ folder to your OpenCart root directory via FTP (no core files are overwritten). +3. Go to System -> Users -> User Groups, select your group (Administrator) and check Access/Modify permissions for: extension/module/img_opti +4. Go to Extensions -> Modules, find "Image Optimizer [NAT]", click "Install" (green plus), then "Edit". + +UPDATING: +Upload new files over the old ones via FTP. + +UNINSTALLATION: +Uninstall the module from the Extensions list. Delete the module files from: +- admin/controller/extension/module/img_opti.php +- admin/language/*/extension/module/img_opti.php +- admin/view/template/extension/module/img_opti.twig + +HOW TO USE (TABS & FEATURES): +WARNING! Always create a full backup of your /image/catalog/ folder before running any optimizations or deletions! + +Tab 1 (Info & Stats): +- Disk Usage Chart: Renders a pie chart of folder sizes (Image Catalog, Cache, Trash). Supports manual hosting quota limit overrides and auto-update toggling on page open. +- Cumulative Stats: Displays total optimized files and total disk space saved. + +Tab 2 (Optimize & Resize): +1. Enter your license token. +2. Select target folders, adjust Max Width/Height and quality settings (WebP/JPG/PNG). +3. Select operation mode (Create a copy or Replace originals) and click "START OPTIMIZATION". + +Tab 3 (Junk Cleaner): +1. Select the folders to scan. +2. Click "FIND JUNK". The module compares physical files on disk with your Database and Theme files. +3. Review the found files in the log and move them to "Quarantine" (safe) or delete them permanently. + +Tab 4 (Watermarks): +- Dynamic Overlay rules: Set up watermarks (image or text) to overlay on category, brand, banner, or product images on-the-fly. +- Position, transparency, angle, font (TTF), size scaling, and category exclusion/inclusion filters are fully supported. + +Tab 5 (Placeholders): +- Renders fallback images for products, categories, or brands that do not have an image assigned in the database, keeping the storefront look consistent. + +Tab 6 (Image Formats 🧪 BETA): +- WebP Support: Enable WebP on-the-fly delivery. +- SVG Support: Safe SVG rendering. +- Progressive Lazy Loading: Blur-up effect (loads micro-preview and fades/de-blurs smoothly as user scrolls). +- Fallback Image: Set global placeholder image for broken images. + +Tab 7 (Tools (pro) 🧪 BETA): +- Filename Sanitizer: Clear names of cyrillic letters, spaces, and special symbols in file and directory paths. +- Broken DB Scanner: Finds and fixes database records referencing non-existent files. +- Broken Files Scanner: Scans the server to clean up corrupted or 0-byte images. +- Heavy Files: Find the largest images on disk and optimize them instantly. +- CMYK Converter: Convert color space of JPEG files from CMYK to sRGB for Apple iOS/Safari compatibility. + +Tab 8 (Cron 🧪 BETA): +- Configure automated background directory scans and image compression schedules using a Cron token. + + + +--------------------------------------------------------- +🇺🇦 УКРАЇНСЬКА МОВА +--------------------------------------------------------- +ВСТАНОВЛЕННЯ: +1. Розпакуйте архів. +2. Завантажте вміст папки upload/ в корінь вашого сайту по FTP (системні файли не перезаписуються). +3. Перейдіть у "Система" -> "Користувачі" -> "Групи", виберіть вашу групу (Administrator) і встановіть галочки прав на перегляд і зміну для: extension/module/img_opti +4. Перейдіть у "Модулі / Розширення" -> "Модулі", знайдіть "Image Optimizer [NAT]", натисніть "Активувати" (зелений плюс), потім "Редагувати". + +ОНОВЛЕННЯ: +Завантажте нові файли поверх старих по FTP. + +ЯК ПРАЦЮВАТИ З МОДУЛЕМ: +УВАГА! Перед будь-якими маніпуляціями ОБОВ'ЯЗКОВО зробіть бекап папки /image/catalog/ на сервері! + +Вкладка 1 (Інфо та статистика): +- Графік диска: Аналізує розмір папок (зображення, кэш, кошик). Можна задати ліміт тарифу вручну та увімкнути/вимкнути автооновлення графіка при вході. +- Статистика оптимізації: Показує загальну кількість оброблених файлів та збережений об`єм диска. + +Вкладка 2 (Стиснення та оптимізація): +1. Введіть ліцензійний токен. +2. Виберіть папки для стиснення, вкажіть ліміти по ширині/висоті та якості (WebP/JPG/PNG). +3. Виберіть режим (Безпечне копіювання в нову папку або Заміна оригіналів) і натисніть "СТАРТ ОПТИМІЗАЦІЇ". + +Вкладка 3 (Очищення невикористовуваних): +1. Виберіть папки для пошуку сміття. +2. Натисніть "ЗНАЙТИ СМІТТЯ". Модуль звірить фізичні файли з базой даних та шаблонами. +3. Перевірте знайдені файли в лозі та відправте їх у "Карантин" (безпечно) або видаліть назавжди. + +Вкладка 4 (Водяні знаки): +- Налаштування правил накладання водяних знаків (зображень або тексту) для товарів, категорій, брендів та банерів "на льоту" з фільтрацією за категоріями, регулюванням кута нахилу, прозорості та заокруглення кутів. + +Вкладка 5 (Заглушки): +- Автопідстановка картинок-заглушек для товарів, категорій та виробників, у яких зображення не задано в каталозі. + +Вкладка 6 (Формати зображень 🧪 BETA): +- WebP "на льоту", підтримка SVG, прогресивний Lazy Load (ефект плавного розмиття blur-up при прокручуванні сторінки) та перевизначення глобальної заглушки для битих посилань. + +Вкладка 7 (Інструменти (pro) 🧪 BETA): +- Санітайзер імен: Очищення назв файлів та папок від кирилиці, пробілів та спецсимволів. +- Биті посилання в БД: Пошук та очищення записів БД з відсутніми файлами. +- Биті файли на сервері: Пошук та вилучення зіпсованих картинок та файлів по 0 байт. +- Пошук важких файлів: Швидке знаходження найоб`ємніших файлів для стиснення. +- Конвертер CMYK: Перекодування зображень з CMYK в sRGB для коректного відображення на iPhone/Safari. + +Вкладка 8 (Крон 🧪 BETA): +- Налаштування розкладу автоматичного фонового стиснення папок через системний планувальник завдань. + + + +========================================================= +🔥 OUR OTHER OPENCART EXTENSIONS / НАШИ ДРУГИЕ МОДУЛИ / НАШІ ІНШІ МОДУЛІ: +========================================================= +• Catalog Optimizer [NAT]: Hide empty categories/brands, clear unused attributes/options. +• Domain Scanner [NAT]: Find and clean external links and images. +• Promo Top Bar [NAT]: Create interactive promo bars, marquees and header slideshows. + + +Support: info@nat.od.ua \ No newline at end of file diff --git a/image_optimizer_nat_OC_20_22_1_1.ocmod.zip b/image_optimizer_nat_OC_20_22_1_1.ocmod.zip new file mode 100644 index 0000000000000000000000000000000000000000..18cee30ecf956354e84261410e8b5bbcd127747c GIT binary patch literal 650358 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%0Ae-S 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 % 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 z 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 literal 0 HcmV?d00001 diff --git a/install.xml b/install.xml new file mode 100644 index 0000000..c7fa895 --- /dev/null +++ b/install.xml @@ -0,0 +1,906 @@ + + + Image Optimizer [NAT] (OC 2.0-2.2) + Image_Optimizer_nat + img_opti_nat_od_ua_v1 + 1.2.0 + NAT + mailto:info@nat.od.ua + + + + + + + + + + + + user->hasPermission('access', 'catalog/product')) {]]> + load->model('setting/setting'); + if ($this->config->get('module_img_opti_status')) { + $nat_pos = $this->config->get('module_img_opti_menu_position'); + if ($nat_pos == 'sidebar' || $nat_pos == 'both') { + if ($this->user->hasPermission('access', 'module/img_opti')) { + $nat_item = array( + 'name' => 'Image Optimizer', + 'href' => $this->url->link('module/img_opti', 'token=' . $this->session->data['token'], true), + 'children' => array() + ); + + $nat_menu_index = -1; + if (isset($data['menus'])) { + foreach ($data['menus'] as $k => $menu) { + if ($menu['id'] == 'menu-nat') { + $nat_menu_index = $k; + break; + } + } + } + + if ($nat_menu_index > -1) { + $data['menus'][$nat_menu_index]['children'][] = $nat_item; + } else { + $data['menus'][] = array( + 'id' => 'menu-nat', + 'icon' => 'fa-rocket fw', + 'name' => '[NAT] Modules', + 'href' => '', + 'children' => array($nat_item) + ); + } + } + } + } + ]]> + + + + + + url->link('common/logout']]> + load->model('setting/setting'); + if ($this->config->get('module_img_opti_status')) { + $nat_pos = $this->config->get('module_img_opti_menu_position'); + if ($nat_pos == 'header' || $nat_pos == 'both') { + if ($this->user->hasPermission('access', 'module/img_opti')) { + $data['nat_menus'][] = array( + 'name' => 'Image Optimizer', + 'href' => $this->url->link('module/img_opti', 'token=' . $this->session->data['token'], true) + ); + } + } + } + ]]> + + + + + + + + + + + ]]> + + + + + + + config->get('module_img_opti_support_svg') && !in_array('svg', $allowed)) { $allowed[] = 'svg'; } + if ($this->config->get('module_img_opti_support_webp') && !in_array('webp', $allowed)) { $allowed[] = 'webp'; } + ]]> + + + + config->get('module_img_opti_support_svg') && !in_array('image/svg+xml', $allowed)) { $allowed[] = 'image/svg+xml'; } + if ($this->config->get('module_img_opti_support_webp')) { + if (!in_array('image/webp', $allowed)) { $allowed[] = 'image/webp'; } + if (!in_array('image/vnd.webp', $allowed)) { $allowed[] = 'image/vnd.webp'; } + if (!in_array('image/x-webp', $allowed)) { $allowed[] = 'image/x-webp'; } + $uploaded_ext = utf8_strtolower(utf8_substr(strrchr($filename, '.'), 1)); + if ($uploaded_ext === 'webp' && !in_array($file['type'], $allowed)) { + $allowed[] = $file['type']; + } + } + ]]> + + + + ]*>(.*?)<\/script>/is', '', $svg_content); + $svg_content = preg_replace('/on\w+\s*=\s*["\'][^"\']*["\']/i', '', $svg_content); + $svg_content = preg_replace('/href\s*=\s*["\']\s*javascript:[^"\']*["\']/i', '', $svg_content); + @file_put_contents($file['tmp_name'], $svg_content); + } + } + ]]> + + + + config->get('module_img_opti_support_svg')) { + $ext_list[] = 'svg'; $ext_list[] = 'SVG'; + } + if ($this->config->get('module_img_opti_support_webp')) { + $ext_list[] = 'webp'; $ext_list[] = 'WEBP'; + } + $files = glob($directory . '/' . $filter_name . '*.{' . implode(',', $ext_list) . '}', GLOB_BRACE); + ]]> + + + + + + + registry; + if (empty($filename) || ($filename !== 'no_image.png' && $filename !== 'placeholder.png' && !is_file(DIR_IMAGE . $filename))) { + if ($this->config->get('module_img_opti_status') && $this->config->get('module_img_opti_broken_fallback')) { + $fallback_image = $this->config->get('module_img_opti_fallback_image'); + if ($fallback_image && is_file(DIR_IMAGE . $fallback_image)) { + $filename = $fallback_image; + } + } + } + if (!empty($filename) && strpos($filename, '.') !== false) { + $nat_ext = strtolower(substr(strrchr($filename, '.'), 1)); + if ($nat_ext === 'svg' || ($nat_ext === 'webp' && !function_exists('imagewebp'))) { + if (isset($this->request->server['HTTPS']) && (($this->request->server['HTTPS'] == 'on' || ($this->request->server['HTTPS'] == '1')))) { + return $this->config->get('config_ssl') . 'image/' . str_replace('\\', '/', $filename); + } else { + return $this->config->get('config_url') . 'image/' . str_replace('\\', '/', $filename); + } + } + } + ]]> + + + + + + + + + + + save(DIR_IMAGE . $image_new);]]> + config->get('module_img_opti_status') && $this->config->get('module_img_opti_wm_status')) { + $this->load->model('module/img_opti'); + try { + $this->model_module_img_opti->applyDynamicWatermark(DIR_IMAGE . $image_new, $filename); + } catch (\Exception $e) {} + } + ]]> + + + + config->get('module_img_opti_status') && $this->config->get('module_img_opti_wm_status')) { + $this->load->model('module/img_opti'); + try { + $this->model_module_img_opti->applyDynamicWatermark(DIR_IMAGE . $image_new, $filename); + } catch (\Exception $e) {} + } + }]]> + + + + + + + registry; + if (empty($filename) || ($filename !== 'no_image.png' && $filename !== 'placeholder.png' && !is_file(DIR_IMAGE . $filename))) { + if ($this->config->get('module_img_opti_status') && $this->config->get('module_img_opti_broken_fallback')) { + $fallback_image = $this->config->get('module_img_opti_fallback_image'); + if ($fallback_image && is_file(DIR_IMAGE . $fallback_image)) { + $filename = $fallback_image; + } + } + } + if (!empty($filename) && strpos($filename, '.') !== false) { + $nat_ext = strtolower(substr(strrchr($filename, '.'), 1)); + if ($nat_ext === 'svg' || ($nat_ext === 'webp' && !function_exists('imagewebp'))) { + $nat_https = isset($this->request->server['HTTPS']) && ($this->request->server['HTTPS'] == 'on' || $this->request->server['HTTPS'] == '1'); + $nat_base = $nat_https ? (defined('HTTPS_CATALOG') ? HTTPS_CATALOG : $this->config->get('config_ssl')) : (defined('HTTP_CATALOG') ? HTTP_CATALOG : $this->config->get('config_url')); + return $nat_base . 'image/' . str_replace('\\', '/', $filename); + } + } + ]]> + + + + + + + + + + + + + + mime == 'image/gif') {]]> + mime == 'image/webp' || $this->mime == 'image/vnd.webp' || $this->mime == 'image/x-webp') && function_exists('imagecreatefromwebp')) { + $this->image = @imagecreatefromwebp($file); + } else + ]]> + + + + image, $file, $quality); + } else + ]]> + + + image, $file, $quality);]]> + image, true); + ]]> + + + + + + output;]]> + output; + $registry = isset($GLOBALS['registry']) ? $GLOBALS['registry'] : null; + if ($registry && !defined('DIR_CATALOG')) { + $config = $registry->get('config'); + if ($config && $config->get('module_img_opti_status')) { + $webp_on_fly = $config->get('module_img_opti_webp_on_fly'); + $lazy_load = $config->get('module_img_opti_lazy_load'); + $broken_fallback = $config->get('module_img_opti_broken_fallback'); + $fallback_image = $config->get('module_img_opti_fallback_image'); + + $theme = $config->get('config_theme'); + $additional_width = 0; + $additional_height = 0; + $thumb_width = 0; + $thumb_height = 0; + + if ($theme) { + $additional_width = $config->get('theme_' . $theme . '_image_additional_width'); + $additional_height = $config->get('theme_' . $theme . '_image_additional_height'); + $thumb_width = $config->get('theme_' . $theme . '_image_thumb_width'); + $thumb_height = $config->get('theme_' . $theme . '_image_thumb_height'); + } + + if (!$additional_width) { $additional_width = $config->get('theme_default_image_additional_width'); } + if (!$additional_height) { $additional_height = $config->get('theme_default_image_additional_height'); } + if (!$thumb_width) { $thumb_width = $config->get('theme_default_image_thumb_width'); } + if (!$thumb_height) { $thumb_height = $config->get('theme_default_image_thumb_height'); } + + if (!$additional_width) { $additional_width = $config->get('config_image_additional_width'); } + if (!$additional_height) { $additional_height = $config->get('config_image_additional_height'); } + if (!$thumb_width) { $thumb_width = $config->get('config_image_thumb_width'); } + if (!$thumb_height) { $thumb_height = $config->get('config_image_thumb_height'); } + + if (!$additional_width) { $additional_width = 74; } + if (!$additional_height) { $additional_height = 74; } + if (!$thumb_width) { $thumb_width = 228; } + if (!$thumb_height) { $thumb_height = 228; } + + $svg_style = ''; + $output = str_ireplace('', $svg_style . '', $output); + + if ($webp_on_fly || $lazy_load || $broken_fallback) { + $registry->get('load')->model('tool/image'); + $model_tool_image = $registry->get('model_tool_image'); + + $output = preg_replace_callback('/]*)>/is', function($matches) use ($config, $registry, $model_tool_image, $webp_on_fly, $lazy_load, $broken_fallback, $fallback_image) { + $attrs_str = $matches[1]; + if (stripos($attrs_str, 'catalog_trash') !== false) { + return $matches[0]; + } + + preg_match_all('/(\b[\w\-:]+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/is', $attrs_str, $attr_matches, PREG_SET_ORDER); + $attrs = array(); + foreach ($attr_matches as $am) { + $name = strtolower($am[1]); + $val = ($am[2] !== '') ? $am[2] : (($am[3] !== '') ? $am[3] : $am[4]); + $attrs[$name] = $val; + } + + $src = isset($attrs['src']) ? $attrs['src'] : ''; + $data_src = isset($attrs['data-src']) ? $attrs['data-src'] : ''; + $has_datasrc = ($data_src !== ''); + + $target_url = $has_datasrc ? $data_src : $src; + $rel_path = ''; + if (preg_match('/\\/image\\/(.*\\.(?:jpg|jpeg|png|webp|svg))/is', $target_url, $m_rel)) { + $rel_path = urldecode($m_rel[1]); + } elseif (preg_match('/^image\\/(.*\\.(?:jpg|jpeg|png|webp|svg))/is', $target_url, $m_rel)) { + $rel_path = urldecode($m_rel[1]); + } + + $clean_rel_path = preg_replace('/\\?.*/', '', $rel_path); + $browser_supports_webp = isset($_SERVER['HTTP_ACCEPT']) && (strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') !== false); + + if ($webp_on_fly && $browser_supports_webp && $clean_rel_path) { + $ext = strtolower(substr(strrchr($clean_rel_path, '.'), 1)); + if (in_array($ext, array('jpg', 'jpeg', 'png'))) { + $phys_source = DIR_IMAGE . $clean_rel_path; + $phys_webp = DIR_IMAGE . preg_replace('/\\.(jpg|jpeg|png)$/i', '.webp', $clean_rel_path); + if (file_exists($phys_source)) { + if (!file_exists($phys_webp)) { + $img_data = @file_get_contents($phys_source); + if ($img_data) { + $im = @imagecreatefromstring($img_data); + if ($im) { + $webp_quality = (int)$config->get('module_img_opti_webp_quality'); + if ($webp_quality <= 0 || $webp_quality > 100) { + $webp_quality = 80; + } + @imagewebp($im, $phys_webp, $webp_quality); + @imagedestroy($im); + } + } + } + if (file_exists($phys_webp)) { + if (isset($attrs['src'])) { + $attrs['src'] = preg_replace('/\\.(jpg|jpeg|png)(\\?|$)/i', '.webp$2', $attrs['src']); + } + if (isset($attrs['data-src'])) { + $attrs['data-src'] = preg_replace('/\\.(jpg|jpeg|png)(\\?|$)/i', '.webp$2', $attrs['data-src']); + } + } + } + } + } + + if ($webp_on_fly && $browser_supports_webp) { + foreach (array('srcset', 'data-srcset') as $srcset_attr) { + if (isset($attrs[$srcset_attr])) { + $parts = explode(',', $attrs[$srcset_attr]); + foreach ($parts as &$part) { + $part = trim($part); + $subparts = preg_split('/\\s+/', $part); + if (!empty($subparts[0])) { + $s_url = $subparts[0]; + $s_rel = ''; + if (preg_match('/\\/image\\/(.*\\.(?:jpg|jpeg|png|webp|svg))/is', $s_url, $sm_rel)) { + $s_rel = urldecode($sm_rel[1]); + } elseif (preg_match('/^image\\/(.*\\.(?:jpg|jpeg|png|webp|svg))/is', $s_url, $sm_rel)) { + $s_rel = urldecode($sm_rel[1]); + } + $s_clean = preg_replace('/\\?.*/', '', $s_rel); + if ($s_clean) { + $s_ext = strtolower(substr(strrchr($s_clean, '.'), 1)); + if (in_array($s_ext, array('jpg', 'jpeg', 'png'))) { + $s_phys_source = DIR_IMAGE . $s_clean; + $s_phys_webp = DIR_IMAGE . preg_replace('/\\.(jpg|jpeg|png)$/i', '.webp', $s_clean); + if (file_exists($s_phys_source)) { + if (!file_exists($s_phys_webp)) { + $img_data = @file_get_contents($s_phys_source); + if ($img_data) { + $im = @imagecreatefromstring($img_data); + if ($im) { + $webp_quality = (int)$config->get('module_img_opti_webp_quality'); + if ($webp_quality <= 0 || $webp_quality > 100) { + $webp_quality = 80; + } + @imagewebp($im, $s_phys_webp, $webp_quality); + @imagedestroy($im); + } + } + } + if (file_exists($s_phys_webp)) { + $subparts[0] = preg_replace('/\\.(jpg|jpeg|png)(\\?|$)/i', '.webp$2', $s_url); + } + } + } + } + } + $part = implode(' ', $subparts); + } + $attrs[$srcset_attr] = implode(', ', $parts); + } + } + } + + $is_svg = (strtolower(substr(strrchr($clean_rel_path, '.'), 1)) === 'svg'); + + if ($lazy_load && $clean_rel_path && is_file(DIR_IMAGE . $clean_rel_path) && !$is_svg) { + try { + $tiny_thumb = $model_tool_image->resize($clean_rel_path, 20, 20); + if ($has_datasrc) { + $attrs['src'] = $tiny_thumb; + } else { + $attrs['data-src'] = isset($attrs['src']) ? $attrs['src'] : ''; + $attrs['src'] = $tiny_thumb; + } + if (isset($attrs['srcset'])) { + $attrs['data-srcset'] = $attrs['srcset']; + unset($attrs['srcset']); + } + $class = isset($attrs['class']) ? $attrs['class'] . ' img-lazyload' : 'img-lazyload'; + $attrs['class'] = $class; + + $style = 'filter: blur(10px); transition: filter 0.3s;'; + if (isset($attrs['style'])) { + $attrs['style'] = rtrim(trim($attrs['style']), ';') . '; ' . $style; + } else { + $attrs['style'] = $style; + } + } catch (Exception $e) {} + } + + if ($broken_fallback) { + $fallback_url = ''; + $req_obj = $registry->get('request'); + $is_https = isset($req_obj->server['HTTPS']) && ($req_obj->server['HTTPS'] == 'on' || $req_obj->server['HTTPS'] == '1'); + $server_url = $is_https ? $config->get('config_ssl') : $config->get('config_url'); + if ($fallback_image && is_file(DIR_IMAGE . $fallback_image)) { + $fallback_url = $server_url . 'image/' . $fallback_image; + } elseif ($config->get('config_logo') && is_file(DIR_IMAGE . $config->get('config_logo'))) { + $fallback_url = $server_url . 'image/' . $config->get('config_logo'); + } else { + $fallback_url = $server_url . 'image/no_image.png'; + } + if ($fallback_url) { + $attrs['onerror'] = "this.src='" . $fallback_url . "'; this.srcset=''; this.onerror=null;"; + } + } + + $new_attrs = array(); + foreach ($attrs as $k => $v) { + $new_attrs[] = $k . '="' . htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . '"'; + } + return ''; + }, $output); + + if ($lazy_load) { + $lazy_script = ' + +'; + $output = str_ireplace('', $lazy_script, $output); + } + } + } + } + $this->output = $output; + echo $this->output; + ]]> + + + + get('config'); + if ($config && $config->get('module_img_opti_status')) { + $webp_on_fly = $config->get('module_img_opti_webp_on_fly'); + $lazy_load = $config->get('module_img_opti_lazy_load'); + $broken_fallback = $config->get('module_img_opti_broken_fallback'); + $fallback_image = $config->get('module_img_opti_fallback_image'); + + $theme = $config->get('config_theme'); + $additional_width = 0; + $additional_height = 0; + $thumb_width = 0; + $thumb_height = 0; + + if ($theme) { + $additional_width = $config->get('theme_' . $theme . '_image_additional_width'); + $additional_height = $config->get('theme_' . $theme . '_image_additional_height'); + $thumb_width = $config->get('theme_' . $theme . '_image_thumb_width'); + $thumb_height = $config->get('theme_' . $theme . '_image_thumb_height'); + } + + if (!$additional_width) { $additional_width = $config->get('theme_default_image_additional_width'); } + if (!$additional_height) { $additional_height = $config->get('theme_default_image_additional_height'); } + if (!$thumb_width) { $thumb_width = $config->get('theme_default_image_thumb_width'); } + if (!$thumb_height) { $thumb_height = $config->get('theme_default_image_thumb_height'); } + + if (!$additional_width) { $additional_width = $config->get('config_image_additional_width'); } + if (!$additional_height) { $additional_height = $config->get('config_image_additional_height'); } + if (!$thumb_width) { $thumb_width = $config->get('config_image_thumb_width'); } + if (!$thumb_height) { $thumb_height = $config->get('config_image_thumb_height'); } + + if (!$additional_width) { $additional_width = 74; } + if (!$additional_height) { $additional_height = 74; } + if (!$thumb_width) { $thumb_width = 228; } + if (!$thumb_height) { $thumb_height = 228; } + + $svg_style = ''; + $output = str_ireplace('', $svg_style . '', $output); + + if ($webp_on_fly || $lazy_load || $broken_fallback) { + $registry->get('load')->model('tool/image'); + $model_tool_image = $registry->get('model_tool_image'); + + $output = preg_replace_callback('/]*)>/is', function($matches) use ($config, $registry, $model_tool_image, $webp_on_fly, $lazy_load, $broken_fallback, $fallback_image) { + $attrs_str = $matches[1]; + if (stripos($attrs_str, 'catalog_trash') !== false) { + return $matches[0]; + } + + preg_match_all('/(\b[\w\-:]+)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s>]+))/is', $attrs_str, $attr_matches, PREG_SET_ORDER); + $attrs = array(); + foreach ($attr_matches as $am) { + $name = strtolower($am[1]); + $val = ($am[2] !== '') ? $am[2] : (($am[3] !== '') ? $am[3] : $am[4]); + $attrs[$name] = $val; + } + + $src = isset($attrs['src']) ? $attrs['src'] : ''; + $data_src = isset($attrs['data-src']) ? $attrs['data-src'] : ''; + $has_datasrc = ($data_src !== ''); + + $target_url = $has_datasrc ? $data_src : $src; + $rel_path = ''; + if (preg_match('/\\/image\\/(.*\\.(?:jpg|jpeg|png|webp|svg))/is', $target_url, $m_rel)) { + $rel_path = urldecode($m_rel[1]); + } elseif (preg_match('/^image\\/(.*\\.(?:jpg|jpeg|png|webp|svg))/is', $target_url, $m_rel)) { + $rel_path = urldecode($m_rel[1]); + } + + $clean_rel_path = preg_replace('/\\?.*/', '', $rel_path); + $browser_supports_webp = isset($_SERVER['HTTP_ACCEPT']) && (strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') !== false); + + if ($webp_on_fly && $browser_supports_webp && $clean_rel_path) { + $ext = strtolower(substr(strrchr($clean_rel_path, '.'), 1)); + if (in_array($ext, array('jpg', 'jpeg', 'png'))) { + $phys_source = DIR_IMAGE . $clean_rel_path; + $phys_webp = DIR_IMAGE . preg_replace('/\\.(jpg|jpeg|png)$/i', '.webp', $clean_rel_path); + if (file_exists($phys_source)) { + if (!file_exists($phys_webp)) { + $img_data = @file_get_contents($phys_source); + if ($img_data) { + $im = @imagecreatefromstring($img_data); + if ($im) { + $webp_quality = (int)$config->get('module_img_opti_webp_quality'); + if ($webp_quality <= 0 || $webp_quality > 100) { + $webp_quality = 80; + } + @imagewebp($im, $phys_webp, $webp_quality); + @imagedestroy($im); + } + } + } + if (file_exists($phys_webp)) { + if (isset($attrs['src'])) { + $attrs['src'] = preg_replace('/\\.(jpg|jpeg|png)(\\?|$)/i', '.webp$2', $attrs['src']); + } + if (isset($attrs['data-src'])) { + $attrs['data-src'] = preg_replace('/\\.(jpg|jpeg|png)(\\?|$)/i', '.webp$2', $attrs['data-src']); + } + } + } + } + } + + if ($webp_on_fly && $browser_supports_webp) { + foreach (array('srcset', 'data-srcset') as $srcset_attr) { + if (isset($attrs[$srcset_attr])) { + $parts = explode(',', $attrs[$srcset_attr]); + foreach ($parts as &$part) { + $part = trim($part); + $subparts = preg_split('/\\s+/', $part); + if (!empty($subparts[0])) { + $s_url = $subparts[0]; + $s_rel = ''; + if (preg_match('/\\/image\\/(.*\\.(?:jpg|jpeg|png|webp|svg))/is', $s_url, $sm_rel)) { + $s_rel = urldecode($sm_rel[1]); + } elseif (preg_match('/^image\\/(.*\\.(?:jpg|jpeg|png|webp|svg))/is', $s_url, $sm_rel)) { + $s_rel = urldecode($sm_rel[1]); + } + $s_clean = preg_replace('/\\?.*/', '', $s_rel); + if ($s_clean) { + $s_ext = strtolower(substr(strrchr($s_clean, '.'), 1)); + if (in_array($s_ext, array('jpg', 'jpeg', 'png'))) { + $s_phys_source = DIR_IMAGE . $s_clean; + $s_phys_webp = DIR_IMAGE . preg_replace('/\\.(jpg|jpeg|png)$/i', '.webp', $s_clean); + if (file_exists($s_phys_source)) { + if (!file_exists($s_phys_webp)) { + $img_data = @file_get_contents($s_phys_source); + if ($img_data) { + $im = @imagecreatefromstring($img_data); + if ($im) { + $webp_quality = (int)$config->get('module_img_opti_webp_quality'); + if ($webp_quality <= 0 || $webp_quality > 100) { + $webp_quality = 80; + } + @imagewebp($im, $s_phys_webp, $webp_quality); + @imagedestroy($im); + } + } + } + if (file_exists($s_phys_webp)) { + $subparts[0] = preg_replace('/\\.(jpg|jpeg|png)(\\?|$)/i', '.webp$2', $s_url); + } + } + } + } + } + $part = implode(' ', $subparts); + } + $attrs[$srcset_attr] = implode(', ', $parts); + } + } + } + + $is_svg = (strtolower(substr(strrchr($clean_rel_path, '.'), 1)) === 'svg'); + + if ($lazy_load && $clean_rel_path && is_file(DIR_IMAGE . $clean_rel_path) && !$is_svg) { + try { + $tiny_thumb = $model_tool_image->resize($clean_rel_path, 20, 20); + if ($has_datasrc) { + $attrs['src'] = $tiny_thumb; + } else { + $attrs['data-src'] = isset($attrs['src']) ? $attrs['src'] : ''; + $attrs['src'] = $tiny_thumb; + } + if (isset($attrs['srcset'])) { + $attrs['data-srcset'] = $attrs['srcset']; + unset($attrs['srcset']); + } + $class = isset($attrs['class']) ? $attrs['class'] . ' img-lazyload' : 'img-lazyload'; + $attrs['class'] = $class; + + $style = 'filter: blur(10px); transition: filter 0.3s;'; + if (isset($attrs['style'])) { + $attrs['style'] = rtrim(trim($attrs['style']), ';') . '; ' . $style; + } else { + $attrs['style'] = $style; + } + } catch (Exception $e) {} + } + + if ($broken_fallback) { + $fallback_url = ''; + $req_obj = $registry->get('request'); + $is_https = isset($req_obj->server['HTTPS']) && ($req_obj->server['HTTPS'] == 'on' || $req_obj->server['HTTPS'] == '1'); + $server_url = $is_https ? $config->get('config_ssl') : $config->get('config_url'); + if ($fallback_image && is_file(DIR_IMAGE . $fallback_image)) { + $fallback_url = $server_url . 'image/' . $fallback_image; + } elseif ($config->get('config_logo') && is_file(DIR_IMAGE . $config->get('config_logo'))) { + $fallback_url = $server_url . 'image/' . $config->get('config_logo'); + } else { + $fallback_url = $server_url . 'image/no_image.png'; + } + if ($fallback_url) { + $attrs['onerror'] = "this.src='" . $fallback_url . "'; this.srcset=''; this.onerror=null;"; + } + } + + $new_attrs = array(); + foreach ($attrs as $k => $v) { + $new_attrs[] = $k . '="' . htmlspecialchars($v, ENT_QUOTES, 'UTF-8') . '"'; + } + return ''; + }, $output); + + if ($lazy_load) { + $lazy_script = ' + +'; + $output = str_ireplace('', $lazy_script, $output); + } + } + } + } + echo $output; + ]]> + + + + + + + config->get('module_img_opti_status') && $this->config->get('module_img_opti_broken_fallback') && $this->config->get('module_img_opti_fallback_image')) { + $product_info['image'] = $this->config->get('module_img_opti_fallback_image'); + } + ]]> + + + + + + + url->link('extension/module']]> + load->model('setting/setting'); + if ($this->config->get('module_img_opti_status')) { + $nat_pos = $this->config->get('module_img_opti_menu_position'); + if ($nat_pos == 'sidebar' || $nat_pos == 'both') { + if ($this->user->hasPermission('access', 'module/img_opti')) { + $data['module_img_opti'] = $this->url->link('module/img_opti', 'token=' . $this->session->data['token'], 'SSL'); + } + } + } + ]]> + + + + + + ]]> + +
  • Image Optimizer
  • + + ]]>
    +
    +
    + + + + + config->get('module_img_opti_truefilemanager_webp') || $this->config->get('img_opti_truefilemanager_webp')) ? 'image/webp' : ''),]]> + + + + diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..a5e7399 --- /dev/null +++ b/readme.txt @@ -0,0 +1,104 @@ +================================================================================ +Image Optimizer [NAT]: Compress originals & remove unused images v1.2.0 +================================================================================ + +Thank you for purchasing our extension! +If you have any questions, feel free to contact us: info@nat.od.ua + + +-------------------------------------------------------------------------------- +TERMS & CONDITIONS (License & Support Policy) +-------------------------------------------------------------------------------- +By purchasing and downloading this extension, you strictly agree to the following terms: + +1. Single Domain License: One purchase allows you to install and use the extension on exactly one (1) live domain and one (1) local/test environment. You need a separate License Key for each additional live project. +2. License Activation: The module requires a License Key to function. Please email info@nat.od.ua with your Order ID and Domain Name after purchase to receive your key. +3. Support: We provide free technical support for bugs and standard installation issues. Support is available via email (info@nat.od.ua). Please allow up to 24-48 hours for a response. +4. Compatibility: The module is guaranteed to work on standard OpenCart / ocStore installations. We cannot guarantee out-of-the-box compatibility with heavily customized core files or third-party checkout/theme modules, though we will do our best to assist you. +5. Refund Policy: We offer a 100% refund if the extension is completely non-functional on a standard OpenCart setup and our support team is unable to fix it. We do not issue refunds for "change of mind" or if you have already requested and received a domain license key. +6. Distribution: The code is open-source (no IonCube), meaning you can modify it for your own needs. However, you may not resell, redistribute, share, or publish this extension in any form. + + +-------------------------------------------------------------------------------- +INSTALLATION +-------------------------------------------------------------------------------- +1. Extract the archive. +2. Upload the contents of the `upload/` folder to your OpenCart root directory via FTP (no core files will be overwritten). +3. Go to OpenCart Admin -> System -> Users -> User Groups. Select your group (Administrator) and check both "Access" and "Modify" permissions for `extension/module/img_opti`. +4. Go to Extensions -> Extensions -> Modules. Find "Image Optimizer [NAT]" and click the "Install" (green plus) button. +5. Click the "Edit" button to access the module panel. + + +-------------------------------------------------------------------------------- +HOW TO USE (TABS & FEATURES) +-------------------------------------------------------------------------------- +WARNING: Always create a full backup of your `/image/catalog/` folder before running any optimizations or deletions! + +TAB 1: INFO & STATS +- Disk Usage Chart: Renders a pie chart of folder sizes (Image Catalog, Cache, Trash). Supports manual hosting quota limit overrides and auto-update toggling on page open. +- Cumulative Stats: Displays total optimized files and total disk space saved. + +TAB 2: OPTIMIZE & RESIZE (COMPRESSION) +1. Enter your License Key (Token) in the respective field. +2. Select the target folders inside `image/catalog/` you want to process. +3. Choose the Mode: "Create a copy" (Safest) or "Replace originals". +4. Set your maximum Width/Height and WebP/JPG/PNG quality settings. +5. Click "START OPTIMIZATION". The module will process files in batches using AJAX without overloading your server. + +TAB 3: UNUSED IMAGES CLEANER (JUNK REMOVAL) +1. Select the folders to scan. +2. Click "FIND JUNK". The module compares physical files on disk with your Database and Theme files. +3. Review the detailed log of unused files. +4. Click "To Quarantine" to safely move the junk files to `image/catalog_trash/`, or "Delete permanently" to wipe them from the server. +5. You can easily restore files from Quarantine with one click if something goes wrong. + +TAB 4: WATERMARKS +- Dynamic Overlay rules: Set up watermarks (image or text) to overlay on category, brand, banner, or product images on-the-fly. +- Position, transparency, angle, font (TTF), size scaling, and category exclusion/inclusion filters are fully supported. + +TAB 5: PLACEHOLDERS +- Renders fallback images for products, categories, or brands that do not have an image assigned in the database, keeping the storefront look consistent. + +TAB 6: IMAGE FORMATS (🧪 BETA) +- WebP Support: Enable WebP on-the-fly delivery. +- SVG Support: Safe SVG rendering. +- Progressive Lazy Loading: Blur-up effect (loads micro-preview and fades/de-blurs smoothly as user scrolls). +- Fallback Image: Set global placeholder image for broken images. + +TAB 7: TOOLS (PRO) (🧪 BETA) +- Filename Sanitizer: Clear names of cyrillic letters, spaces, and special symbols in file and directory paths. +- Broken DB Scanner: Finds and fixes database records referencing non-existent files. +- Broken Files Scanner: Scans the server to clean up corrupted or 0-byte images. +- Heavy Files: Find the largest images on disk and optimize them instantly. +- CMYK Converter: Convert color space of JPEG files from CMYK to sRGB for Apple iOS/Safari compatibility. + +TAB 8: CRON (🧪 BETA) +- Configure automated background directory scans and image compression schedules using a Cron token. + + +-------------------------------------------------------------------------------- +UNINSTALLATION +-------------------------------------------------------------------------------- +1. Uninstall the module from the Extensions list in your OpenCart Admin. +2. Delete the module files from: + - admin/controller/extension/module/img_opti.php + - admin/language/*/extension/module/img_opti.php + - admin/view/template/extension/module/img_opti.twig + + +-------------------------------------------------------------------------------- +🔥 CHECK OUT OUR OTHER OPENCART MODULES: +-------------------------------------------------------------------------------- +Maximize your store's performance with our professional toolset: + +• Catalog Optimizer [NAT]: Hide empty categories/brands, clear unused attributes/options. +• Domain Scanner [NAT]: Find and clean external links and images. +• Promo Top Bar [NAT]: Create interactive promo bars, marquees and header slideshows. + +👉 View all our extensions here: +https://www.opencart.com/index.php?route=marketplace/extension&filter_member=nat.odua + +================================================================================ + + +Support info@nat.od.ua | All rights reserved. \ No newline at end of file diff --git a/upload/admin/controller/module/img_opti.php b/upload/admin/controller/module/img_opti.php new file mode 100644 index 0000000..4153c45 --- /dev/null +++ b/upload/admin/controller/module/img_opti.php @@ -0,0 +1,2323 @@ +loadLanguageSafe(); + $this->document->setTitle($this->language->get('heading_title_raw')); + + $this->load->model('setting/setting'); + + if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validate()) { + if (!isset($this->request->post['module_img_opti_cron_folders'])) { + $this->request->post['module_img_opti_cron_folders'] = array(); + } + if (!isset($this->request->post['module_img_opti_cron_exclude_folders'])) { + $this->request->post['module_img_opti_cron_exclude_folders'] = array(); + } + if (!isset($this->request->post['module_img_opti_cron_entities'])) { + $this->request->post['module_img_opti_cron_entities'] = array(); + } + if (!isset($this->request->post['module_img_opti_wm_categories'])) { + $this->request->post['module_img_opti_wm_categories'] = array(); + } + if (!isset($this->request->post['module_img_opti_wm_brands'])) { + $this->request->post['module_img_opti_wm_brands'] = array(); + } + $quality_keys = array('module_img_opti_jpg_quality', 'module_img_opti_png_quality', 'module_img_opti_webp_quality', 'module_img_opti_max_width', 'module_img_opti_max_height', 'module_img_opti_engine'); + $quality_changed = false; + foreach ($quality_keys as $qk) { + $old_val = $this->config->get($qk); + $new_val = isset($this->request->post[$qk]) ? $this->request->post[$qk] : null; + if ((string)$old_val !== (string)$new_val) { + $quality_changed = true; + break; + } + } + $this->model_setting_setting->editSetting('module_img_opti', $this->request->post); + $this->cache->delete('img_opti_disk_stats'); + if ($quality_changed) { + $cache_file = DIR_CACHE . 'img_opti_optimized.json'; + if (file_exists($cache_file)) { + @unlink($cache_file); + } + } + $this->session->data['success'] = $this->language->get('text_success'); + $this->response->redirect($this->url->link('module/img_opti', 'token=' . $this->session->data['token'], true)); + } + + if (isset($this->session->data['success'])) { + $data['success'] = $this->session->data['success']; + unset($this->session->data['success']); + } else { + $data['success'] = ''; + } + + if (isset($this->error['warning'])) { + $data['error_warning'] = $this->error['warning']; + } else { + $data['error_warning'] = ''; + } + + $data['gd_enabled'] = extension_loaded('gd'); + $data['webp_enabled'] = function_exists('imagewebp'); + $data['imagick_enabled'] = extension_loaded('imagick'); + + $data['breadcrumbs'] = array(); + $data['breadcrumbs'][] = array('text' => $this->language->get('text_home'), 'href' => $this->url->link('common/dashboard', 'token=' . $this->session->data['token'], true)); + $extension_route = version_compare(VERSION, '2.3.0.0', '<') ? 'extension/module' : 'extension/extension'; + $extension_link_params = version_compare(VERSION, '2.3.0.0', '<') ? '' : '&type=module'; + + $data['breadcrumbs'][] = array('text' => $this->language->get('text_extension'), 'href' => $this->url->link($extension_route, 'token=' . $this->session->data['token'] . $extension_link_params, true)); + $data['breadcrumbs'][] = array('text' => $this->language->get('heading_title_raw'), 'href' => $this->url->link('module/img_opti', 'token=' . $this->session->data['token'], true)); + + $data['action'] = $this->url->link('module/img_opti', 'token=' . $this->session->data['token'], true); + $data['cancel'] = $this->url->link($extension_route, 'token=' . $this->session->data['token'] . $extension_link_params, true); + + $fields = array( + 'module_img_opti_status' => 1, + 'module_img_opti_disk_status' => 0, + 'module_img_opti_wm_status' => 0, + 'module_img_opti_ph_status' => 0, + 'module_img_opti_wm_rules' => '', + 'module_img_opti_ph_rules' => '', + 'module_img_opti_menu_position' => 'both', + 'module_img_opti_token' => '', + 'module_img_opti_engine' => 'gd', + 'module_img_opti_support_svg' => 0, + 'module_img_opti_support_webp' => 0, + 'module_img_opti_truefilemanager_webp'=> (is_file(DIR_APPLICATION . 'controller/common/truefilemanager.php') || is_file(DIR_SYSTEM . 'library/sitecreator/trueFileManager/ControllerCommonTrueFileManager.php')) ? 1 : 0, + 'module_img_opti_cron_new_only' => 1, + 'module_img_opti_cron_folder_mode' => 'exclude', + 'module_img_opti_cron_folders' => array(), + 'module_img_opti_cron_exclude_folders'=> array(), + 'module_img_opti_cron_entities' => array('product', 'category', 'manufacturer', 'banner', 'blog'), + 'module_img_opti_cron_quality_override'=> 0, + 'module_img_opti_cron_jpg_quality' => 72, + 'module_img_opti_cron_webp_quality' => 80, + 'module_img_opti_cron_max_width' => 1600, + 'module_img_opti_cron_max_height' => 1600, + 'module_img_opti_cron_tasks' => '', + 'module_img_opti_wm_type' => 'image', + 'module_img_opti_wm_image' => '', + 'module_img_opti_wm_text_val' => '', + 'module_img_opti_wm_position' => '5', + 'module_img_opti_wm_opacity' => '50', + 'module_img_opti_wm_categories' => array(), + 'module_img_opti_wm_brands' => array(), + 'module_img_opti_wm_target_product' => 1, + 'module_img_opti_wm_target_category' => 0, + 'module_img_opti_wm_target_brand' => 0, + 'module_img_opti_wm_target_banner' => 0, + 'module_img_opti_wm_target_blog' => 0, + 'module_img_opti_wm_angle' => 0, + 'module_img_opti_wm_corner_radius' => 0, + 'module_img_opti_wm_size_type' => 'original', + 'module_img_opti_wm_size_percent' => 20, + 'module_img_opti_wm_text_color' => '#ffffff', + 'module_img_opti_wm_text_font' => '', + 'module_img_opti_wm_filter_mode' => 'exclude', + 'module_img_opti_wm_max_width' => '800', + 'module_img_opti_wm_max_height' => '800', + 'module_img_opti_min_width' => '300', + 'module_img_opti_min_height' => '300', + 'module_img_opti_ph_product' => '', + 'module_img_opti_ph_category' => '', + 'module_img_opti_ph_brand' => '', + 'module_img_opti_ph_target_product' => 1, + 'module_img_opti_ph_target_category' => 1, + 'module_img_opti_ph_target_brand' => 1, + 'module_img_opti_sz_translit' => 1, + 'module_img_opti_sz_spaces' => 1, + 'module_img_opti_sz_special' => 1, + 'module_img_opti_sz_lowercase_name' => 1, + 'module_img_opti_sz_lowercase_ext' => 1, + 'module_img_opti_sz_normalize_ext' => 1, + 'module_img_opti_webp_on_fly' => 0, + 'module_img_opti_lazy_load' => 0, + 'module_img_opti_broken_fallback' => 0, + 'module_img_opti_fallback_image' => '', + 'module_img_opti_log_level' => 'changed', + 'module_img_opti_min_savings_bytes' => 100, + 'module_img_opti_min_savings_percent' => 0.3, + 'module_img_opti_folder_rules' => array(), + 'module_img_opti_heavy_limit' => 50, + 'module_img_opti_heavy_min_size' => 500, + 'module_img_opti_png_jpg_limit' => 100, + 'module_img_opti_png_jpg_min_size' => 500, + 'module_img_opti_sanitizer_limit' => 100, + 'module_img_opti_disk_quota_total' => 0, + 'module_img_opti_disk_quota_used' => 0 + ); + + foreach ($fields as $key => $default) { + if (isset($this->request->post[$key])) { + $data[$key] = $this->request->post[$key]; + } else { + $data[$key] = ($this->config->get($key) !== null) ? $this->config->get($key) : $default; + } + } + + $data['module_img_opti_wm_rules_json'] = $data['module_img_opti_wm_rules']; + $data['module_img_opti_ph_rules_json'] = $data['module_img_opti_ph_rules']; + $data['module_img_opti_cron_tasks_json'] = $data['module_img_opti_cron_tasks']; + + if (empty($data['module_img_opti_cron_tasks'])) { + $default_cron_tasks = array( + array( + 'status' => 1, + 'name' => 'Default Task', + 'new_only' => (int)$data['module_img_opti_cron_new_only'], + 'entities' => $data['module_img_opti_cron_entities'], + 'folder_mode' => $data['module_img_opti_cron_folder_mode'], + 'folders' => $data['module_img_opti_cron_folders'], + 'quality_override' => (int)$data['module_img_opti_cron_quality_override'], + 'jpg_quality' => (int)$data['module_img_opti_cron_jpg_quality'], + 'webp_quality' => (int)$data['module_img_opti_cron_webp_quality'], + 'max_width' => (int)$data['module_img_opti_cron_max_width'], + 'max_height' => (int)$data['module_img_opti_cron_max_height'] + ) + ); + $data['module_img_opti_cron_tasks_json'] = json_encode($default_cron_tasks, JSON_UNESCAPED_UNICODE); + } + + if (empty($data['module_img_opti_wm_rules'])) { + $default_wm_rules = array(); + if ($data['module_img_opti_wm_type']) { + $categories_data = array(); + $this->load->model('catalog/category'); + foreach ((array)$data['module_img_opti_wm_categories'] as $c_id) { + $c_info = $this->model_catalog_category->getCategory($c_id); + if ($c_info) { + $categories_data[] = array('id' => (int)$c_id, 'name' => $c_info['name']); + } + } + + $brands_data = array(); + $this->load->model('catalog/manufacturer'); + foreach ((array)$data['module_img_opti_wm_brands'] as $b_id) { + $b_info = $this->model_catalog_manufacturer->getManufacturer($b_id); + if ($b_info) { + $brands_data[] = array('id' => (int)$b_id, 'name' => $b_info['name']); + } + } + + $default_wm_rules[] = array( + 'status' => 1, + 'name' => 'Default Rule', + 'type' => $data['module_img_opti_wm_type'], + 'image' => $data['module_img_opti_wm_image'], + 'text_val' => $data['module_img_opti_wm_text_val'], + 'position' => $data['module_img_opti_wm_position'], + 'opacity' => $data['module_img_opti_wm_opacity'], + 'size_type' => $data['module_img_opti_wm_size_type'], + 'size_percent' => $data['module_img_opti_wm_size_percent'], + 'angle' => $data['module_img_opti_wm_angle'], + 'corner_radius' => $data['module_img_opti_wm_corner_radius'], + 'text_font' => $data['module_img_opti_wm_text_font'], + 'text_color' => $data['module_img_opti_wm_text_color'], + 'target_product' => (int)$data['module_img_opti_wm_target_product'], + 'target_category' => (int)$data['module_img_opti_wm_target_category'], + 'target_brand' => (int)$data['module_img_opti_wm_target_brand'], + 'target_banner' => (int)$data['module_img_opti_wm_target_banner'], + 'target_blog' => (int)$data['module_img_opti_wm_target_blog'], + 'filter_mode' => $data['module_img_opti_wm_filter_mode'], + 'categories' => $categories_data, + 'brands' => $brands_data + ); + } + $data['module_img_opti_wm_rules_json'] = json_encode($default_wm_rules); + } + + if (empty($data['module_img_opti_ph_rules'])) { + $default_ph_rules = array(); + if ($data['module_img_opti_ph_product']) { + $default_ph_rules[] = array( + 'status' => 1, + 'name' => 'Products Placeholder', + 'target_product' => 1, + 'target_category' => 0, + 'target_brand' => 0, + 'image' => $data['module_img_opti_ph_product'], + 'filter_mode' => 'exclude', + 'categories' => array(), + 'brands' => array() + ); + } + if ($data['module_img_opti_ph_category']) { + $default_ph_rules[] = array( + 'status' => 1, + 'name' => 'Categories Placeholder', + 'target_product' => 0, + 'target_category' => 1, + 'target_brand' => 0, + 'image' => $data['module_img_opti_ph_category'], + 'filter_mode' => 'exclude', + 'categories' => array(), + 'brands' => array() + ); + } + if ($data['module_img_opti_ph_brand']) { + $default_ph_rules[] = array( + 'status' => 1, + 'name' => 'Brands Placeholder', + 'target_product' => 0, + 'target_category' => 0, + 'target_brand' => 1, + 'image' => $data['module_img_opti_ph_brand'], + 'filter_mode' => 'exclude', + 'categories' => array(), + 'brands' => array() + ); + } + $data['module_img_opti_ph_rules_json'] = json_encode($default_ph_rules); + } + + foreach (array('module_img_opti_cron_folders', 'module_img_opti_cron_exclude_folders', 'module_img_opti_cron_entities') as $arr_key) { + if (isset($data[$arr_key]) && !is_array($data[$arr_key])) { + if (is_string($data[$arr_key])) { + if (strpos($data[$arr_key], '[') === 0 || strpos($data[$arr_key], '{') === 0) { + $decoded = json_decode(html_entity_decode($data[$arr_key], ENT_QUOTES, 'UTF-8'), true); + $data[$arr_key] = is_array($decoded) ? $decoded : array(); + } else { + $data[$arr_key] = explode(',', $data[$arr_key]); + } + } else { + $data[$arr_key] = array(); + } + } + } + + if (isset($data['module_img_opti_folder_rules'])) { + if (!is_array($data['module_img_opti_folder_rules'])) { + $rules = json_decode(html_entity_decode((string)$data['module_img_opti_folder_rules'], ENT_QUOTES, 'UTF-8'), true); + $data['module_img_opti_folder_rules'] = is_array($rules) ? $rules : array(); + } + } else { + $data['module_img_opti_folder_rules'] = array(); + } + + $this->load->model('tool/image'); + $data['placeholder'] = $this->model_tool_image->resize('no_image.png', 100, 100); + + $image_fields = array('wm_image', 'ph_product', 'ph_category', 'ph_brand', 'fallback_image'); + foreach ($image_fields as $img_field) { + $cfg_val = $data['module_img_opti_' . $img_field]; + if (is_string($cfg_val) && is_file(DIR_IMAGE . $cfg_val)) { + $data['thumb_' . $img_field] = $this->model_tool_image->resize($cfg_val, 100, 100); + } else { + $data['thumb_' . $img_field] = $data['placeholder']; + } + } + + $this->load->model('catalog/category'); + $data['wm_categories'] = array(); + if (!empty($data['module_img_opti_wm_categories'])) { + foreach ($data['module_img_opti_wm_categories'] as $category_id) { + $category_info = $this->model_catalog_category->getCategory((int)$category_id); + if ($category_info) { + $data['wm_categories'][] = array( + 'category_id' => $category_info['category_id'], + 'name' => ($category_info['path']) ? $category_info['path'] . ' > ' . $category_info['name'] : $category_info['name'] + ); + } + } + } + + $this->load->model('catalog/manufacturer'); + $data['wm_brands'] = array(); + if (!empty($data['module_img_opti_wm_brands'])) { + foreach ($data['module_img_opti_wm_brands'] as $manufacturer_id) { + $manufacturer_info = $this->model_catalog_manufacturer->getManufacturer((int)$manufacturer_id); + if ($manufacturer_info) { + $data['wm_brands'][] = array( + 'manufacturer_id' => $manufacturer_info['manufacturer_id'], + 'name' => $manufacturer_info['name'] + ); + } + } + } + + $data['token'] = $this->session->data['token']; + $data['catalog_url'] = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + $data['is_licensed'] = $this->checkLicense($data['module_img_opti_token']); + $data['catalog_dir'] = DIR_IMAGE . 'catalog'; + + $data['catalog_items'] = array(); + $data['cron_folders_list'] = array(); + $base_dir = DIR_IMAGE . 'catalog/'; + + if ($data['is_licensed'] && is_dir($base_dir)) { + $data['cron_folders_list'][] = 'catalog'; + $cached_tree = $this->cache->get('img_opti_cron_folders_tree'); + if (is_array($cached_tree)) { + $tree = $cached_tree; + } else { + $tree = $this->getFoldersTree($base_dir, 'catalog'); + $this->cache->set('img_opti_cron_folders_tree', $tree); + } + $data['cron_folders_list'] = array_merge($data['cron_folders_list'], $tree); + + $items = array_diff(scandir($base_dir), array('.', '..')); + foreach ($items as $item) { + if (is_dir($base_dir . $item)) { + $data['catalog_items'][] = array('type' => 'folder', 'name' => $item, 'path' => 'catalog/' . $item); + } + } + } + + if ($this->config->get('module_img_opti_cron_folders') === null && !isset($this->request->post['module_img_opti_cron_folders'])) { + $data['module_img_opti_cron_folders'] = array(); + } + + $open_basedir = ini_get('open_basedir'); + + $data['ttf_fonts'] = array(); + $font_dirs = array( + DIR_SYSTEM . 'library/font/', + DIR_SYSTEM . 'fonts/', + DIR_SYSTEM . 'font/', + DIR_CATALOG . 'view/theme/default/fonts/' + ); + if (DIRECTORY_SEPARATOR === '/') { + if (!$open_basedir) { + $font_dirs[] = '/usr/share/fonts/truetype/dejavu/'; + $font_dirs[] = '/usr/share/fonts/truetype/liberation/'; + $font_dirs[] = '/usr/share/fonts/truetype/freefont/'; + } + } else { + if (!$open_basedir) { + $font_dirs[] = 'C:/Windows/Fonts/'; + } + } + $allowed_dirs = array(); + if ($open_basedir) { + $allowed_dirs = explode(PATH_SEPARATOR, $open_basedir); + } + $font_added = array(); + foreach ($font_dirs as $dir) { + $is_allowed = true; + if ($open_basedir) { + $is_allowed = false; + $norm_dir = rtrim(str_replace('\\', '/', $dir), '/'); + foreach ($allowed_dirs as $allowed) { + $norm_allowed = rtrim(str_replace('\\', '/', $allowed), '/'); + if ($norm_allowed && (strpos($norm_dir, $norm_allowed) === 0 || strpos($norm_allowed, $norm_dir) === 0)) { + $is_allowed = true; + break; + } + } + } + if (!$is_allowed) { + continue; + } + if (@is_dir($dir)) { + $files = @glob($dir . '*.ttf'); + if ($files) { + $count = 0; + foreach ($files as $file) { + $name = $this->mb_basename($file); + if (!in_array($name, $font_added)) { + $data['ttf_fonts'][] = array( + 'name' => $name, + 'path' => $file + ); + $font_added[] = $name; + $count++; + if ($count > 30) break; + } + } + } + } + } + + $data['header'] = $this->load->controller('common/header'); + $data['column_left'] = $this->load->controller('common/column_left'); + $data['footer'] = $this->load->controller('common/footer'); + + if (version_compare(VERSION, '2.2.0.0', '<')) { + $this->response->setOutput($this->load->view('module/img_opti.tpl', $data)); + } else { + $this->response->setOutput($this->load->view('module/img_opti', $data)); + } + } + + public function autocomplete_category() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (isset($this->request->get['filter_name'])) { + $this->load->model('catalog/category'); + + $filter_data = array( + 'filter_name' => $this->request->get['filter_name'], + 'sort' => 'name', + 'order' => 'ASC', + 'start' => 0, + 'limit' => 15 + ); + + $results = $this->model_catalog_category->getCategories($filter_data); + + foreach ($results as $result) { + $json[] = array( + 'category_id' => $result['category_id'], + 'name' => strip_tags(html_entity_decode($result['name'], ENT_QUOTES, 'UTF-8')) + ); + } + } + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + public function autocomplete_manufacturer() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (isset($this->request->get['filter_name'])) { + $this->load->model('catalog/manufacturer'); + + $filter_data = array( + 'filter_name' => $this->request->get['filter_name'], + 'start' => 0, + 'limit' => 15 + ); + + $results = $this->model_catalog_manufacturer->getManufacturers($filter_data); + + foreach ($results as $result) { + $json[] = array( + 'manufacturer_id' => $result['manufacturer_id'], + 'name' => strip_tags(html_entity_decode($result['name'], ENT_QUOTES, 'UTF-8')) + ); + } + } + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + private function validate() { + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $this->error['warning'] = $this->language->get('error_permission'); + } + return !$this->error; + } + + private function getToken() { + if (!empty($this->request->post['module_token'])) { + return trim($this->request->post['module_token']); + } + if (!empty($this->request->post['module_img_opti_token'])) { + return trim($this->request->post['module_img_opti_token']); + } + return $this->config->get('module_img_opti_token'); + } + + public function scan() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->checkLicense($this->getToken())) { + $json['error'] = $this->language->get('error_license') . $this->language->get('error_domain') . $_SERVER['HTTP_HOST'] . ')'; + $this->response->setOutput(json_encode($json)); + return; + } + + session_write_close(); + + $selected_items = isset($this->request->post['selected_items']) ? (array)$this->request->post['selected_items'] : array(); + $allFiles = $this->scanDirectoryHelper($selected_items); + + file_put_contents(DIR_CACHE . 'img_opti_queue.json', json_encode($allFiles, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); + $json['total'] = count($allFiles); + $json['success'] = $this->language->get('text_found') . $json['total']; + + $this->response->setOutput(json_encode($json)); + } + + public function process() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array('log' => array(), 'done' => 0, 'finished' => false, 'batch_old' => 0, 'batch_new' => 0); + $queueFile = DIR_CACHE . 'img_opti_queue.json'; + + if (!$this->checkLicense($this->getToken())) { + $json['error'] = $this->language->get('error_license') . $this->language->get('error_domain') . $_SERVER['HTTP_HOST'] . ')'; + $this->response->setOutput(json_encode($json)); + return; + } + + session_write_close(); + + if (!file_exists($queueFile)) { + $json['finished'] = true; + $this->response->setOutput(json_encode($json)); + return; + } + + $fp = fopen($queueFile, 'c+'); + if (!$fp) { + $json['finished'] = true; + $this->response->setOutput(json_encode($json)); + return; + } + flock($fp, LOCK_EX); + + $filesize = filesize($queueFile); + $queue = array(); + if ($filesize > 0) { + $content = fread($fp, $filesize); + $queue = json_decode($content, true); + } + + if (empty($queue)) { + flock($fp, LOCK_UN); + fclose($fp); + @unlink($queueFile); + $json['finished'] = true; + $this->response->setOutput(json_encode($json)); + return; + } + + $batch = array(); + $batchSize = isset($this->request->post['batch_size']) ? (int)$this->request->post['batch_size'] : 200; + for ($i = 0; $i < $batchSize; $i++) { + if (empty($queue)) break; + $batch[] = array_shift($queue); + } + + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, json_encode($queue, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); + fflush($fp); + flock($fp, LOCK_UN); + fclose($fp); + + $sourceDir = DIR_IMAGE . 'catalog'; + $mode = isset($this->request->post['mode']) ? preg_replace('/[^a-z]/', '', (string)$this->request->post['mode']) : 'copy'; + $copyFolder = !empty($this->request->post['copy_folder']) ? preg_replace('/[^a-zA-Z0-9_-]/', '', $this->request->post['copy_folder']) : 'catalog_new'; + $engine = $this->config->get('module_img_opti_engine') == 'imagick' && extension_loaded('imagick') ? 'imagick' : 'gd'; + $this->load->model('tool/image'); + + if ($mode == 'copy') { + $this->registerModuleFolder($copyFolder); + } + + $targetDir = ($mode == 'copy') ? DIR_IMAGE . $copyFolder : $sourceDir; + + $maxWidth = isset($this->request->post['max_width']) ? (int)$this->request->post['max_width'] : 1600; + $maxHeight = isset($this->request->post['max_height']) ? (int)$this->request->post['max_height'] : 1600; + $jpgQ = isset($this->request->post['jpg_quality']) ? (int)$this->request->post['jpg_quality'] : 72; + $pngQ = isset($this->request->post['png_quality']) ? (int)$this->request->post['png_quality'] : 7; + $webpQ = isset($this->request->post['webp_quality']) ? (int)$this->request->post['webp_quality'] : 80; + $threshold = isset($this->request->post['size_threshold']) ? (float)$this->request->post['size_threshold'] : 0; + $min_savings_bytes = isset($this->request->post['module_img_opti_min_savings_bytes']) ? (int)$this->request->post['module_img_opti_min_savings_bytes'] : 100; + $min_savings_percent = isset($this->request->post['module_img_opti_min_savings_percent']) ? (float)$this->request->post['module_img_opti_min_savings_percent'] : 0.3; + $log_level = isset($this->request->post['module_img_opti_log_level']) ? (string)$this->request->post['module_img_opti_log_level'] : (isset($this->request->post['log_level']) ? (string)$this->request->post['log_level'] : 'changed'); + + $processed = 0; + $batchOptimized = 0; + $batchOldSize = 0; + $batchNewSize = 0; + + $dirImageNorm = rtrim(str_replace('\\', '/', DIR_IMAGE), '/') . '/'; + + $cache_updates = array(); + foreach ($batch as $filePath) { + $realFilePath = realpath($filePath); + if (!$realFilePath || strpos(str_replace('\\', '/', $realFilePath), $dirImageNorm) !== 0 || !is_file($realFilePath)) continue; + + $pathNorm = str_replace('\\', '/', $realFilePath); + $relativePath = preg_replace('/^' . preg_quote($dirImageNorm, '/') . '/i', '', $pathNorm); + $relativePath = ltrim($relativePath, '/'); + + $cleanRel = preg_replace('/^catalog\//i', '', $relativePath); + $cleanRel = ltrim($cleanRel, '/'); + + $targetDirNorm = rtrim(str_replace('\\', '/', $targetDir), '/'); + $destPath = $targetDirNorm . '/' . $cleanRel; + + if (!is_dir($this->mb_dirname($destPath))) { + @mkdir($this->mb_dirname($destPath), 0755, true); + } + + $oldSize = filesize($realFilePath); + $tempPath = DIR_CACHE . 'opti_temp_' . md5($realFilePath); + + $fileParams = $this->getImageParameters($relativePath, array( + 'module_img_opti_folder_rules' => $this->config->get('module_img_opti_folder_rules'), + 'module_img_opti_jpg_quality' => $jpgQ, + 'module_img_opti_png_quality' => $pngQ, + 'module_img_opti_webp_quality' => $webpQ, + 'module_img_opti_max_width' => $maxWidth, + 'module_img_opti_max_height' => $maxHeight + )); + + if ($this->optimizeImage($realFilePath, $tempPath, $fileParams['max_width'], $fileParams['max_height'], $fileParams['jpg_quality'], $fileParams['png_quality'], $fileParams['webp_quality'], $engine)) { + clearstatcache(); + $newSize = filesize($tempPath); + + $increasePercent = ($oldSize > 0) ? (($newSize - $oldSize) / $oldSize) * 100 : 0; + $statusClass = 'text-success'; + $sign = $increasePercent > 0 ? '+' : ''; + $statusText = $this->language->get('text_compressed') . ' (' . $sign . round($increasePercent, 1) . '%)'; + + $file_ok = true; + $savedBytes = $oldSize - $newSize; + $savedPercent = ($oldSize > 0) ? ($savedBytes / $oldSize) * 100 : 0; + $is_too_small_gain = ($savedBytes < $min_savings_bytes || $savedPercent < $min_savings_percent); + + if ($increasePercent > $threshold || $is_too_small_gain) { + @unlink($tempPath); + $copied = true; + if ($mode == 'copy' || $realFilePath !== realpath($destPath)) { + $copied = @copy($realFilePath, $destPath); + } + if (!$copied) { + $file_ok = false; + $statusClass = 'text-danger'; + $statusText = $this->language->get('text_error_process') . ' (Failed to copy original)'; + } else { + $newSize = $oldSize; + $statusClass = 'text-muted'; + $statusText = $this->language->get('text_skipped_no_gain') ? $this->language->get('text_skipped_no_gain') : 'Skipped'; + $cacheKey = $this->getRelativeImagePath($realFilePath); + $cache_updates[$cacheKey] = array( + 'size' => $oldSize, + 'mtime' => @filemtime($realFilePath) + ); + } + } else { + if (file_exists($destPath)) { + @unlink($destPath); + } + $moved = false; + if (@rename($tempPath, $destPath)) { + $moved = true; + } else { + if (@copy($tempPath, $destPath)) { + @unlink($tempPath); + $moved = true; + } + } + if (!$moved) { + $file_ok = false; + $statusClass = 'text-danger'; + $statusText = $this->language->get('text_error_process') . ' (Failed to write optimized file)'; + } else { + clearstatcache(true, $destPath); + $cacheKey = $this->getRelativeImagePath($realFilePath); + $cache_updates[$cacheKey] = array( + 'size' => @filesize($destPath), + 'mtime' => ($mode == 'replace') ? @filemtime($destPath) : @filemtime($realFilePath) + ); + } + } + + if ($file_ok) { + $batchOldSize += $oldSize; + $batchNewSize += $newSize; + } + + $displayPath = $this->safe_utf8($cleanRel); + $isUnchanged = ($statusClass === 'text-muted'); + if ($file_ok && !$isUnchanged) { + $batchOptimized++; + } + if (!$isUnchanged || $log_level === 'all') { + $compareBtn = ''; + if (!$isUnchanged) { + $imgSize = @getimagesize($realFilePath); + if ($imgSize && $imgSize[0] >= 150 && $imgSize[1] >= 150) { + $compareBtn = sprintf( + '', + htmlspecialchars($relativePath, ENT_QUOTES, 'UTF-8'), + $this->translate('text_compare', 'Compare'), + $this->translate('text_compare', 'Compare') + ); + } + } + $json['log'][] = sprintf( + "
    [%s] %s (%s → %s)%s
    ", + $statusClass, + $statusText, + htmlspecialchars('image/catalog/' . $displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8'), + $this->formatBytes($oldSize), + $this->formatBytes($newSize), + $isUnchanged ? '' : $compareBtn + ); + } + } else { + $displayPath = $this->safe_utf8($cleanRel); + $json['log'][] = sprintf( + "
    %s %s
    ", + $this->language->get('text_error_process'), + htmlspecialchars('image/catalog/' . $displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') + ); + } + $processed++; + gc_collect_cycles(); + + $memLimit = $this->getMemoryLimitInBytesHelper(); + if (memory_get_usage(true) > 0.85 * $memLimit) { + break; + } + } + + if (!empty($cache_updates)) { + $cache = $this->getOptimizedCache(); + foreach ($cache_updates as $k => $v) { + $cache[$k] = $v; + } + $this->saveOptimizedCache($cache); + } + + $json['done'] = $processed; + $json['optimized'] = $batchOptimized; + $json['batch_old'] = $batchOldSize; + $json['batch_new'] = $batchNewSize; + + if (!file_exists($queueFile)) { + $json['finished'] = true; + } + + $this->response->setOutput(json_encode($json)); + } + + public function applyChanges() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + $copyFolder = !empty($this->request->post['copy_folder']) ? preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$this->request->post['copy_folder']) : 'catalog_new'; + if (empty($copyFolder) || $copyFolder === 'catalog') { + $json['error'] = $this->language->get('error_invalid_folder'); + $this->response->setOutput(json_encode($json)); + return; + } + + $source_dir = realpath(DIR_IMAGE . $copyFolder); + $target_dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + + if (!$source_dir || strpos($source_dir, $realImageDir) !== 0 || !is_dir($source_dir)) { + $json['error'] = $this->language->get('text_error_source'); + $this->response->setOutput(json_encode($json)); + return; + } + + $backup_name = 'backup_' . date('Ymd_His'); + $backup_dir = DIR_IMAGE . $backup_name; + + $do_backup = isset($this->request->post['backup']) && (int)$this->request->post['backup'] === 1; + + if ($do_backup) { + $this->registerModuleFolder($backup_name); + if (!is_dir($backup_dir)) @mkdir($backup_dir, 0755, true); + $backup_dir = realpath($backup_dir); + } + + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source_dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST); + $errors = array(); + $cache_updates = array(); + + foreach ($files as $file) { + $currentPath = str_replace('\\', '/', $file->getPathname()); + $relative_path = preg_replace('/^' . preg_quote(str_replace('\\', '/', $source_dir), '/') . '/i', '', $currentPath); + $relative_path = ltrim($relative_path, '/'); + + $dest_file = rtrim(str_replace('\\', '/', $target_dir), '/') . '/' . $relative_path; + $dest_folder = $this->mb_dirname($dest_file); + + if ($file->isDir()) { + if (!is_dir($dest_folder)) { + if (!@mkdir($dest_folder, 0755, true)) { + $errors[] = 'Failed to create directory: ' . $relative_path; + } + } + } else { + if (!is_dir($dest_folder)) { + @mkdir($dest_folder, 0755, true); + } + + $replaced = true; + if (file_exists($dest_file)) { + if ($do_backup) { + $back_file = rtrim(str_replace('\\', '/', $backup_dir), '/') . '/' . $relative_path; + if (!is_dir($this->mb_dirname($back_file))) @mkdir($this->mb_dirname($back_file), 0755, true); + if (file_exists($back_file)) { + @unlink($back_file); + } + if (!@rename($dest_file, $back_file)) { + if (!@copy($dest_file, $back_file)) { + $replaced = false; + $errors[] = 'Backup failed: ' . $relative_path; + } else { + @unlink($dest_file); + } + } + } else { + if (!@unlink($dest_file)) { + $replaced = false; + $errors[] = 'Failed to delete original: ' . $relative_path; + } + } + } + + if ($replaced) { + $moved = false; + if (@rename($file->getPathname(), $dest_file)) { + $moved = true; + } else { + if (@copy($file->getPathname(), $dest_file)) { + @unlink($file->getPathname()); + $moved = true; + } + } + if (!$moved) { + $errors[] = 'Failed to apply changes: ' . $relative_path; + } else { + clearstatcache(true, $dest_file); + $cacheKey = $this->getRelativeImagePath($dest_file); + $cache_updates[$cacheKey] = array( + 'size' => @filesize($dest_file), + 'mtime' => @filemtime($dest_file) + ); + } + } + } + } + + if (!empty($cache_updates)) { + $cache = $this->getOptimizedCache(); + foreach ($cache_updates as $k => $v) { + $cache[$k] = $v; + } + $this->saveOptimizedCache($cache); + } + + $this->recursiveDelete($source_dir); + + $msg = $this->language->get('text_apply_success'); + $msg .= $do_backup ? $this->language->get('text_apply_backup_ok') . htmlspecialchars($backup_name, ENT_QUOTES, 'UTF-8') : $this->language->get('text_apply_no_backup'); + + if (!empty($errors)) { + $json['success'] = $msg . '
    ' . $this->language->get('text_apply_errors_warning') . '
    ' . implode('
    ', array_slice($errors, 0, 10)) . (count($errors) > 10 ? '
    ...' : '') . '
    '; + } else { + $json['success'] = $msg; + } + + $this->response->setOutput(json_encode($json)); + } + + public function resetCache() { + $this->loadLanguageSafe(); + $json = array(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + $cache_file = DIR_CACHE . 'img_opti_optimized.json'; + if (file_exists($cache_file)) { + @unlink($cache_file); + } + $json['success'] = $this->translate('text_cache_cleared', 'Optimization cache cleared. Next scan will check all files.'); + $this->response->setOutput(json_encode($json)); + } + + public function clearTrash() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + $query = $this->db->query("SELECT `value` FROM `" . DB_PREFIX . "setting` WHERE `code` = 'module_img_opti' AND `key` = 'module_img_opti_folders'"); + $folders = ($query->num_rows && $query->row['value']) ? json_decode($query->row['value'], true) : array(); + + $folders[] = 'catalog_trash'; + $folders = array_unique($folders); + + $realImageDir = realpath(DIR_IMAGE); + + foreach ($folders as $folder) { + $folder = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', (string)$folder); + if (empty($folder) || $folder === 'catalog') continue; + + $path = realpath(DIR_IMAGE . $folder); + if ($path && strpos($path, $realImageDir) === 0 && is_dir($path)) { + $this->recursiveDelete($path); + } + } + + $compare_path = realpath(DIR_IMAGE . 'cache/compare'); + if ($compare_path && strpos($compare_path, $realImageDir) === 0 && is_dir($compare_path)) { + $this->recursiveDelete($compare_path); + } + $cache_file = DIR_CACHE . 'img_opti_optimized.json'; + if (file_exists($cache_file)) { + @unlink($cache_file); + } + $this->db->query("DELETE FROM `" . DB_PREFIX . "setting` WHERE `code` = 'module_img_opti' AND `key` = 'module_img_opti_folders'"); + + $json['success'] = $this->language->get('text_quarantine_empty'); + + $this->response->setOutput(json_encode($json)); + } + + public function clear_backups() { + $this->loadLanguageSafe(); + $json = array(); + + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + $backup_dir = realpath(DIR_IMAGE . 'catalog_trash/backup'); + $realImageDir = realpath(DIR_IMAGE); + + if ($backup_dir && strpos($backup_dir, $realImageDir) === 0 && is_dir($backup_dir)) { + $this->recursiveDelete($backup_dir); + $json['success'] = $this->language->get('text_backups_cleared'); + } else { + $json['error'] = $this->language->get('error_no_backups'); + } + + $this->response->setOutput(json_encode($json)); + } + + public function download_folder() { + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + return; + } + + $folder = isset($this->request->get['folder']) ? preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$this->request->get['folder']) : ''; + if (empty($folder) || $folder === 'catalog') { + return; + } + + $dir_path = realpath(DIR_IMAGE . $folder); + $realImageDir = realpath(DIR_IMAGE); + + if (!$dir_path || strpos($dir_path, $realImageDir) !== 0 || !is_dir($dir_path)) { + return; + } + + $zip_file = DIR_CACHE . $folder . '_' . date('Ymd_His') . '.zip'; + + $zip = new ZipArchive(); + if ($zip->open($zip_file, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) { + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_path, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST); + foreach ($files as $file) { + if ($file->isFile()) { + $filePath = $file->getPathname(); + $relativePath = substr($filePath, strlen($dir_path) + 1); + $zip->addFile($filePath, $relativePath); + } + } + $zip->close(); + } + + if (file_exists($zip_file)) { + header('Content-Type: application/zip'); + header('Content-Disposition: attachment; filename="' . $this->mb_basename($zip_file) . '"'); + header('Content-Length: ' . filesize($zip_file)); + readfile($zip_file); + @unlink($zip_file); + exit; + } + } + + public function compare_preview() { + $this->loadLanguageSafe(); + $json = array(); + + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + $id = isset($this->request->post['id']) ? str_replace(array('../', '..\\', "\0"), '', (string)$this->request->post['id']) : ''; + $tool = isset($this->request->post['tool']) ? preg_replace('/[^a-zA-Z0-9_]/', '', (string)$this->request->post['tool']) : ''; + + $this->load->model('module/img_opti'); + $json = $this->model_module_img_opti->generateCompareUrls($tool, $id); + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + private function registerModuleFolder($folder_name) { + $folder_name = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', $folder_name); + if (empty($folder_name)) return; + + $query = $this->db->query("SELECT `value` FROM `" . DB_PREFIX . "setting` WHERE `code` = 'module_img_opti' AND `key` = 'module_img_opti_folders'"); + $folders = ($query->num_rows && $query->row['value']) ? json_decode($query->row['value'], true) : array(); + + if (!in_array($folder_name, $folders)) { + $folders[] = $folder_name; + $this->db->query("DELETE FROM `" . DB_PREFIX . "setting` WHERE `code` = 'module_img_opti' AND `key` = 'module_img_opti_folders'"); + $this->db->query("INSERT INTO `" . DB_PREFIX . "setting` SET `store_id` = '0', `code` = 'module_img_opti', `key` = 'module_img_opti_folders', `value` = '" . $this->db->escape(json_encode($folders)) . "'"); + } + } + + private function recursiveDelete($dir) { + $realDir = realpath($dir); + if (!$realDir || strpos($realDir, realpath(DIR_IMAGE)) !== 0 || !is_dir($realDir)) return false; + + $files = array_diff(scandir($realDir), array('.','..')); + foreach ($files as $file) { + $path = $realDir . DIRECTORY_SEPARATOR . $file; + (is_dir($path)) ? $this->recursiveDelete($path) : @unlink($path); + } + return @rmdir($realDir); + } + + private function safe_utf8($str) { + $str = (string)$str; + if (mb_check_encoding($str, 'UTF-8')) { + return $str; + } + $converted = @mb_convert_encoding($str, 'UTF-8', 'Windows-1251'); + return $converted ? $converted : $str; + } + + private function scanDirectoryHelper($selected_items) { + $allFiles = array(); + $scanRootFiles = false; + $dirImageNorm = rtrim(str_replace('\\', '/', DIR_IMAGE), '/'); + $cache = $this->getOptimizedCache(); + + if (empty($selected_items) || in_array('all', (array)$selected_items)) { + $dirsToScan = array($dirImageNorm . '/catalog'); + } else { + $dirsToScan = array(); + if (in_array('root_files', (array)$selected_items)) { + $scanRootFiles = true; + } + foreach ((array)$selected_items as $item) { + if ($item === 'root_files' || $item === 'all') continue; + + $clean_item = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', (string)$item); + $clean_item = str_replace(array('../', '..\\'), '', $clean_item); + + $fullPath = realpath($dirImageNorm . '/' . ltrim($clean_item, '/')); + if ($fullPath && strpos(str_replace('\\', '/', $fullPath), $dirImageNorm) === 0 && is_dir($fullPath)) { + $dirsToScan[] = str_replace('\\', '/', $fullPath); + } + } + } + + if (!empty($dirsToScan)) { + foreach ($dirsToScan as $dir) { + if (!is_dir($dir)) continue; + try { + $directory = new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new RecursiveIteratorIterator($directory); + foreach ($iterator as $file) { + if ($file->isFile()) { + $ext = mb_strtolower($file->getExtension(), 'UTF-8'); + if (in_array($ext, array('jpg', 'jpeg', 'png', 'webp', 'gif'))) { + $filePathNorm = str_replace('\\', '/', $file->getPathname()); + $relPath = $this->getRelativeImagePath($filePathNorm); + $skip = false; + if (isset($cache[$relPath])) { + clearstatcache(true, $filePathNorm); + $size = @filesize($filePathNorm); + $mtime = @filemtime($filePathNorm); + if ($size === $cache[$relPath]['size'] && $mtime === $cache[$relPath]['mtime']) { + $skip = true; + } + } + if (!$skip) { + $allFiles[] = $filePathNorm; + } + } + } + } + } catch (Exception $e) { + continue; + } + } + } + + if ($scanRootFiles) { + $base_dir = $dirImageNorm . '/catalog/'; + if (is_dir($base_dir)) { + $items = scandir($base_dir); + foreach ($items as $item) { + if ($item == '.' || $item == '..') continue; + $fullPath = $base_dir . $item; + if (is_file($fullPath)) { + $ext = mb_strtolower($this->mb_pathinfo($fullPath, PATHINFO_EXTENSION), 'UTF-8'); + if (in_array($ext, array('jpg', 'jpeg', 'png', 'webp', 'gif'))) { + $filePathNorm = str_replace('\\', '/', $fullPath); + $relPath = $this->getRelativeImagePath($filePathNorm); + $skip = false; + if (isset($cache[$relPath])) { + clearstatcache(true, $filePathNorm); + $size = @filesize($filePathNorm); + $mtime = @filemtime($filePathNorm); + if ($size === $cache[$relPath]['size'] && $mtime === $cache[$relPath]['mtime']) { + $skip = true; + } + } + if (!$skip) { + $allFiles[] = $filePathNorm; + } + } + } + } + } + } + return array_unique($allFiles); + } + + private function optimizeImage($src, $dest, $maxWidth, $maxHeight, $jpgQuality, $pngCompression, $webpQuality, $engine = 'gd') { + if ($engine == 'imagick' && extension_loaded('imagick')) { + try { + $img = new Imagick($src); + $img->transformImageColorspace(Imagick::COLORSPACE_SRGB); + + $width = $img->getImageWidth(); + $height = $img->getImageHeight(); + $ext = strtolower($this->mb_pathinfo($src, PATHINFO_EXTENSION)); + + if (($maxWidth > 0 && $width > $maxWidth) || ($maxHeight > 0 && $height > $maxHeight)) { + $targetW = $maxWidth; + $targetH = $maxHeight; + if ($targetW == 0) { + $targetW = (int)round($width * ($targetH / $height)); + } + if ($targetH == 0) { + $targetH = (int)round($height * ($targetW / $width)); + } + $img->resizeImage($targetW, $targetH, Imagick::FILTER_LANCZOS, 1, true); + } + + $img->stripImage(); + + if ($ext === 'png') { + $img->setImageFormat('png'); + $img->setCompressionQuality($pngCompression * 10); + } elseif ($ext === 'webp') { + $img->setImageFormat('webp'); + $img->setCompressionQuality($webpQuality); + } else { + $img->setImageFormat('jpeg'); + $img->setImageCompression(Imagick::COMPRESSION_JPEG); + $img->setCompressionQuality($jpgQuality); + } + + return $img->writeImage($dest); + } catch (Exception $e) { + $engine = 'gd'; + } + } + + if ($engine == 'gd') { + $img = @imagecreatefromstring(file_get_contents($src)); + if (!$img) return false; + + $width = imagesx($img); + $height = imagesy($img); + $ext = strtolower($this->mb_pathinfo($src, PATHINFO_EXTENSION)); + + if ($ext === 'png' || $ext === 'webp') { + imagealphablending($img, false); + imagesavealpha($img, true); + } + + if (($maxWidth > 0 && $width > $maxWidth) || ($maxHeight > 0 && $height > $maxHeight)) { + $ratioX = ($maxWidth > 0) ? ($maxWidth / $width) : 1; + $ratioY = ($maxHeight > 0) ? ($maxHeight / $height) : 1; + $ratio = min($ratioX, $ratioY); + + $newW = (int)($width * $ratio); + $newH = (int)($height * $ratio); + + $newImg = imagecreatetruecolor($newW, $newH); + + if ($ext === 'png' || $ext === 'webp') { + imagealphablending($newImg, false); + imagesavealpha($newImg, true); + $transparent = imagecolorallocatealpha($newImg, 255, 255, 255, 127); + imagefill($newImg, 0, 0, $transparent); + } else { + $white = imagecolorallocate($newImg, 255, 255, 255); + imagefill($newImg, 0, 0, $white); + } + + imagecopyresampled($newImg, $img, 0, 0, 0, 0, $newW, $newH, $width, $height); + imagedestroy($img); + $img = $newImg; + } + + $res = false; + if ($ext === 'png') { + $res = imagepng($img, $dest, $pngCompression); + } elseif ($ext === 'webp' && function_exists('imagewebp')) { + $res = imagewebp($img, $dest, $webpQuality); + } else { + $res = imagejpeg($img, $dest, $jpgQuality); + } + + imagedestroy($img); + return $res; + } + return false; + } + + private function formatBytes($bytes) { + if ($bytes >= 1048576) return round($bytes / 1048576, 2) . ' MB'; + if ($bytes >= 1024) return round($bytes / 1024, 0) . ' KB'; + return $bytes . ' B'; + } + + public function stop() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $queueFile = DIR_CACHE . 'img_opti_queue.json'; + if (file_exists($queueFile)) @unlink($queueFile); + + $json = array('success' => true); + $this->response->setOutput(json_encode($json)); + } + + private function checkLicense($key) { + $host = explode(':', str_replace(array('http://', 'https://', 'www.'), '', strtolower($this->request->server['HTTP_HOST'])))[0]; + if ($host === 'localhost' || $host === '127.0.0.1') return true; + if (empty($key)) return false; + $parts = explode('.', $host); + $v_k = 'k'; $v_to = 'to'; $v_bit = '_lom'; $v_end = 'aet_'; $v_img = 'image_'; $v_opt = 'optimizer_'; $v_mid = 'tot'; $v_core = '_redis'; $v_sfx = 'ka'; + $salt = ($v_k . $v_to . $v_bit) . ($v_end . $v_img . $v_opt . $v_mid) . ($v_core . $v_sfx); + while(count($parts) >= 2) { + $domain = implode('.', $parts); + if ($key === strtoupper(implode('-', str_split(substr(md5($domain . $salt), 0, 16), 4)))) return true; + array_shift($parts); + } + return false; + } + + public function run_tool() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->checkLicense($this->getToken())) { + $json['error'] = $this->language->get('error_license') . $this->language->get('error_domain') . $_SERVER['HTTP_HOST'] . ')'; + $this->response->setOutput(json_encode($json)); + return; + } + + session_write_close(); + + $tool = isset($this->request->post['tool']) ? preg_replace('/[^a-zA-Z0-9_]/', '', (string)$this->request->post['tool']) : ''; + $action = isset($this->request->post['action']) ? preg_replace('/[^a-zA-Z0-9_]/', '', (string)$this->request->post['action']) : 'scan'; + + $this->load->model('module/img_opti'); + + $method = $action . '_' . $tool; + if ($tool === 'broken') { + $method = $action . '_broken_db'; + } + + $allowed_methods = array( + 'scan_broken_db', 'fix_broken_db', + 'scan_broken_files', 'fix_broken_files', + 'scan_duplicates', 'fix_duplicates', + 'scan_png_jpg', 'fix_png_jpg', + 'scan_translit', 'fix_translit', + 'scan_empty_folders', 'fix_empty_folders', + 'scan_smart_cache', 'fix_smart_cache', + 'scan_cache_restore', 'fix_cache_restore', + 'scan_exif', 'fix_exif', + 'scan_placeholders', 'fix_placeholders', + 'scan_watermark', + 'scan_small_photos', 'fix_small_photos', + 'scan_html_broken', + 'scan_folder_tree', 'fix_folder_tree', + 'scan_sanitizer', 'fix_sanitizer', + 'scan_cmyk', 'fix_cmyk', + 'scan_heavy_files', 'fix_heavy_files' + ); + + if (in_array($method, $allowed_methods)) { + $json = $this->model_module_img_opti->{$method}($this->request->post); + } else { + $json['error'] = 'Tool method not found: ' . $method; + } + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + public function upload_font() { + $this->loadLanguageSafe(); + $json = array(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $json['error'] = $this->language->get('error_permission'); + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + return; + } + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + return; + } + if (!empty($this->request->files['file']['name']) && is_file($this->request->files['file']['tmp_name'])) { + $filename = $this->mb_basename(html_entity_decode($this->request->files['file']['name'], ENT_QUOTES, 'UTF-8')); + $filename = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $filename); + $extension = strtolower($this->mb_pathinfo($filename, PATHINFO_EXTENSION)); + if ($extension === 'ttf' && $this->request->files['file']['size'] < 5242880) { + $dir = DIR_SYSTEM . 'library/font/'; + if (!is_dir($dir)) { + @mkdir($dir, 0755, true); + } + if (move_uploaded_file($this->request->files['file']['tmp_name'], $dir . $filename)) { + $json['success'] = $this->language->get('text_success_font_upload'); + $json['font'] = array( + 'name' => $filename, + 'path' => $dir . $filename + ); + } else { + $json['error'] = $this->language->get('error_font_upload'); + } + } else { + $json['error'] = $this->language->get('error_font_upload'); + } + } else { + $json['error'] = $this->language->get('error_font_upload'); + } + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + public function scan_broken() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->checkLicense($this->getToken())) { + $json['error'] = $this->language->get('error_license') . $this->language->get('error_domain') . $_SERVER['HTTP_HOST'] . ')'; + $this->response->setOutput(json_encode($json)); + return; + } + + $this->load->model('module/img_opti'); + + $data = array( + 'start' => isset($this->request->post['start']) ? (int)$this->request->post['start'] : 0, + 'limit' => isset($this->request->post['limit']) ? (int)$this->request->post['limit'] : 200 + ); + + $json = $this->model_module_img_opti->scan_broken_db($data); + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + public function fix_broken() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->checkLicense($this->getToken())) { + $json['error'] = $this->language->get('error_license') . $this->language->get('error_domain') . $_SERVER['HTTP_HOST'] . ')'; + $this->response->setOutput(json_encode($json)); + return; + } + + $this->load->model('module/img_opti'); + + $data = array( + 'ids' => isset($this->request->post['ids']) && is_array($this->request->post['ids']) ? array_map(function($v) { return preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$v); }, $this->request->post['ids']) : array(), + 'fix_all' => isset($this->request->post['fix_all']) ? (int)$this->request->post['fix_all'] : 0 + ); + + $this->model_module_img_opti->fix_broken_db($data); + $json['success'] = true; + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + public function build_index() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + $sysToken = isset($this->request->post['module_token']) ? trim((string)$this->request->post['module_token']) : ''; + if (!$this->checkLicense($sysToken)) { + $json['error'] = $this->language->get('error_token'); + $this->response->setOutput(json_encode($json)); + return; + } + + $whitelist = array(); + $tables = $this->db->query("SHOW TABLES"); + + foreach ($tables->rows as $table_row) { + $table = array_values($table_row)[0]; + $columns = $this->db->query("SHOW COLUMNS FROM `" . $this->db->escape((string)$table) . "`"); + $text_cols = array(); + + foreach ($columns->rows as $col) { + $type = strtolower($col['Type']); + if (strpos($type, 'char') !== false || strpos($type, 'text') !== false) { + $text_cols[] = $col['Field']; + } + } + + if (!empty($text_cols)) { + foreach ($text_cols as $col) { + $query = $this->db->query("SELECT `" . $this->db->escape((string)$col) . "` FROM `" . $this->db->escape((string)$table) . "` WHERE `" . $this->db->escape((string)$col) . "` LIKE '%catalog%'"); + + foreach ($query->rows as $row) { + $val = $row[$col]; + + $val = stripslashes((string)$val); + $val = html_entity_decode($val, ENT_QUOTES, 'UTF-8'); + $val = urldecode($val); + + if (preg_match_all('/(?:image\/)?(catalog\/[^\'"<>\\*|?\n\r\t]+?\.(?:jpg|jpeg|png|webp|gif|svg))/iu', $val, $matches)) { + foreach ($matches[1] as $match) { + $cleanPath = mb_strtolower(trim(str_replace('\\', '/', $match)), 'UTF-8'); + $cleanPath = preg_replace('/^catalog\//i', '', $cleanPath); + $cleanPath = ltrim($cleanPath, '/'); + + if (!isset($whitelist[$cleanPath])) { + $whitelist[$cleanPath] = array(); + } + + $source = 'DB: ' . $table; + if (!in_array($source, $whitelist[$cleanPath])) { + $whitelist[$cleanPath][] = $source; + } + } + } + } + } + } + } + + $theme_dir = realpath(DIR_CATALOG . 'view/theme/'); + if ($theme_dir && is_dir($theme_dir)) { + $directory = new RecursiveDirectoryIterator($theme_dir, RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new RecursiveIteratorIterator($directory); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $ext = strtolower($file->getExtension()); + if (in_array($ext, array('twig', 'tpl', 'css', 'php', 'js'))) { + $content = file_get_contents($file->getRealPath()); + + $content = urldecode($content); + + if (preg_match_all('/(?:image\/)?(catalog\/[^\'"<>\\*|?\n\r\t]+?\.(?:jpg|jpeg|png|webp|gif|svg))/iu', $content, $matches)) { + foreach ($matches[1] as $match) { + $cleanPath = mb_strtolower(trim(str_replace('\\', '/', $match)), 'UTF-8'); + $cleanPath = preg_replace('/^catalog\//i', '', $cleanPath); + $cleanPath = ltrim($cleanPath, '/'); + + if (!isset($whitelist[$cleanPath])) { + $whitelist[$cleanPath] = array(); + } + + $source = 'Theme: ' . $this->mb_basename($file->getFilename()); + if (!in_array($source, $whitelist[$cleanPath])) { + $whitelist[$cleanPath][] = $source; + } + } + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_whitelist.json', json_encode($whitelist, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); + + $json['success'] = true; + $this->response->setOutput(json_encode($json)); + } + + public function scan_trash() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array('log' => array(), 'total' => 0, 'total_bytes' => 0, 'scanned_folders' => ''); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->checkLicense($this->getToken())) { + $json['error'] = $this->language->get('error_license') . $this->language->get('error_domain') . $_SERVER['HTTP_HOST'] . ')'; + $this->response->setOutput(json_encode($json)); + return; + } + + $extendedLog = isset($this->request->post['extended_log']) ? true : false; + $processBroken = isset($this->request->post['process_broken']) ? true : false; + + $whitelist_file = DIR_CACHE . 'img_opti_whitelist.json'; + $whitelist = file_exists($whitelist_file) ? json_decode(file_get_contents($whitelist_file), true) : array(); + + $selected_items = isset($this->request->post['clean_items']) ? (array)$this->request->post['clean_items'] : array(); + + if (empty($selected_items) || in_array('all', $selected_items)) { + $json['scanned_folders'] = $this->language->get('text_all_folders'); + } else { + $folders_str = array(); + foreach ($selected_items as $si) { + if ($si === 'root_files') { + $folders_str[] = $this->language->get('text_root_folder'); + } else { + $clean_si = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', (string)$si); + $folders_str[] = 'image/' . str_replace(array('../', '..\\'), '', $clean_si); + } + } + $json['scanned_folders'] = implode(', ', $folders_str); + } + + $physicalFiles = $this->scanDirectoryHelper($selected_items); + $trashFiles = array(); + $dirImageNorm = rtrim(str_replace('\\', '/', DIR_IMAGE), '/') . '/'; + + foreach ($physicalFiles as $path) { + $realPath = realpath($path); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), $dirImageNorm) !== 0) continue; + + $pathNorm = str_replace('\\', '/', $realPath); + $relPath = preg_replace('/^' . preg_quote($dirImageNorm, '/') . '/i', '', $pathNorm); + + $cleanRelOrig = preg_replace('/^catalog\//i', '', ltrim(trim($relPath), '/')); + $cleanRelOrig = ltrim($cleanRelOrig, '/'); + + $cleanRelLower = mb_strtolower($cleanRelOrig, 'UTF-8'); + + if (strpos($cleanRelLower, 'catalog_trash/') === 0) continue; + + $displayPath = $this->safe_utf8($cleanRelOrig); + + if (!$processBroken && (strpos($displayPath, '?') !== false || strpos($cleanRelOrig, '?') !== false)) { + continue; + } + + if (!isset($whitelist[$cleanRelLower])) { + $trashFiles[] = array( + 'path_full' => $realPath, + 'path_clean' => $cleanRelOrig, + 'path_display' => $displayPath + ); + + $json['total_bytes'] += filesize($realPath); + $json['log'][] = "
    " . $this->language->get('text_log_trash') . " image/catalog/" . htmlspecialchars($displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "
    "; + } else { + if ($extendedLog) { + $sources = implode(', ', $whitelist[$cleanRelLower]); + $json['log'][] = "
    " . $this->language->get('text_log_used') . " image/catalog/" . htmlspecialchars($displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . " (" . $this->language->get('text_used_in') . htmlspecialchars($sources, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . ")
    "; + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_trash_queue.json', json_encode($trashFiles, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); + $json['total'] = count($trashFiles); + $this->response->setOutput(json_encode($json)); + } + + public function process_trash() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array('log' => array(), 'finished' => false); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->checkLicense($this->getToken())) { + $json['error'] = $this->language->get('error_license') . $this->language->get('error_domain') . $_SERVER['HTTP_HOST'] . ')'; + $this->response->setOutput(json_encode($json)); + return; + } + + $queueFile = DIR_CACHE . 'img_opti_trash_queue.json'; + if (!file_exists($queueFile)) { + $json['finished'] = true; + $this->response->setOutput(json_encode($json)); + return; + } + + $queue = json_decode(file_get_contents($queueFile), true); + if (empty($queue)) { + @unlink($queueFile); + $json['finished'] = true; + $this->response->setOutput(json_encode($json)); + return; + } + + $action = isset($this->request->post['action']) ? preg_replace('/[^a-z]/', '', (string)$this->request->post['action']) : 'quarantine'; + $batchSize = 200; + $processed = 0; + + $dirImageNorm = rtrim(str_replace('\\', '/', DIR_IMAGE), '/') . '/'; + + while ($processed < $batchSize && !empty($queue)) { + $item = array_shift($queue); + $filePath = $item['path_full']; + $displayPath = $item['path_display']; + + $realFilePath = realpath($filePath); + + if ($realFilePath && strpos(str_replace('\\', '/', $realFilePath), $dirImageNorm) === 0 && is_file($realFilePath)) { + if ($action === 'delete') { + if (@unlink($realFilePath)) { + $json['log'][] = "
    " . $this->language->get('text_log_deleted') . " image/catalog/" . htmlspecialchars($displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "
    "; + } else { + $json['log'][] = "
    " . $this->language->get('text_log_del_err') . " image/catalog/" . htmlspecialchars($displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "
    "; + } + } else { + $cleanRel = str_replace(array('../', '..\\', "\0"), '', (string)$item['path_clean']); + + $destPath = DIR_IMAGE . 'catalog_trash/' . ltrim($cleanRel, '/'); + $destDir = $this->mb_dirname($destPath); + + if (!is_dir($destDir)) { + @mkdir($destDir, 0755, true); + } + + if (file_exists($destPath)) { + @unlink($destPath); + } + $rename_ok = false; + if (@rename($realFilePath, $destPath)) { + $rename_ok = true; + } else { + if (@copy($realFilePath, $destPath)) { + @unlink($realFilePath); + $rename_ok = true; + } + } + if ($rename_ok) { + $json['log'][] = "
    " . $this->language->get('text_log_quarantine') . " image/catalog_trash/" . htmlspecialchars($displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "
    "; + } else { + $json['log'][] = "
    " . $this->language->get('text_log_q_err') . " image/catalog/" . htmlspecialchars($displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "
    "; + } + } + } + $processed++; + } + + file_put_contents($queueFile, json_encode($queue, JSON_UNESCAPED_UNICODE | JSON_PARTIAL_OUTPUT_ON_ERROR)); + if (empty($queue)) { + @unlink($queueFile); + $json['finished'] = true; + } + $this->response->setOutput(json_encode($json)); + } + + public function restore_quarantine() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array('log' => array(), 'success' => true); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + if (!$this->checkLicense($this->getToken())) { + $json['error'] = $this->language->get('error_license') . $this->language->get('error_domain') . $_SERVER['HTTP_HOST'] . ')'; + $this->response->setOutput(json_encode($json)); + return; + } + + $trash_dir = realpath(DIR_IMAGE . 'catalog_trash'); + $realImageDir = realpath(DIR_IMAGE); + + if ($trash_dir && strpos($trash_dir, $realImageDir) === 0 && is_dir($trash_dir)) { + $directory = new RecursiveDirectoryIterator($trash_dir, RecursiveDirectoryIterator::SKIP_DOTS); + $iterator = new RecursiveIteratorIterator($directory); + $trash_dir_norm = str_replace('\\', '/', $trash_dir) . '/'; + + foreach ($iterator as $file) { + if ($file->isFile()) { + $src = str_replace('\\', '/', $file->getPathname()); + $rel = preg_replace('/^' . preg_quote($trash_dir_norm, '/') . '/i', '', $src); + $cleanRel = str_replace(array('../', '..\\', "\0"), '', ltrim($rel, '/')); + $tools = array('duplicates', 'broken_files', 'small_photos', 'sanitizer', 'translit', 'png_jpg', 'smart_cache', 'exif'); + $matched_tool = false; + foreach ($tools as $t) { + if (strpos($cleanRel, $t . '/') === 0) { + $exactRel = substr($cleanRel, strlen($t . '/')); + $dest = rtrim(str_replace('\\', '/', DIR_IMAGE), '/') . '/' . $exactRel; + $matched_tool = true; + break; + } + } + if (!$matched_tool) { + $dest = rtrim(str_replace('\\', '/', DIR_IMAGE), '/') . '/catalog/' . $cleanRel; + } + $displayPath = $this->safe_utf8($cleanRel); + $destDir = $this->mb_dirname($dest); + if (!is_dir($destDir)) { + @mkdir($destDir, 0755, true); + } + if (file_exists($dest)) { + @unlink($dest); + } + $rename_ok = false; + if (@rename($src, $dest)) { + $rename_ok = true; + } else { + if (@copy($src, $dest)) { + @unlink($src); + $rename_ok = true; + } + } + if ($rename_ok) { + $json['log'][] = "
    " . $this->language->get('text_log_restored') . " image/catalog/" . htmlspecialchars($displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "
    "; + } else { + $json['log'][] = "
    " . $this->language->get('text_log_rest_err') . " image/catalog_trash/" . htmlspecialchars($displayPath, ENT_QUOTES | ENT_IGNORE, 'UTF-8') . "
    "; + } + } + } + $this->recursiveDelete($trash_dir); + } else { + $json['log'][] = "
    " . $this->language->get('text_q_empty_err') . "
    "; + } + + $this->response->setOutput(json_encode($json)); + } + + private function getImageParameters($relativePath, $globalSettings) { + $rules = isset($globalSettings['module_img_opti_folder_rules']) ? (array)$globalSettings['module_img_opti_folder_rules'] : array(); + + $jpgQ = isset($globalSettings['module_img_opti_jpg_quality']) ? (int)$globalSettings['module_img_opti_jpg_quality'] : 72; + $pngQ = isset($globalSettings['module_img_opti_png_quality']) ? (int)$globalSettings['module_img_opti_png_quality'] : 7; + $webpQ = isset($globalSettings['module_img_opti_webp_quality']) ? (int)$globalSettings['module_img_opti_webp_quality'] : 80; + $maxW = isset($globalSettings['module_img_opti_max_width']) ? (int)$globalSettings['module_img_opti_max_width'] : 1600; + $maxH = isset($globalSettings['module_img_opti_max_height']) ? (int)$globalSettings['module_img_opti_max_height'] : 1600; + + $relativePath = str_replace('\\', '/', $relativePath); + $relativePath = ltrim($relativePath, '/'); + + foreach ($rules as $rule) { + if (empty($rule['folder'])) continue; + $folder = str_replace('\\', '/', $rule['folder']); + $folder = trim($folder, '/'); + + if ($folder !== '' && strpos($relativePath, $folder) !== false) { + if (isset($rule['jpg_quality']) && $rule['jpg_quality'] !== '') { + $jpgQ = (int)$rule['jpg_quality']; + } + if (isset($rule['webp_quality']) && $rule['webp_quality'] !== '') { + $webpQ = (int)$rule['webp_quality']; + } + if (isset($rule['max_width']) && $rule['max_width'] !== '') { + $maxW = (int)$rule['max_width']; + } + if (isset($rule['max_height']) && $rule['max_height'] !== '') { + $maxH = (int)$rule['max_height']; + } + break; + } + } + + return array( + 'jpg_quality' => $jpgQ, + 'png_quality' => $pngQ, + 'webp_quality' => $webpQ, + 'max_width' => $maxW, + 'max_height' => $maxH + ); + } + + private function getMemoryLimitInBytesHelper() { + $limit = ini_get('memory_limit'); + if (!$limit || (int)$limit === -1) return 512 * 1024 * 1024; + $unit = strtolower(substr($limit, -1)); + $bytes = (int)$limit; + switch ($unit) { + case 'g': $bytes *= 1024; + case 'm': $bytes *= 1024; + case 'k': $bytes *= 1024; + } + return $bytes; + } + + public function preview_watermark() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || !isset($this->session->data['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + + $json = array(); + + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $json['error'] = $this->language->get('error_permission'); + $this->response->setOutput(json_encode($json)); + return; + } + + session_write_close(); + + $test_image = DIR_IMAGE . 'no_image.png'; + if (!is_file($test_image)) { + $test_image = DIR_IMAGE . 'catalog/no_image.png'; + } + if (!is_file($test_image)) { + $test_image = DIR_CACHE . 'temp_wm_test.png'; + $im = imagecreatetruecolor(500, 500); + $white = imagecolorallocate($im, 240, 240, 240); + imagefill($im, 0, 0, $white); + imagepng($im, $test_image); + imagedestroy($im); + } + + $temp_preview = DIR_IMAGE . 'cache/temp_wm_preview.png'; + $compare_dir = DIR_IMAGE . 'cache/'; + if (!is_dir($compare_dir)) { + @mkdir($compare_dir, 0755, true); + } + + @copy($test_image, $temp_preview); + + $this->config->set('module_img_opti_wm_type', $this->request->post['module_img_opti_wm_type']); + $this->config->set('module_img_opti_wm_image', $this->request->post['module_img_opti_wm_image']); + $this->config->set('module_img_opti_wm_text_val', $this->request->post['module_img_opti_wm_text_val']); + $this->config->set('module_img_opti_wm_position', $this->request->post['module_img_opti_wm_position']); + $this->config->set('module_img_opti_wm_opacity', $this->request->post['module_img_opti_wm_opacity']); + $this->config->set('module_img_opti_wm_angle', isset($this->request->post['module_img_opti_wm_angle']) ? $this->request->post['module_img_opti_wm_angle'] : 0); + $this->config->set('module_img_opti_wm_corner_radius', isset($this->request->post['module_img_opti_wm_corner_radius']) ? $this->request->post['module_img_opti_wm_corner_radius'] : 0); + $this->config->set('module_img_opti_wm_size_type', isset($this->request->post['module_img_opti_wm_size_type']) ? $this->request->post['module_img_opti_wm_size_type'] : 'original'); + $this->config->set('module_img_opti_wm_size_percent', isset($this->request->post['module_img_opti_wm_size_percent']) ? $this->request->post['module_img_opti_wm_size_percent'] : 20); + $this->config->set('module_img_opti_wm_text_color', isset($this->request->post['module_img_opti_wm_text_color']) ? $this->request->post['module_img_opti_wm_text_color'] : '#ffffff'); + $this->config->set('module_img_opti_wm_text_font', isset($this->request->post['module_img_opti_wm_text_font']) ? $this->request->post['module_img_opti_wm_text_font'] : ''); + + $this->config->set('module_img_opti_wm_target_product', 1); + $this->config->set('module_img_opti_wm_target_category', 0); + $this->config->set('module_img_opti_wm_target_brand', 0); + $this->config->set('module_img_opti_wm_target_banner', 0); + $this->config->set('module_img_opti_wm_target_blog', 0); + + $this->load->model('module/img_opti'); + $res = $this->model_module_img_opti->applyDynamicWatermark($temp_preview, '', true); + + if ($res) { + $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + $json['success'] = true; + $json['preview_url'] = $catalog_url . 'image/cache/temp_wm_preview.png?t=' . time(); + } else { + $json['error'] = $this->language->get('error_wm_preview') ?: 'Failed to generate preview'; + } + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + private function getFoldersTree($dir, $base = '') { + $folders = array(); + $realDir = realpath($dir); + if (!$realDir || !is_dir($realDir)) return $folders; + $items = array_diff(scandir($realDir), array('.', '..')); + foreach ($items as $item) { + $path = $realDir . DIRECTORY_SEPARATOR . $item; + if (is_dir($path)) { + $relPath = $base ? $base . '/' . $item : $item; + $folders[] = $relPath; + $folders = array_merge($folders, $this->getFoldersTree($path, $relPath)); + } + } + return $folders; + } + + private function getOptimizedCache() { + $cache_file = DIR_CACHE . 'img_opti_optimized.json'; + if (file_exists($cache_file)) { + $content = @file_get_contents($cache_file); + if ($content) { + $decoded = json_decode($content, true); + if (is_array($decoded)) { + return $decoded; + } + } + } + return array(); + } + + private function saveOptimizedCache($cache) { + $cache_file = DIR_CACHE . 'img_opti_optimized.json'; + @file_put_contents($cache_file, json_encode($cache, JSON_UNESCAPED_UNICODE)); + } + + private function getRelativeImagePath($absolutePath) { + $absolutePath = str_replace('\\', '/', $absolutePath); + $dirImageNorm = str_replace('\\', '/', DIR_IMAGE); + $rel = $absolutePath; + if (substr($absolutePath, 0, strlen($dirImageNorm)) === $dirImageNorm) { + $rel = substr($absolutePath, strlen($dirImageNorm)); + } elseif (strtolower(substr($absolutePath, 0, 3)) === strtolower(substr($dirImageNorm, 0, 3)) && substr($absolutePath, 3) === substr($dirImageNorm, 3)) { + $rel = substr($absolutePath, strlen($dirImageNorm)); + } + $rel = ltrim($rel, '/'); + return $this->safe_utf8($rel); + } + + private function translate($key, $default = '') { + if (isset($this->translations[$key])) { + return $this->translations[$key]; + } + return $default ? $default : $key; + } + + private function loadLanguageSafe() { + $data = array(); + $en_paths = array( + DIR_LANGUAGE . 'en-gb/module/img_opti.php', + DIR_LANGUAGE . 'english/module/img_opti.php' + ); + foreach ($en_paths as $path) { + if (is_file($path)) { + $_ = array(); + require($path); + $data = array_merge($data, $_); + break; + } + } + $lang = $this->config->get('config_admin_language'); + if (!$lang) { + $lang = $this->config->get('config_language'); + } + if (isset($this->session->data['language'])) { + $lang = $this->session->data['language']; + } + if (!$lang) { + $lang = 'en-gb'; + } + if ($lang && $lang !== 'en-gb' && $lang !== 'english') { + $paths = array( + DIR_LANGUAGE . $lang . '/module/img_opti.php' + ); + if (strpos($lang, 'ru') === 0) { + $paths[] = DIR_LANGUAGE . 'ru-ru/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'russian/module/img_opti.php'; + } elseif (strpos($lang, 'uk') === 0 || strpos($lang, 'ua') === 0) { + $paths[] = DIR_LANGUAGE . 'uk-ua/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'ukrainian/module/img_opti.php'; + } + foreach ($paths as $path) { + if (is_file($path)) { + $_ = array(); + require($path); + $data = array_merge($data, $_); + break; + } + } + } + foreach ($data as $key => $value) { + $this->language->set($key, $value); + } + $this->translations = $data; + return $data; + } + + private function mb_dirname($path) { + $path = str_replace('\\', '/', $path); + $last_slash = strrpos($path, '/'); + if ($last_slash !== false) { + return substr($path, 0, $last_slash); + } + return ''; + } + + private function mb_pathinfo($path, $options = null) { + $ret = array('dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''); + $path = str_replace('\\', '/', $path); + $last_slash = strrpos($path, '/'); + if ($last_slash !== false) { + $ret['dirname'] = substr($path, 0, $last_slash); + $basename = substr($path, $last_slash + 1); + } else { + $basename = $path; + } + $ret['basename'] = $basename; + $last_dot = strrpos($basename, '.'); + if ($last_dot !== false) { + $ret['filename'] = substr($basename, 0, $last_dot); + $ret['extension'] = substr($basename, $last_dot + 1); + } else { + $ret['filename'] = $basename; + } + if ($options === PATHINFO_DIRNAME) return $ret['dirname']; + if ($options === PATHINFO_BASENAME) return $ret['basename']; + if ($options === PATHINFO_EXTENSION) return $ret['extension']; + if ($options === PATHINFO_FILENAME) return $ret['filename']; + return $ret; + } + + private function mb_basename($path) { + $path = str_replace('\\', '/', $path); + $parts = explode('/', $path); + return end($parts); + } + + public function get_disk_stats() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => 'Permission denied'))); + return; + } + + session_write_close(); + + if (!$this->config->get('module_img_opti_disk_status')) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('disabled' => true))); + return; + } + + $refresh = isset($this->request->get['refresh']) ? (int)$this->request->get['refresh'] : 0; + + if (!$refresh) { + $cached = $this->cache->get('img_opti_disk_stats'); + if ($cached) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($cached)); + return; + } + } + + $image_dir = rtrim(str_replace('\\', '/', DIR_IMAGE), '/'); + + $quota_total = (float)$this->config->get('module_img_opti_disk_quota_total'); + $quota_used = (float)$this->config->get('module_img_opti_disk_quota_used'); + + if ($quota_total > 0 && $quota_used > 0) { + $disk_total = $quota_total * 1024 * 1024 * 1024; + $disk_used = $quota_used * 1024 * 1024 * 1024; + $disk_free = max(0.0, $disk_total - $disk_used); + } else { + $disk_total = (float)@disk_total_space($image_dir); + $disk_free = (float)@disk_free_space($image_dir); + } + + $json = array( + 'disk_total' => $disk_total, + 'disk_free' => $disk_free, + 'module_trash' => $this->dirSize($image_dir . '/catalog_trash'), + 'oc_cache' => $this->dirSize($image_dir . '/cache'), + 'catalog' => $this->dirSize($image_dir . '/catalog'), + ); + + $this->cache->set('img_opti_disk_stats', $json, 28800); // cache for 8 hours + + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } + + private function dirSize($dir) { + $size = 0; + if (!is_dir($dir)) return 0; + try { + $it = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS) + ); + foreach ($it as $file) { + if ($file->isFile()) $size += $file->getSize(); + } + } catch (Exception $e) {} + return (float)$size; + } + + public function export_settings() { + if (!isset($this->request->get['token']) || $this->request->get['token'] !== $this->session->data['token']) { + die('Permission denied'); + } + $this->load->model('setting/setting'); + $settings = $this->model_setting_setting->getSetting('module_img_opti'); + $export = array( + '_module' => 'img_opti', + '_version' => '1.1.0', + '_exported' => date('Y-m-d H:i:s'), + 'settings' => $settings + ); + $filename = 'img_opti_settings_' . date('Ymd_His') . '.json'; + $this->response->addHeader('Content-Type: application/json'); + $this->response->addHeader('Content-Disposition: attachment; filename="' . $filename . '"'); + $this->response->setOutput(json_encode($export, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } + + public function import_settings() { + $this->loadLanguageSafe(); + if (!isset($this->request->get['token']) || $this->request->get['token'] !== $this->session->data['token']) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + if (!$this->user->hasPermission('modify', 'module/img_opti')) { + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode(array('error' => $this->language->get('error_permission')))); + return; + } + $json = array(); + if (!isset($_FILES['import_json']) || $_FILES['import_json']['error'] !== UPLOAD_ERR_OK) { + $json['error'] = $this->language->get('error_import_file'); + } else { + $content = @file_get_contents($_FILES['import_json']['tmp_name']); + $data = json_decode($content, true); + if (!$data || !isset($data['settings']) || !isset($data['_module']) || $data['_module'] !== 'img_opti') { + $json['error'] = $this->language->get('error_import_invalid'); + } else { + $this->load->model('setting/setting'); + $safe = array(); + foreach ($data['settings'] as $key => $value) { + if (strpos($key, 'module_img_opti') === 0) { + $safe[$key] = $value; + } + } + $this->model_setting_setting->editSetting('module_img_opti', $safe); + $this->cache->delete('img_opti_disk_stats'); + $json['success'] = true; + } + } + $this->response->addHeader('Content-Type: application/json'); + $this->response->setOutput(json_encode($json)); + } +} \ No newline at end of file diff --git a/upload/admin/language/en-gb/module/img_opti.php b/upload/admin/language/en-gb/module/img_opti.php new file mode 100644 index 0000000..16f7e4e --- /dev/null +++ b/upload/admin/language/en-gb/module/img_opti.php @@ -0,0 +1,565 @@ +Image Optimizer [NAT]:
    Compress Originals & Delete Junk'; +$_['text_extension'] = 'Extensions'; +$_['text_edit'] = 'Module Settings'; +$_['text_success'] = 'Settings have been successfully saved!'; +$_['entry_warning'] = 'WARNING: Always make a backup of your files, especially the /image/catalog folder before starting!'; +$_['text_tech'] = 'Server Technology Check:'; +$_['text_author'] = 'Technical support and suggestions: info@nat.od.ua'; +$_['tab_optimize'] = 'Optimization & Resize'; +$_['tab_cleaner'] = 'Clean Unused Images'; +$_['tab_info'] = 'Info / Ecosystem [NAT]'; +$_['tab_settings'] = 'Settings'; +$_['tab_tools'] = 'Tools (pro)'; +$_['tab_cron'] = 'Cron Jobs'; +$_['entry_mode'] = 'Operation Mode'; +$_['text_copy'] = 'Create a copy (Safe)'; +$_['text_replace'] = 'Replace originals (Requires backup!)'; +$_['entry_folder'] = 'Folder name for copy'; +$_['entry_batch'] = 'Files per batch'; +$_['entry_log_limit'] = 'Log lines per page'; +$_['entry_max_width'] = 'Max width (px) [0 - no limit]'; +$_['entry_max_height'] = 'Max height (px) [0 - no limit]'; +$_['entry_jpg'] = 'JPG Quality (0-100)'; +$_['entry_png'] = 'PNG Compression (0-9)'; +$_['entry_webp'] = 'WebP Quality (0-100)'; +$_['entry_targets'] = 'What to optimize?'; +$_['text_all_catalog'] = '[ Entire image/catalog ]'; +$_['text_root_files'] = 'Files only in image/catalog root'; +$_['entry_threshold'] = 'Replacement threshold (%)'; +$_['help_threshold'] = '0: replace only if file is smaller. 10: allow up to 10% file size increase.'; +$_['entry_token'] = 'Activation Token (Root Domain)'; +$_['entry_status'] = 'Module Status'; +$_['entry_menu_position'] = 'Menu Position [NAT]'; +$_['text_menu_module_only'] = 'Modules List Only'; +$_['text_menu_header'] = 'Top Header'; +$_['text_menu_sidebar'] = 'Sidebar Menu'; +$_['text_menu_both'] = 'Both (Header + Sidebar)'; +$_['text_status_on'] = 'Enabled'; +$_['text_status_off'] = 'Disabled'; +$_['entry_engine'] = 'Image Processing Engine'; +$_['text_engine_gd'] = 'GD (Standard)'; +$_['text_engine_imagick'] = 'Imagick (High quality, sRGB profile keeping)'; +$_['entry_license'] = 'License'; +$_['text_cleaner_info'] = 'This tool looks for files that physically exist on the server but are not linked in the database or template files.'; +$_['entry_clean_targets'] = 'Folders to scan for junk?'; +$_['entry_extended_log'] = 'Extended log (show used photos and their locations)'; +$_['entry_process_broken'] = 'Include files with broken encoding ("?" in names)'; +$_['button_scan_trash'] = 'FIND JUNK FILES'; +$_['button_quarantine'] = 'Move to Quarantine (Safe)'; +$_['button_delete_trash'] = 'Delete Permanently'; +$_['button_empty_quarantine'] = 'Empty Quarantine Folder'; +$_['button_restore_quarantine'] = 'Restore from Quarantine'; +$_['text_scan_result'] = 'Scan Results'; +$_['text_trash_files'] = 'Junk files found: '; +$_['text_trash_size'] = 'Potential space to free up: '; +$_['button_start'] = 'START OPTIMIZATION'; +$_['button_pause'] = 'PAUSE'; +$_['button_stop'] = 'STOP'; +$_['button_continue'] = 'CONTINUE'; +$_['button_start_new'] = 'START (New Run)'; +$_['button_download_log'] = 'Download TXT Log'; +$_['button_download'] = 'Download'; +$_['button_save'] = 'Save'; +$_['button_cancel'] = 'Cancel'; +$_['text_progress'] = 'Progress:'; +$_['text_old_size'] = 'Before:'; +$_['text_new_size'] = 'After:'; +$_['text_savings'] = 'Saved:'; +$_['text_detailed_log'] = 'Detailed Log'; +$_['text_page'] = 'Page'; +$_['text_waiting'] = 'Waiting to start...'; +$_['text_enabled'] = 'Enabled'; +$_['text_disabled_error'] = 'Disabled (ERROR)'; +$_['text_supported'] = 'Supported'; +$_['text_no'] = 'No'; +$_['error_permission'] = 'You do not have permission to modify this module!'; +$_['error_license'] = 'Access Error: Invalid activation token for this domain!'; +$_['error_domain'] = ' (Domain: '; +$_['text_found'] = 'Files found: '; +$_['text_compressed'] = 'Compressed'; +$_['text_original'] = 'Original'; +$_['text_error_process'] = 'Processing error: '; +$_['error_log_empty'] = 'Log is empty! Run the process first.'; +$_['text_report_header'] = '=== REPORT ==='; +$_['text_total_files'] = 'Total files: '; +$_['text_searching'] = 'Searching for files...'; +$_['error_ajax'] = 'Error: '; +$_['text_resumed'] = '▶ PROCESS RESUMED...'; +$_['text_paused'] = '⏸ PAUSED. Process suspended.'; +$_['confirm_stop'] = 'Are you sure you want to completely abort the process?'; +$_['confirm_delete'] = 'WARNING! Files will be permanently deleted from the server with no chance of recovery! Continue?'; +$_['confirm_empty_q'] = 'Are you sure you want to completely empty the quarantine folder?'; +$_['confirm_restore'] = 'All files from quarantine will be returned to their original locations. Continue?'; +$_['button_stopping'] = 'Stopping...'; +$_['text_stopped_user'] = '🛑 PROCESS ABORTED BY USER!'; +$_['text_increase'] = 'Increased'; +$_['text_done'] = '✅ DONE! Process completed.'; +$_['error_timeout'] = 'Server timeout. Auto-retrying in 3 seconds...'; +$_['text_building_index'] = 'Step 1/2: Indexing DB and templates (searching for used photos)...'; +$_['text_comparing'] = 'Step 2/2: Comparing physical files with the index...'; +$_['text_quarantine_done'] = '✅ Quarantined to image/catalog_trash/ successfully!'; +$_['text_deleted_done'] = '✅ Junk deletion completed successfully!'; +$_['text_quarantine_empty'] = '✅ Quarantine folder emptied successfully!'; +$_['text_restore_done'] = '✅ Files restored from quarantine to their original locations!'; +$_['text_quarantine_move'] = 'Quarantined: '; +$_['text_deleted_file'] = 'Deleted: '; +$_['text_no_trash'] = 'No junk found in selected folders! All files are in use.'; +$_['text_scanning_dirs'] = 'Scanning directories: '; +$_['text_log_used'] = '[IN USE]'; +$_['text_log_trash'] = '[JUNK]'; +$_['text_log_deleted'] = '[DELETED]'; +$_['text_log_del_err'] = '[DELETE ERROR]'; +$_['text_log_quarantine'] = '[QUARANTINED]'; +$_['text_log_q_err'] = '[MOVE ERROR]'; +$_['text_log_restored'] = '[RESTORED]'; +$_['text_log_rest_err'] = '[RESTORE ERROR]'; +$_['text_used_in'] = 'in: '; +$_['text_analyzing'] = 'Analyzing database...'; +$_['text_clean_empty'] = 'Clean! No junk found.'; +$_['text_processing'] = 'Processing...'; +$_['text_restoring'] = 'Restoring files...'; +$_['text_all_folders'] = 'image/catalog/ (And all subfolders)'; +$_['text_root_folder'] = 'image/catalog/ (Root only)'; +$_['text_q_empty_err'] = 'Quarantine folder is empty or does not exist.'; +$_['error_token'] = 'Invalid token. Please refresh the page.'; +$_['text_apply_title'] = 'Optimization completed in folder: '; +$_['text_apply_info'] = 'Check the result. If everything looks good, apply changes to the main catalog.'; +$_['entry_apply_backup'] = 'Move and create a backup (in Quarantine)'; +$_['entry_apply_replace'] = 'Replace originals permanently'; +$_['button_apply_main'] = 'APPLY TO MAIN CATALOG'; +$_['button_delete_copies'] = 'Delete temporary copies (Cancel)'; +$_['button_full_cleanup'] = 'Full cleanup of all temp folders and backups'; +$_['text_apply_success'] = '✅ Files successfully moved. '; +$_['text_apply_backup_ok'] = 'Originals saved in folder: '; +$_['text_apply_no_backup'] = 'Originals deleted.'; +$_['text_error_source'] = 'Error: Folder with optimized files not found.'; +$_['confirm_full_cleanup'] = 'WARNING! This action will permanently delete ALL backup folders (backup_*) and temp copy folders. Continue?'; +$_['text_tools_desc'] = 'Global scanners and tools for file system and database image optimization.'; +$_['text_tool_select'] = 'Select Tool:'; +$_['text_group_scanners'] = 'Scanners & Analysis'; +$_['text_group_generators'] = 'Generators & Processing'; +$_['text_group_experimental'] = 'Experimental Features (BETA)'; +$_['text_experimental_warn'] = 'Warning: Experimental features modify system files or the database. Backup is highly recommended!'; +$_['text_tool_broken_db'] = 'Broken/Empty DB Image Scanner'; +$_['text_tool_watermark'] = 'Dynamic Watermark (Cache only)'; +$_['text_tool_duplicates'] = 'Duplicate Finder (MD5)'; +$_['text_tool_png_jpg'] = 'Heavy PNG to JPG Converter (non-transparent PNG scanner)'; +$_['text_tool_placeholders'] = 'Placeholder Generator (Replaces No Image)'; +$_['text_tool_exif'] = 'Clean EXIF Data (Geotags/Meta)'; +$_['text_tool_small_photos'] = 'Too Small Photo Scanner'; +$_['text_tool_html_broken'] = 'Broken Images in HTML'; +$_['text_tool_empty_folders'] = 'Remove Empty Folders (image/catalog)'; +$_['text_tool_smart_cache'] = 'Smart Cache Cleanup'; +$_['text_tool_folder_tree'] = 'Folder Tree Statistics'; +$_['text_tool_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load (No DB)'; +$_['text_tool_translit'] = 'File Name Transliteration'; +$_['text_tool_cache_restore'] = 'Restore Originals from Cache'; +$_['entry_wm_type'] = 'Watermark Type'; +$_['text_wm_image'] = 'Image (PNG)'; +$_['text_wm_text'] = 'Text'; +$_['entry_wm_image_path'] = 'Image Path'; +$_['entry_wm_text_val'] = 'Watermark Text'; +$_['entry_wm_position'] = 'Position (1-9)'; +$_['entry_wm_opacity'] = 'Opacity (0-100)'; +$_['entry_wm_category'] = 'Only for Categories'; +$_['entry_wm_brand'] = 'Only for Brands'; +$_['entry_min_width'] = 'Minimum Width (px)'; +$_['entry_min_height'] = 'Minimum Height (px)'; +$_['entry_ph_product'] = 'Placeholder for Products'; +$_['entry_ph_category'] = 'Placeholder for Categories'; +$_['entry_ph_brand'] = 'Placeholder for Brands'; +$_['button_run_tool'] = 'Run Tool'; +$_['button_scan_tool'] = 'Scan'; +$_['button_fix_tool'] = 'Fix Found'; +$_['text_th_path'] = 'File / Path'; +$_['text_th_size'] = 'Size'; +$_['text_th_action'] = 'Action'; +$_['text_th_status'] = 'Status'; +$_['text_th_problem'] = 'Problem / Value'; +$_['text_cron_title'] = 'Cron Settings'; +$_['text_cron_desc'] = 'Add the command to your hosting Cron for automatic background compression.'; +$_['entry_cron_new_only'] = 'Process only new (unprocessed) files'; +$_['entry_cron_folders'] = 'Folders to scan (leave empty for entire catalog)'; +$_['text_promo_title'] = 'Other [NAT] Series Modules'; +$_['text_promo_desc'] = '[NAT] Series Modules are tools for deep OpenCart optimization. We focus on automation, DB speed, and server cleanup. All modules are open-source and share the same core logic.'; +$_['text_support'] = 'Technical support and suggestions:'; +$_['button_more'] = 'View all extensions on OpenCart.com'; +$_['button_more_forum'] = 'Opencartforum'; +$_['text_mod_cat_opt'] = 'Catalog Optimizer [NAT]'; +$_['text_mod_cat_opt_desc'] = 'Hide empty categories/brands + SEO sorting (in stock first) + DB junk cleanup.'; +$_['text_mod_new_arrivals'] = 'New Arrivals [NAT]: Sort by exact date'; +$_['text_mod_new_arrivals_desc'] = 'Automated SEO page for new arrivals. Displays items strictly by the real date added. Sliders, grid, and smart date archive sidebar.'; +$_['text_mod_img_opt'] = 'Image Optimizer [NAT]'; +$_['text_mod_img_opt_desc'] = 'Current Module. Compress and resize ORIGINAL photos (image/catalog) to WebP + Server cleanup of unused images.'; +$_['text_mod_dom_scan'] = 'Domain Scanner [NAT]'; +$_['text_mod_dom_scan_desc'] = 'Find and clean external links and HTTP images in DB and theme files. Ideal for HTTPS migration or cleanup after parsing.'; +$_['text_mod_poll'] = 'Poll Widget [NAT]'; +$_['text_mod_poll_desc'] = 'Create polls and voting widgets with a visual design builder, deep statistics, and cheat protection.'; +$_['text_mod_db_opt'] = 'Database Optimizer [NAT]'; +$_['text_mod_db_opt_desc'] = 'Intelligent garbage cleanup in the database, system log management, and crucial index addition for speed boost.'; +$_['text_mod_im_ex'] = 'Import/Export [NAT]'; +$_['text_mod_im_ex_desc'] = 'Import/export prices and stock levels from/to files (xls, xlsx, csv, xml, json) or by url. Field mapping, Cron automation, text utilities.'; +$_['text_mod_admin_tools'] = 'Admin Tools [NAT]'; +$_['text_mod_admin_tools_desc'] = 'Handy administrator tools: login as customer, admin customization, user permissions management, and hiding unnecessary menu items.'; +$_['text_mod_utm_tracker'] = 'Order UTM Tracker [NAT]'; +$_['text_mod_utm_tracker_desc'] = 'Automatic tracking of UTM tags during checkout. Saves the traffic source, campaign, keywords, and displays this data in order details to analyze advertising effectiveness.'; +$_['text_info_title_promo_top_bar'] = 'Promo Top Bar [NAT]: Upper promotional info-banner, information bar above header, marquee and top-banner'; +$_['text_info_desc_promo_top_bar'] = 'Create interactive promo bars and header slideshows with a block builder, countdown timers, subscription forms, and flexible targeting.'; +$_['text_tool_no_image'] = 'Products without main image (empty in DB)'; +$_['text_problem_main_broken'] = 'Broken main image (file deleted)'; +$_['text_problem_add_broken'] = 'Broken additional image (file deleted)'; +$_['text_problem_no_image'] = 'Missing main image'; +$_['text_err_dir'] = 'Error: Directory not found or invalid path.'; +$_['text_err_no_queue'] = 'Error: No queue found. Run scan first.'; +$_['text_log_del_empty_folder'] = 'Deleted empty folder: '; +$_['text_log_no_empty_folders'] = 'No empty folders found in image/catalog/.'; +$_['text_log_total_empty_folders'] = 'Total empty folders removed: '; +$_['text_log_cache_cleared'] = 'Image cache has been cleared.'; +$_['text_log_wm_smart_mode'] = 'Watermarks are operating in SMART mode: originals remain untouched, applied only on cache generation.'; +$_['text_log_scan_dupes'] = 'Scanning image/catalog/ for MD5 duplicates... This may take a while.'; +$_['text_log_found_dupe'] = 'Found duplicate: '; +$_['text_log_no_dupes'] = 'No duplicates found.'; +$_['text_log_total_dupes'] = 'Found duplicates: %s. Potential savings: %s'; +$_['text_log_fix_dupes_prompt'] = 'Click "Fix Found" to redirect DB links to original and delete duplicates.'; +$_['text_log_fixed_dupe'] = 'Fixed and removed duplicate: '; +$_['text_log_success_dupes'] = 'Successfully fixed %s duplicates. DB updated.'; +$_['text_log_scan_png'] = 'Scanning for heavy PNG files without transparency...'; +$_['text_log_no_png'] = 'No suitable PNG files found.'; +$_['text_log_total_png'] = 'Found %s PNG without transparency. Click "Fix Found" to convert to JPG.'; +$_['text_log_converted_jpg'] = 'Converted to JPG: '; +$_['text_log_success_png'] = 'Successfully converted %s files. Space saved: %s'; +$_['text_log_ph_prod'] = 'Updated %s Products with placeholder.'; +$_['text_log_ph_cat'] = 'Updated %s Categories with placeholder.'; +$_['text_log_ph_brand'] = 'Updated %s Brands with placeholder.'; +$_['text_log_ph_empty'] = 'No placeholders selected in settings.'; +$_['text_log_scan_exif'] = 'Scanning for EXIF data...'; +$_['text_log_stripped_exif'] = 'Stripped EXIF: '; +$_['text_log_no_exif'] = 'No EXIF data found to clean.'; +$_['text_log_success_exif'] = 'Cleaned EXIF from %s files. Saved: %s'; +$_['text_log_scan_small'] = 'Scanning for photos smaller than %sx%s px...'; +$_['text_log_too_small'] = 'Too small (%sx%s): '; +$_['text_log_no_small'] = 'No small photos found.'; +$_['text_log_total_small'] = 'Found %s small photos. Please review and re-upload.'; +$_['text_log_html_broken'] = 'Broken IMG in HTML table %s (ID: %s): '; +$_['text_log_no_html_broken'] = 'No broken images found in HTML descriptions.'; +$_['text_log_total_html_broken'] = 'Found %s broken links in HTML. Please edit them manually.'; +$_['text_log_scan_smart_cache'] = 'Scanning image/cache/ for orphaned files...'; +$_['text_log_del_orph_cache'] = 'Deleted orphaned cache: '; +$_['text_log_no_orph_cache'] = 'No orphaned cache files found.'; +$_['text_log_success_smart_cache'] = 'Removed %s orphaned cache files. Space saved: %s'; +$_['text_log_scan_translit'] = 'Scanning for files with cyrillic or spaces...'; +$_['text_log_no_bad_names'] = 'No files with bad names found.'; +$_['text_log_total_bad_names'] = 'Found %s bad files. Click "Fix Found" to transliterate and update DB.'; +$_['text_log_renamed'] = 'Renamed: '; +$_['text_log_success_translit'] = 'Successfully transliterated %s files and updated DB.'; +$_['text_log_scan_restore'] = 'Scanning image/cache/ to restore lost originals...'; +$_['text_log_restored_cache'] = 'Restored from cache: '; +$_['text_log_no_lost_orig'] = 'No missing originals found in cache.'; +$_['text_log_success_restore'] = 'Successfully restored %s original images from cache.'; +$_['error_invalid_folder'] = 'Error: Invalid folder name!'; +$_['text_formats_title'] = 'Modern Formats (WebP & SVG)'; +$_['text_formats_desc'] = 'Settings for integrating modern formats into the OpenCart file system and templates.'; +$_['entry_support_svg'] = 'SVG Upload Support'; +$_['help_support_svg'] = 'Allows uploading vector SVG images via the standard Filemanager and safely displaying them on the storefront without size distortion.'; +$_['entry_support_webp'] = 'WebP Upload Support'; +$_['help_support_webp'] = 'Allows manually uploading, linking, and displaying ready .webp files on the website via the Filemanager.'; +$_['entry_truefilemanager_webp'] = 'WebP in TrueFileManager'; +$_['help_truefilemanager_webp'] = 'Adds WebP upload, preview, and thumbnail support to sitecreator\'s TrueFileManager (if installed).'; +$_['entry_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load'; +$_['help_webp_on_fly'] = 'Ultra-fast replacement: automatically changes .jpg/.png extensions to .webp directly in the storefront HTML output and adds the loading="lazy" attribute. Does not touch the database!'; +$_['help_tool_broken_db'] = 'Finds images in the database whose physical files have been deleted from the server. Clears broken links.'; +$_['help_tool_empty_folders'] = 'Recursively scans image/catalog/ and safely removes all folders that contain no files.'; +$_['help_tool_folder_tree'] = 'Builds a directory tree and shows the real size of each folder to find space eaters.'; +$_['help_tool_html_broken'] = 'Scans HTML descriptions of products and categories for tags with non-existent images.'; +$_['help_tool_small_photos'] = 'Finds pictures with resolution below the specified one. Added search links to Google and Yandex Images so you can easily find high-quality replacements.'; +$_['help_tool_duplicates'] = 'Searches for exact duplicates by MD5 hash (different names, same image). Merges them into one file and updates the DB.'; +$_['help_tool_watermark'] = 'Applies watermarks in SMART mode (only to storefront cache, originals remain unchanged).'; +$_['help_tool_placeholders'] = 'Massively sets the selected placeholder image for products and categories that have no photo.'; +$_['help_tool_png_jpg'] = 'Scans for heavy PNG files without transparency (alpha channel). Since they do not need transparency, they can be safely converted to lightweight JPG. This greatly reduces file size (typically by 70-80%) and speeds up your website, automatically updating all links in the database.'; +$_['help_tool_exif'] = 'Scans JPEG files and removes hidden EXIF data (geotags, camera data), reducing size by 10-15%.'; +$_['help_tool_smart_cache'] = 'Finds cached files in image/cache/ whose originals have already been deleted, and removes this trash.'; +$_['help_tool_translit'] = 'Finds files with Cyrillic or spaces, renames them using transliteration, and updates paths in the DB.'; +$_['help_tool_cache_restore'] = 'If you accidentally deleted an original from catalog, this tool will try to find its copy in cache and restore it.'; +$_['entry_quarantine'] = 'Move originals to Quarantine (instead of deleting)'; +$_['entry_backup_db'] = 'Create database backup of modified tables in SQL before starting'; +$_['button_fix_selected'] = 'FIX SELECTED'; +$_['button_export_log'] = 'Export to CSV'; +$_['text_th_before'] = 'State BEFORE'; +$_['text_th_after'] = 'Expected result AFTER'; +$_['text_th_select'] = 'Action'; +$_['text_th_preview'] = 'Preview'; +$_['text_th_name'] = 'Object / File'; +$_['text_global_action'] = 'Bulk action for selected:'; +$_['text_action_clear'] = 'Clear DB Link'; +$_['text_action_disable'] = 'Disable (Status = 0)'; +$_['text_action_stock'] = 'Set Out of Stock'; +$_['text_action_placeholder'] = 'Set Placeholder'; +$_['text_action_ignore'] = 'Do nothing'; +$_['text_action_merge'] = 'Merge (delete duplicate, keep original)'; +$_['text_action_convert'] = 'Convert to JPG'; +$_['text_action_rename'] = 'Rename to translit'; +$_['text_action_delete'] = 'Delete'; +$_['text_action_restore'] = 'Restore'; +$_['text_db_backup_created'] = 'Database backup created: %s'; +$_['text_quarantine_created'] = 'Quarantined to: %s'; +$_['text_fix_status_success'] = 'Successfully executed'; +$_['text_fix_status_error'] = 'Error: %s'; +$_['text_log_exported'] = 'Log successfully exported!'; +$_['text_select_action'] = '-- Select Action --'; +$_['error_empty_selection'] = 'Error: No rows selected for fixing!'; +$_['text_link_admin'] = 'Admin'; +$_['text_link_catalog'] = 'Store'; +$_['entry_threads'] = 'Processing Threads'; +$_['text_threads_optimal'] = 'optimal'; +$_['button_download_zip'] = 'Download ZIP and Delete Folder'; +$_['confirm_download_and_delete'] = 'Did the ZIP download successfully? Can we delete the temporary copies folder on the server?'; +$_['button_clear_backups'] = 'Delete DB Backups'; +$_['button_clear_backups_short'] = 'Clear Backups'; +$_['confirm_clear_backups'] = 'Are you sure you want to delete all SQL database backups?'; +$_['text_compare_title'] = 'Compare Images BEFORE / AFTER'; +$_['text_compare_before'] = 'BEFORE'; +$_['text_compare_after'] = 'AFTER'; +$_['text_backups_cleared'] = 'All database backups deleted successfully!'; +$_['error_no_backups'] = 'No backups found or folder is empty.'; +$_['button_restore'] = 'Restore'; +$_['button_empty'] = 'Clear'; +$_['entry_wm_targets'] = 'Apply watermark to:'; +$_['text_wm_target_product'] = 'Product Photos'; +$_['text_wm_target_category'] = 'Category Photos'; +$_['text_wm_target_brand'] = 'Brand Logos'; +$_['text_wm_target_banner'] = 'Banners and Slides'; +$_['text_wm_target_blog'] = 'Articles / Blog'; +$_['error_no_scan_data'] = 'Error: No scan data found. Please run scan first!'; +$_['error_row_not_found'] = 'Error: Row not found in scan results!'; +$_['error_file_not_found'] = 'Error: File not found on server!'; +$_['text_folder_rules_title'] = 'Individual Folder Rules'; +$_['text_folder_rules_desc'] = 'You can override compression quality and maximum sizes for specific folders. Leave a field blank to use global settings. The path is relative to the image/ directory (e.g. catalog/banners/).'; +$_['entry_folder_path'] = 'Folder Path'; +$_['entry_jpg_quality'] = 'JPG Quality'; +$_['entry_webp_quality'] = 'WebP Quality'; +$_['button_add_rule'] = 'Add Rule'; +$_['button_remove'] = 'Remove'; +$_['entry_wm_angle'] = 'Watermark Rotation Angle (degrees)'; +$_['entry_wm_corner_radius'] = 'Watermark Image Corner Radius (px)'; +$_['entry_wm_size_type'] = 'Watermark Scaling'; +$_['entry_wm_size_percent'] = 'Watermark Size (% of image width)'; +$_['entry_wm_text_color'] = 'Watermark Text Color'; +$_['entry_wm_text_font'] = 'Watermark Font (TTF)'; +$_['entry_wm_filter_mode'] = 'Categories/Brands Filter Mode'; +$_['text_wm_filter_include'] = 'Only for Selected'; +$_['text_wm_filter_exclude'] = 'For All Except Selected'; +$_['text_wm_size_original'] = 'Original Size'; +$_['text_wm_size_percent'] = 'Proportional to image width (%)'; +$_['button_preview_watermark'] = 'Preview Watermark'; +$_['text_wm_preview_title'] = 'Watermark Preview'; +$_['text_cron_exclude_folders'] = 'Exclude folders from scan'; +$_['text_cron_entities'] = 'Which photo types to process?'; +$_['text_cron_quality_override'] = 'Override compression settings for Cron'; +$_['text_cron_recommendations'] = 'Recommended Cron Schedule'; +$_['text_cron_recommendations_desc'] = '
    • For small sites (up to 5000 products): run Cron every hour (0 * * * *).
    • For medium sites (5000-20000 products): run every 3-6 hours (0 */6 * * *).
    • For large sites (over 20000 products): run once a day at night (e.g. at 2:00 AM: 0 2 * * *).
    '; +$_['text_tool_broken_files'] = 'Server Broken/Empty Files Scanner'; +$_['help_tool_broken_files'] = 'Scans physical files in image/catalog/ for corrupted images or empty files of 0 bytes.'; +$_['text_grid_broken_file'] = 'Broken or empty file on server'; +$_['text_grid_broken_file_after'] = 'Delete or replace with placeholder'; +$_['text_description_broken_files'] = 'Scanning results of physical files in image/catalog/. Shows broken (corrupted) images or files with 0 bytes size.'; +$_['text_action_strip'] = 'Strip EXIF'; +$_['text_action_set_placeholder'] = 'Set Placeholder'; +$_['text_th_entity'] = 'Entity / Related Links'; +$_['text_cron_entity_product'] = 'Products'; +$_['text_cron_entity_category'] = 'Categories'; +$_['text_cron_entity_manufacturer'] = 'Manufacturers / Brands'; +$_['text_cron_entity_banner'] = 'Banners'; +$_['text_cron_entity_blog'] = 'Blog / Articles'; +$_['entry_cron_jpg_quality'] = 'JPG Quality (Cron)'; +$_['entry_cron_webp_quality'] = 'WebP Quality (Cron)'; +$_['entry_cron_max_width'] = 'Max Width px (Cron)'; +$_['entry_cron_max_height'] = 'Max Height px (Cron)'; +$_['text_cron_folder_scan'] = 'Folders to scan'; +$_['text_no_fonts'] = 'No TTF fonts found in system/library/font/'; +$_['button_apply'] = 'Apply'; +$_['text_backup_will_be_created'] = 'A database backup will be automatically created before running this action.'; +$_['text_bulk_action'] = 'Bulk action for selected'; +$_['text_grid_results'] = 'Scanner Results & Details'; +$_['text_quarantine_will_be_created'] = 'Original files will be moved to quarantine (image/catalog_trash/) before modification.'; +$_['text_success_apply'] = 'Changes successfully applied!'; +$_['text_description_broken'] = 'Database scan results for broken links (table records referencing non-existent files).'; +$_['text_description_cache_restore'] = 'Results of search for cache files whose originals are missing in the catalog.'; +$_['text_description_duplicates'] = 'Results of search for exact duplicate images by MD5 hash.'; +$_['text_description_empty_folders'] = 'List of empty folders in image/catalog/ directory for deletion.'; +$_['text_description_exif'] = 'Results of search for images containing EXIF metadata that can be stripped.'; +$_['text_description_folder_tree'] = 'Folder size statistics in image/catalog/ directory.'; +$_['text_description_html_broken'] = 'Results of search for non-existent image files referenced in HTML descriptions.'; +$_['text_description_placeholders'] = 'Results of search for entities without images to fill with placeholders.'; +$_['text_description_png_jpg'] = 'Results of search for heavy PNG images without transparency that can be compressed to JPG.'; +$_['text_description_small_photos'] = 'List of images with dimensions smaller than the minimum specified (%sx%s px).'; +$_['text_description_smart_cache'] = 'Results of search for cache files whose original images have already been deleted.'; +$_['text_description_translit'] = 'Results of search for files with Cyrillic characters or spaces in their names.'; +$_['text_grid_broken_db_after'] = 'Clear link or set placeholder'; +$_['text_grid_dupe_after'] = 'Original (remains)'; +$_['text_grid_dupe_before'] = 'Duplicate (will be deleted)'; +$_['text_grid_empty_folder'] = 'Empty folder'; +$_['text_grid_empty_folder_after'] = 'Delete empty folder'; +$_['text_grid_exif_after'] = 'Strip metadata'; +$_['text_grid_exif_before'] = 'Contains EXIF'; +$_['text_grid_html_after'] = 'Fix link manually'; +$_['text_grid_missing_file'] = 'File is missing'; +$_['text_grid_missing_html'] = 'Image not found in description'; +$_['text_grid_missing_orig'] = 'Original is missing'; +$_['text_grid_missing_orig_after'] = 'Restore original from cache'; +$_['text_grid_no_image'] = 'No image (empty)'; +$_['text_grid_orph_cache'] = 'Orphaned cache'; +$_['text_grid_orph_cache_after'] = 'Delete unneeded cache'; +$_['text_grid_ph_brand_after'] = 'Set brand placeholder'; +$_['text_grid_ph_category_after'] = 'Set category placeholder'; +$_['text_grid_ph_product_after'] = 'Set product placeholder'; +$_['text_grid_small_photo_after'] = 'Recommended to replace with larger one'; +$_['text_grid_too_small'] = 'Too small'; +$_['text_no_relations'] = 'No database relations'; +$_['text_action_archive'] = 'Archive (to ZIP)'; +$_['text_action_archive_delete'] = 'Archive + Delete'; +$_['text_tool_sanitizer'] = 'File Name Sanitizer'; +$_['help_tool_sanitizer'] = 'Bulk cleanup of file and folder names from Cyrillic, spaces and special characters, normalization of extensions.'; +$_['text_description_sanitizer'] = 'Results of file name analysis for sanitization based on selected rules.'; +$_['entry_sz_translit'] = 'Transliterate (Cyrillic to Latin)'; +$_['entry_sz_spaces'] = 'Replace spaces with "_"'; +$_['entry_sz_special'] = 'Remove special characters'; +$_['entry_sz_lowercase_name'] = 'Lowercase file name'; +$_['entry_sz_lowercase_ext'] = 'Lowercase extension'; +$_['entry_sz_normalize_ext'] = 'Normalize extension (jpeg -> jpg)'; +$_['entry_cron_folder_mode'] = 'Folder filtering mode'; +$_['text_cron_folder_mode_exclude'] = 'Scan all folders except selected (Exceptions)'; +$_['text_cron_folder_mode_include'] = 'Scan only selected folders'; +$_['button_select_all'] = 'Select All'; +$_['button_deselect_all'] = 'Deselect All'; +$_['entry_wm_upload_font'] = 'Upload custom TTF font'; +$_['button_upload_font'] = 'Upload Font'; +$_['text_success_font_upload'] = 'Font successfully uploaded!'; +$_['error_font_upload'] = 'Error uploading font! Only .ttf files up to 5 MB are allowed.'; +$_['entry_lazy_load'] = 'Progressive Lazy Load'; +$_['help_lazy_load'] = 'Enables a premium progressive "blur-up" lazy loading on the frontend. Images are replaced with tiny blurred versions and loaded fully when scrolled into view.'; +$_['confirm_replace_mode'] = 'WARNING! You have selected the "Replace originals" mode. All images will be overwritten directly on the server. We strongly recommend making a backup first. Are you sure you want to continue?'; +$_['text_compare'] = 'Compare'; +$_['text_home'] = 'Home'; +$_['text_tech_gd'] = 'GD Library'; +$_['text_tech_imagick'] = 'Imagick'; +$_['text_tech_webp'] = 'WebP Support'; +$_['text_position_1'] = '1 - Top Left'; +$_['text_position_2'] = '2 - Top Center'; +$_['text_position_3'] = '3 - Top Right'; +$_['text_position_4'] = '4 - Middle Left'; +$_['text_position_5'] = '5 - Center'; +$_['text_position_6'] = '6 - Middle Right'; +$_['text_position_7'] = '7 - Bottom Left'; +$_['text_position_8'] = '8 - Bottom Center'; +$_['text_position_9'] = '9 - Bottom Right'; + +$_['entry_log_level'] = 'Log Level'; +$_['text_log_changed_only'] = 'Changed files only'; +$_['text_log_all'] = 'All files (verbose)'; +$_['text_skipped_no_gain'] = 'Skipped (no improvement)'; +$_['text_reset_cache_hint'] = 'Reset optimization cache (force full rescan)'; +$_['confirm_reset_cache'] = 'Reset optimization cache? Next scan will check ALL files again.'; +$_['text_cache_cleared'] = 'Optimization cache cleared. Next scan will check all files.'; +$_['error_wm_preview'] = 'Failed to generate watermark preview.'; +$_['error_wm_too_large'] = 'Watermark image is too large (%dx%d px)! Maximum allowed size is %dx%d px.'; +$_['text_apply_errors_warning'] = 'WARNING: Some files could not be replaced (locked or permission denied):'; +$_['text_action_compress'] = 'Compress Images'; +$_['text_estimated'] = 'Estimated'; +$_['entry_min_savings_bytes'] = 'Min savings (bytes)'; +$_['entry_min_savings_percent'] = 'Min savings (%)'; +$_['text_optimized'] = 'Optimized'; +$_['entry_broken_fallback'] = 'Broken Image Fallback'; +$_['help_broken_fallback'] = 'Automatically replaces missing or corrupted catalog images on the storefront with a fallback image.'; +$_['entry_fallback_image'] = 'Fallback Image'; +$_['help_fallback_image'] = 'Select a custom image to display when a product or category image is broken. Defaults to no_image.png.'; +$_['text_tool_cmyk'] = 'CMYK to RGB Converter'; +$_['help_tool_cmyk'] = 'Convert JPEG images from CMYK color profile to sRGB to ensure correct display on Safari and iOS devices.'; +$_['text_tool_heavy_files'] = 'Top Heavy Files Scanner'; +$_['help_tool_heavy_files'] = 'Scan and find the heaviest original files in catalog directory and optimize them directly.'; +$_['entry_heavy_files_limit'] = 'Max files to find (total limit)'; +$_['text_action_convert_rgb'] = 'Convert to RGB'; +$_['text_description_cmyk'] = 'CMYK JPEGs found. Conversion to sRGB guarantees they are displayed correctly on all platforms, including iOS/macOS.'; +$_['text_description_heavy'] = 'List of the top %d heaviest files in the image directory. You can optimize them here directly.'; +$_['text_cron_folder_hint_exclude_all'] = 'Status: All folders will be scanned (no exclusions).'; +$_['text_cron_folder_hint_exclude_some'] = 'Status: All folders will be scanned EXCEPT the %d checked folders.'; +$_['text_cron_folder_hint_include_some'] = 'Status: Only the %d checked folders will be scanned.'; +$_['text_cron_folder_hint_include_none'] = 'WARNING: No folders are selected! The Cron job will scan nothing.'; +$_['text_action_apply_changes'] = 'Apply specified changes'; +$_['entry_wm_status'] = 'Watermark Status'; +$_['text_disabled'] = 'Disabled'; +$_['entry_rule_name'] = 'Rule Name'; +$_['entry_rule_status'] = 'Rule Status'; +$_['entry_action'] = 'Action'; +$_['entry_ph_status'] = 'Placeholder Status'; +$_['entry_sanitizer_limit'] = 'Max files to scan (limit)'; +$_['entry_heavy_files_min_size'] = 'Min Size Threshold'; +$_['entry_png_jpg_limit'] = 'Max files to scan (limit)'; +$_['entry_png_jpg_min_size'] = 'Min Size Threshold'; +$_['text_wm_rule_title'] = 'Watermark Rule'; +$_['button_close'] = 'Close'; +$_['text_ph_rule_title'] = 'Placeholder Rule'; +$_['entry_ph_image'] = 'Placeholder Image'; +$_['button_clear_cache_wm'] = 'Clear Cache (apply watermarks)'; +$_['text_active'] = 'Active'; +$_['text_error'] = 'Not Active'; +$_['text_th_dupe_delete'] = 'File to delete (Duplicate)'; +$_['text_th_dupe_keep'] = 'File to keep (Original)'; +$_['text_dupe_original_label'] = 'Original'; +$_['text_dupe_duplicate_label'] = 'Duplicate'; + +$_['text_disk_usage'] = 'Disk Usage'; +$_['text_cumulative_stats'] = 'Optimization Statistics'; +$_['text_disk_catalog'] = 'Catalog images'; +$_['text_disk_cache'] = 'OC Cache'; +$_['text_disk_trash'] = 'Module trash'; +$_['text_disk_other'] = 'Other'; +$_['text_disk_free'] = 'Free'; +$_['text_total_files_opt'] = 'Files optimized'; +$_['text_total_saved'] = 'Total saved'; +$_['text_avg_saving_pct'] = 'Avg. savings'; +$_['text_stats_hint'] = 'Statistics accumulate across all optimization sessions (stored locally)'; +$_['text_reset_stats'] = 'Reset statistics'; +$_['text_reset_stats_confirm']= 'Are you sure you want to reset optimization statistics?'; +$_['text_export_import'] = 'Export / Import Settings'; +$_['button_export_settings'] = 'Export settings (JSON)'; +$_['button_import_settings'] = 'Import settings'; +$_['help_export_settings'] = 'Download all module settings as a JSON file'; +$_['help_import_settings'] = 'Upload a previously exported JSON to restore settings'; +$_['text_import_success'] = 'Settings imported successfully. Reload the page to apply.'; +$_['error_import_file'] = 'No file uploaded or upload error'; +$_['error_import_invalid'] = 'Invalid settings file (wrong module or format)'; +$_['text_click_to_load'] = 'Click ↑ to load disk usage'; +$_['tab_formats'] = 'Image Formats'; + +$_['text_disk_quota_legend'] = 'Hosting Disk Quota Limits (manual)'; +$_['text_disk_quota_desc'] = 'If the disk space graph shows incorrect data (the server\'s physical partition instead of your hosting account limit), you can specify your hosting quota limits manually. Enter 0 for auto-detect.'; +$_['entry_disk_quota_total'] = 'Total Hosting Disk Space (GB)'; +$_['entry_disk_quota_used'] = 'Total Hosting Used Disk Space (GB)'; +$_['entry_auto_disk'] = 'Auto-update on page load'; + +// Cron Tasks +$_['entry_cron_task_name'] = 'Task Name'; +$_['entry_cron_task_summary'] = 'Compression settings'; +$_['button_add_cron_task'] = 'Add Cron Task'; +$_['button_copy_cron_url'] = 'Copy URL'; +$_['text_cron_recommended_url'] = 'Cron task execution URL:'; + +// Licensing +$_['text_license_required'] = 'License Key Required'; +$_['text_license_required_desc'] = 'Please enter your license key to activate the module. All configuration and optimization features are locked until a valid key is provided.'; +$_['entry_token_desc'] = 'Enter the activation token issued for your root domain. You can obtain it from your purchase page or by contacting support.'; +$_['button_activate'] = 'Activate Module'; +$_['text_support'] = 'Technical Support'; +$_['text_support_desc'] = 'If you don\'t have a key yet, or face issues, please provide your order ID and domain name.'; + +// New settings +$_['entry_wm_max_width'] = 'Watermark Max Width (px)'; +$_['entry_wm_max_height'] = 'Watermark Max Height (px)'; +$_['help_wm_max_width'] = 'If the source watermark image exceeds this width, it will be automatically scaled down. Set 0 or leave empty for default 800px.'; +$_['help_wm_max_height'] = 'If the source watermark image exceeds this height, it will be automatically scaled down. Set 0 or leave empty for default 800px.'; +$_['entry_disk_status'] = 'Enable Disk Space Scan & Graph'; +$_['help_disk_status'] = 'If enabled, the module will periodically scan the image folder size and display a pie chart. Disable this on large sites to improve settings page load speed.'; +$_['text_disk_settings_title'] = 'Disk Limits & Scanning Settings'; + diff --git a/upload/admin/language/english/module/img_opti.php b/upload/admin/language/english/module/img_opti.php new file mode 100644 index 0000000..16f7e4e --- /dev/null +++ b/upload/admin/language/english/module/img_opti.php @@ -0,0 +1,565 @@ +Image Optimizer [NAT]: Compress Originals & Delete Junk'; +$_['text_extension'] = 'Extensions'; +$_['text_edit'] = 'Module Settings'; +$_['text_success'] = 'Settings have been successfully saved!'; +$_['entry_warning'] = 'WARNING: Always make a backup of your files, especially the /image/catalog folder before starting!'; +$_['text_tech'] = 'Server Technology Check:'; +$_['text_author'] = 'Technical support and suggestions: info@nat.od.ua'; +$_['tab_optimize'] = 'Optimization & Resize'; +$_['tab_cleaner'] = 'Clean Unused Images'; +$_['tab_info'] = 'Info / Ecosystem [NAT]'; +$_['tab_settings'] = 'Settings'; +$_['tab_tools'] = 'Tools (pro)'; +$_['tab_cron'] = 'Cron Jobs'; +$_['entry_mode'] = 'Operation Mode'; +$_['text_copy'] = 'Create a copy (Safe)'; +$_['text_replace'] = 'Replace originals (Requires backup!)'; +$_['entry_folder'] = 'Folder name for copy'; +$_['entry_batch'] = 'Files per batch'; +$_['entry_log_limit'] = 'Log lines per page'; +$_['entry_max_width'] = 'Max width (px) [0 - no limit]'; +$_['entry_max_height'] = 'Max height (px) [0 - no limit]'; +$_['entry_jpg'] = 'JPG Quality (0-100)'; +$_['entry_png'] = 'PNG Compression (0-9)'; +$_['entry_webp'] = 'WebP Quality (0-100)'; +$_['entry_targets'] = 'What to optimize?'; +$_['text_all_catalog'] = '[ Entire image/catalog ]'; +$_['text_root_files'] = 'Files only in image/catalog root'; +$_['entry_threshold'] = 'Replacement threshold (%)'; +$_['help_threshold'] = '0: replace only if file is smaller. 10: allow up to 10% file size increase.'; +$_['entry_token'] = 'Activation Token (Root Domain)'; +$_['entry_status'] = 'Module Status'; +$_['entry_menu_position'] = 'Menu Position [NAT]'; +$_['text_menu_module_only'] = 'Modules List Only'; +$_['text_menu_header'] = 'Top Header'; +$_['text_menu_sidebar'] = 'Sidebar Menu'; +$_['text_menu_both'] = 'Both (Header + Sidebar)'; +$_['text_status_on'] = 'Enabled'; +$_['text_status_off'] = 'Disabled'; +$_['entry_engine'] = 'Image Processing Engine'; +$_['text_engine_gd'] = 'GD (Standard)'; +$_['text_engine_imagick'] = 'Imagick (High quality, sRGB profile keeping)'; +$_['entry_license'] = 'License'; +$_['text_cleaner_info'] = 'This tool looks for files that physically exist on the server but are not linked in the database or template files.'; +$_['entry_clean_targets'] = 'Folders to scan for junk?'; +$_['entry_extended_log'] = 'Extended log (show used photos and their locations)'; +$_['entry_process_broken'] = 'Include files with broken encoding ("?" in names)'; +$_['button_scan_trash'] = 'FIND JUNK FILES'; +$_['button_quarantine'] = 'Move to Quarantine (Safe)'; +$_['button_delete_trash'] = 'Delete Permanently'; +$_['button_empty_quarantine'] = 'Empty Quarantine Folder'; +$_['button_restore_quarantine'] = 'Restore from Quarantine'; +$_['text_scan_result'] = 'Scan Results'; +$_['text_trash_files'] = 'Junk files found: '; +$_['text_trash_size'] = 'Potential space to free up: '; +$_['button_start'] = 'START OPTIMIZATION'; +$_['button_pause'] = 'PAUSE'; +$_['button_stop'] = 'STOP'; +$_['button_continue'] = 'CONTINUE'; +$_['button_start_new'] = 'START (New Run)'; +$_['button_download_log'] = 'Download TXT Log'; +$_['button_download'] = 'Download'; +$_['button_save'] = 'Save'; +$_['button_cancel'] = 'Cancel'; +$_['text_progress'] = 'Progress:'; +$_['text_old_size'] = 'Before:'; +$_['text_new_size'] = 'After:'; +$_['text_savings'] = 'Saved:'; +$_['text_detailed_log'] = 'Detailed Log'; +$_['text_page'] = 'Page'; +$_['text_waiting'] = 'Waiting to start...'; +$_['text_enabled'] = 'Enabled'; +$_['text_disabled_error'] = 'Disabled (ERROR)'; +$_['text_supported'] = 'Supported'; +$_['text_no'] = 'No'; +$_['error_permission'] = 'You do not have permission to modify this module!'; +$_['error_license'] = 'Access Error: Invalid activation token for this domain!'; +$_['error_domain'] = ' (Domain: '; +$_['text_found'] = 'Files found: '; +$_['text_compressed'] = 'Compressed'; +$_['text_original'] = 'Original'; +$_['text_error_process'] = 'Processing error: '; +$_['error_log_empty'] = 'Log is empty! Run the process first.'; +$_['text_report_header'] = '=== REPORT ==='; +$_['text_total_files'] = 'Total files: '; +$_['text_searching'] = 'Searching for files...'; +$_['error_ajax'] = 'Error: '; +$_['text_resumed'] = '▶ PROCESS RESUMED...'; +$_['text_paused'] = '⏸ PAUSED. Process suspended.'; +$_['confirm_stop'] = 'Are you sure you want to completely abort the process?'; +$_['confirm_delete'] = 'WARNING! Files will be permanently deleted from the server with no chance of recovery! Continue?'; +$_['confirm_empty_q'] = 'Are you sure you want to completely empty the quarantine folder?'; +$_['confirm_restore'] = 'All files from quarantine will be returned to their original locations. Continue?'; +$_['button_stopping'] = 'Stopping...'; +$_['text_stopped_user'] = '🛑 PROCESS ABORTED BY USER!'; +$_['text_increase'] = 'Increased'; +$_['text_done'] = '✅ DONE! Process completed.'; +$_['error_timeout'] = 'Server timeout. Auto-retrying in 3 seconds...'; +$_['text_building_index'] = 'Step 1/2: Indexing DB and templates (searching for used photos)...'; +$_['text_comparing'] = 'Step 2/2: Comparing physical files with the index...'; +$_['text_quarantine_done'] = '✅ Quarantined to image/catalog_trash/ successfully!'; +$_['text_deleted_done'] = '✅ Junk deletion completed successfully!'; +$_['text_quarantine_empty'] = '✅ Quarantine folder emptied successfully!'; +$_['text_restore_done'] = '✅ Files restored from quarantine to their original locations!'; +$_['text_quarantine_move'] = 'Quarantined: '; +$_['text_deleted_file'] = 'Deleted: '; +$_['text_no_trash'] = 'No junk found in selected folders! All files are in use.'; +$_['text_scanning_dirs'] = 'Scanning directories: '; +$_['text_log_used'] = '[IN USE]'; +$_['text_log_trash'] = '[JUNK]'; +$_['text_log_deleted'] = '[DELETED]'; +$_['text_log_del_err'] = '[DELETE ERROR]'; +$_['text_log_quarantine'] = '[QUARANTINED]'; +$_['text_log_q_err'] = '[MOVE ERROR]'; +$_['text_log_restored'] = '[RESTORED]'; +$_['text_log_rest_err'] = '[RESTORE ERROR]'; +$_['text_used_in'] = 'in: '; +$_['text_analyzing'] = 'Analyzing database...'; +$_['text_clean_empty'] = 'Clean! No junk found.'; +$_['text_processing'] = 'Processing...'; +$_['text_restoring'] = 'Restoring files...'; +$_['text_all_folders'] = 'image/catalog/ (And all subfolders)'; +$_['text_root_folder'] = 'image/catalog/ (Root only)'; +$_['text_q_empty_err'] = 'Quarantine folder is empty or does not exist.'; +$_['error_token'] = 'Invalid token. Please refresh the page.'; +$_['text_apply_title'] = 'Optimization completed in folder: '; +$_['text_apply_info'] = 'Check the result. If everything looks good, apply changes to the main catalog.'; +$_['entry_apply_backup'] = 'Move and create a backup (in Quarantine)'; +$_['entry_apply_replace'] = 'Replace originals permanently'; +$_['button_apply_main'] = 'APPLY TO MAIN CATALOG'; +$_['button_delete_copies'] = 'Delete temporary copies (Cancel)'; +$_['button_full_cleanup'] = 'Full cleanup of all temp folders and backups'; +$_['text_apply_success'] = '✅ Files successfully moved. '; +$_['text_apply_backup_ok'] = 'Originals saved in folder: '; +$_['text_apply_no_backup'] = 'Originals deleted.'; +$_['text_error_source'] = 'Error: Folder with optimized files not found.'; +$_['confirm_full_cleanup'] = 'WARNING! This action will permanently delete ALL backup folders (backup_*) and temp copy folders. Continue?'; +$_['text_tools_desc'] = 'Global scanners and tools for file system and database image optimization.'; +$_['text_tool_select'] = 'Select Tool:'; +$_['text_group_scanners'] = 'Scanners & Analysis'; +$_['text_group_generators'] = 'Generators & Processing'; +$_['text_group_experimental'] = 'Experimental Features (BETA)'; +$_['text_experimental_warn'] = 'Warning: Experimental features modify system files or the database. Backup is highly recommended!'; +$_['text_tool_broken_db'] = 'Broken/Empty DB Image Scanner'; +$_['text_tool_watermark'] = 'Dynamic Watermark (Cache only)'; +$_['text_tool_duplicates'] = 'Duplicate Finder (MD5)'; +$_['text_tool_png_jpg'] = 'Heavy PNG to JPG Converter (non-transparent PNG scanner)'; +$_['text_tool_placeholders'] = 'Placeholder Generator (Replaces No Image)'; +$_['text_tool_exif'] = 'Clean EXIF Data (Geotags/Meta)'; +$_['text_tool_small_photos'] = 'Too Small Photo Scanner'; +$_['text_tool_html_broken'] = 'Broken Images in HTML'; +$_['text_tool_empty_folders'] = 'Remove Empty Folders (image/catalog)'; +$_['text_tool_smart_cache'] = 'Smart Cache Cleanup'; +$_['text_tool_folder_tree'] = 'Folder Tree Statistics'; +$_['text_tool_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load (No DB)'; +$_['text_tool_translit'] = 'File Name Transliteration'; +$_['text_tool_cache_restore'] = 'Restore Originals from Cache'; +$_['entry_wm_type'] = 'Watermark Type'; +$_['text_wm_image'] = 'Image (PNG)'; +$_['text_wm_text'] = 'Text'; +$_['entry_wm_image_path'] = 'Image Path'; +$_['entry_wm_text_val'] = 'Watermark Text'; +$_['entry_wm_position'] = 'Position (1-9)'; +$_['entry_wm_opacity'] = 'Opacity (0-100)'; +$_['entry_wm_category'] = 'Only for Categories'; +$_['entry_wm_brand'] = 'Only for Brands'; +$_['entry_min_width'] = 'Minimum Width (px)'; +$_['entry_min_height'] = 'Minimum Height (px)'; +$_['entry_ph_product'] = 'Placeholder for Products'; +$_['entry_ph_category'] = 'Placeholder for Categories'; +$_['entry_ph_brand'] = 'Placeholder for Brands'; +$_['button_run_tool'] = 'Run Tool'; +$_['button_scan_tool'] = 'Scan'; +$_['button_fix_tool'] = 'Fix Found'; +$_['text_th_path'] = 'File / Path'; +$_['text_th_size'] = 'Size'; +$_['text_th_action'] = 'Action'; +$_['text_th_status'] = 'Status'; +$_['text_th_problem'] = 'Problem / Value'; +$_['text_cron_title'] = 'Cron Settings'; +$_['text_cron_desc'] = 'Add the command to your hosting Cron for automatic background compression.'; +$_['entry_cron_new_only'] = 'Process only new (unprocessed) files'; +$_['entry_cron_folders'] = 'Folders to scan (leave empty for entire catalog)'; +$_['text_promo_title'] = 'Other [NAT] Series Modules'; +$_['text_promo_desc'] = '[NAT] Series Modules are tools for deep OpenCart optimization. We focus on automation, DB speed, and server cleanup. All modules are open-source and share the same core logic.'; +$_['text_support'] = 'Technical support and suggestions:'; +$_['button_more'] = 'View all extensions on OpenCart.com'; +$_['button_more_forum'] = 'Opencartforum'; +$_['text_mod_cat_opt'] = 'Catalog Optimizer [NAT]'; +$_['text_mod_cat_opt_desc'] = 'Hide empty categories/brands + SEO sorting (in stock first) + DB junk cleanup.'; +$_['text_mod_new_arrivals'] = 'New Arrivals [NAT]: Sort by exact date'; +$_['text_mod_new_arrivals_desc'] = 'Automated SEO page for new arrivals. Displays items strictly by the real date added. Sliders, grid, and smart date archive sidebar.'; +$_['text_mod_img_opt'] = 'Image Optimizer [NAT]'; +$_['text_mod_img_opt_desc'] = 'Current Module. Compress and resize ORIGINAL photos (image/catalog) to WebP + Server cleanup of unused images.'; +$_['text_mod_dom_scan'] = 'Domain Scanner [NAT]'; +$_['text_mod_dom_scan_desc'] = 'Find and clean external links and HTTP images in DB and theme files. Ideal for HTTPS migration or cleanup after parsing.'; +$_['text_mod_poll'] = 'Poll Widget [NAT]'; +$_['text_mod_poll_desc'] = 'Create polls and voting widgets with a visual design builder, deep statistics, and cheat protection.'; +$_['text_mod_db_opt'] = 'Database Optimizer [NAT]'; +$_['text_mod_db_opt_desc'] = 'Intelligent garbage cleanup in the database, system log management, and crucial index addition for speed boost.'; +$_['text_mod_im_ex'] = 'Import/Export [NAT]'; +$_['text_mod_im_ex_desc'] = 'Import/export prices and stock levels from/to files (xls, xlsx, csv, xml, json) or by url. Field mapping, Cron automation, text utilities.'; +$_['text_mod_admin_tools'] = 'Admin Tools [NAT]'; +$_['text_mod_admin_tools_desc'] = 'Handy administrator tools: login as customer, admin customization, user permissions management, and hiding unnecessary menu items.'; +$_['text_mod_utm_tracker'] = 'Order UTM Tracker [NAT]'; +$_['text_mod_utm_tracker_desc'] = 'Automatic tracking of UTM tags during checkout. Saves the traffic source, campaign, keywords, and displays this data in order details to analyze advertising effectiveness.'; +$_['text_info_title_promo_top_bar'] = 'Promo Top Bar [NAT]: Upper promotional info-banner, information bar above header, marquee and top-banner'; +$_['text_info_desc_promo_top_bar'] = 'Create interactive promo bars and header slideshows with a block builder, countdown timers, subscription forms, and flexible targeting.'; +$_['text_tool_no_image'] = 'Products without main image (empty in DB)'; +$_['text_problem_main_broken'] = 'Broken main image (file deleted)'; +$_['text_problem_add_broken'] = 'Broken additional image (file deleted)'; +$_['text_problem_no_image'] = 'Missing main image'; +$_['text_err_dir'] = 'Error: Directory not found or invalid path.'; +$_['text_err_no_queue'] = 'Error: No queue found. Run scan first.'; +$_['text_log_del_empty_folder'] = 'Deleted empty folder: '; +$_['text_log_no_empty_folders'] = 'No empty folders found in image/catalog/.'; +$_['text_log_total_empty_folders'] = 'Total empty folders removed: '; +$_['text_log_cache_cleared'] = 'Image cache has been cleared.'; +$_['text_log_wm_smart_mode'] = 'Watermarks are operating in SMART mode: originals remain untouched, applied only on cache generation.'; +$_['text_log_scan_dupes'] = 'Scanning image/catalog/ for MD5 duplicates... This may take a while.'; +$_['text_log_found_dupe'] = 'Found duplicate: '; +$_['text_log_no_dupes'] = 'No duplicates found.'; +$_['text_log_total_dupes'] = 'Found duplicates: %s. Potential savings: %s'; +$_['text_log_fix_dupes_prompt'] = 'Click "Fix Found" to redirect DB links to original and delete duplicates.'; +$_['text_log_fixed_dupe'] = 'Fixed and removed duplicate: '; +$_['text_log_success_dupes'] = 'Successfully fixed %s duplicates. DB updated.'; +$_['text_log_scan_png'] = 'Scanning for heavy PNG files without transparency...'; +$_['text_log_no_png'] = 'No suitable PNG files found.'; +$_['text_log_total_png'] = 'Found %s PNG without transparency. Click "Fix Found" to convert to JPG.'; +$_['text_log_converted_jpg'] = 'Converted to JPG: '; +$_['text_log_success_png'] = 'Successfully converted %s files. Space saved: %s'; +$_['text_log_ph_prod'] = 'Updated %s Products with placeholder.'; +$_['text_log_ph_cat'] = 'Updated %s Categories with placeholder.'; +$_['text_log_ph_brand'] = 'Updated %s Brands with placeholder.'; +$_['text_log_ph_empty'] = 'No placeholders selected in settings.'; +$_['text_log_scan_exif'] = 'Scanning for EXIF data...'; +$_['text_log_stripped_exif'] = 'Stripped EXIF: '; +$_['text_log_no_exif'] = 'No EXIF data found to clean.'; +$_['text_log_success_exif'] = 'Cleaned EXIF from %s files. Saved: %s'; +$_['text_log_scan_small'] = 'Scanning for photos smaller than %sx%s px...'; +$_['text_log_too_small'] = 'Too small (%sx%s): '; +$_['text_log_no_small'] = 'No small photos found.'; +$_['text_log_total_small'] = 'Found %s small photos. Please review and re-upload.'; +$_['text_log_html_broken'] = 'Broken IMG in HTML table %s (ID: %s): '; +$_['text_log_no_html_broken'] = 'No broken images found in HTML descriptions.'; +$_['text_log_total_html_broken'] = 'Found %s broken links in HTML. Please edit them manually.'; +$_['text_log_scan_smart_cache'] = 'Scanning image/cache/ for orphaned files...'; +$_['text_log_del_orph_cache'] = 'Deleted orphaned cache: '; +$_['text_log_no_orph_cache'] = 'No orphaned cache files found.'; +$_['text_log_success_smart_cache'] = 'Removed %s orphaned cache files. Space saved: %s'; +$_['text_log_scan_translit'] = 'Scanning for files with cyrillic or spaces...'; +$_['text_log_no_bad_names'] = 'No files with bad names found.'; +$_['text_log_total_bad_names'] = 'Found %s bad files. Click "Fix Found" to transliterate and update DB.'; +$_['text_log_renamed'] = 'Renamed: '; +$_['text_log_success_translit'] = 'Successfully transliterated %s files and updated DB.'; +$_['text_log_scan_restore'] = 'Scanning image/cache/ to restore lost originals...'; +$_['text_log_restored_cache'] = 'Restored from cache: '; +$_['text_log_no_lost_orig'] = 'No missing originals found in cache.'; +$_['text_log_success_restore'] = 'Successfully restored %s original images from cache.'; +$_['error_invalid_folder'] = 'Error: Invalid folder name!'; +$_['text_formats_title'] = 'Modern Formats (WebP & SVG)'; +$_['text_formats_desc'] = 'Settings for integrating modern formats into the OpenCart file system and templates.'; +$_['entry_support_svg'] = 'SVG Upload Support'; +$_['help_support_svg'] = 'Allows uploading vector SVG images via the standard Filemanager and safely displaying them on the storefront without size distortion.'; +$_['entry_support_webp'] = 'WebP Upload Support'; +$_['help_support_webp'] = 'Allows manually uploading, linking, and displaying ready .webp files on the website via the Filemanager.'; +$_['entry_truefilemanager_webp'] = 'WebP in TrueFileManager'; +$_['help_truefilemanager_webp'] = 'Adds WebP upload, preview, and thumbnail support to sitecreator\'s TrueFileManager (if installed).'; +$_['entry_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load'; +$_['help_webp_on_fly'] = 'Ultra-fast replacement: automatically changes .jpg/.png extensions to .webp directly in the storefront HTML output and adds the loading="lazy" attribute. Does not touch the database!'; +$_['help_tool_broken_db'] = 'Finds images in the database whose physical files have been deleted from the server. Clears broken links.'; +$_['help_tool_empty_folders'] = 'Recursively scans image/catalog/ and safely removes all folders that contain no files.'; +$_['help_tool_folder_tree'] = 'Builds a directory tree and shows the real size of each folder to find space eaters.'; +$_['help_tool_html_broken'] = 'Scans HTML descriptions of products and categories for tags with non-existent images.'; +$_['help_tool_small_photos'] = 'Finds pictures with resolution below the specified one. Added search links to Google and Yandex Images so you can easily find high-quality replacements.'; +$_['help_tool_duplicates'] = 'Searches for exact duplicates by MD5 hash (different names, same image). Merges them into one file and updates the DB.'; +$_['help_tool_watermark'] = 'Applies watermarks in SMART mode (only to storefront cache, originals remain unchanged).'; +$_['help_tool_placeholders'] = 'Massively sets the selected placeholder image for products and categories that have no photo.'; +$_['help_tool_png_jpg'] = 'Scans for heavy PNG files without transparency (alpha channel). Since they do not need transparency, they can be safely converted to lightweight JPG. This greatly reduces file size (typically by 70-80%) and speeds up your website, automatically updating all links in the database.'; +$_['help_tool_exif'] = 'Scans JPEG files and removes hidden EXIF data (geotags, camera data), reducing size by 10-15%.'; +$_['help_tool_smart_cache'] = 'Finds cached files in image/cache/ whose originals have already been deleted, and removes this trash.'; +$_['help_tool_translit'] = 'Finds files with Cyrillic or spaces, renames them using transliteration, and updates paths in the DB.'; +$_['help_tool_cache_restore'] = 'If you accidentally deleted an original from catalog, this tool will try to find its copy in cache and restore it.'; +$_['entry_quarantine'] = 'Move originals to Quarantine (instead of deleting)'; +$_['entry_backup_db'] = 'Create database backup of modified tables in SQL before starting'; +$_['button_fix_selected'] = 'FIX SELECTED'; +$_['button_export_log'] = 'Export to CSV'; +$_['text_th_before'] = 'State BEFORE'; +$_['text_th_after'] = 'Expected result AFTER'; +$_['text_th_select'] = 'Action'; +$_['text_th_preview'] = 'Preview'; +$_['text_th_name'] = 'Object / File'; +$_['text_global_action'] = 'Bulk action for selected:'; +$_['text_action_clear'] = 'Clear DB Link'; +$_['text_action_disable'] = 'Disable (Status = 0)'; +$_['text_action_stock'] = 'Set Out of Stock'; +$_['text_action_placeholder'] = 'Set Placeholder'; +$_['text_action_ignore'] = 'Do nothing'; +$_['text_action_merge'] = 'Merge (delete duplicate, keep original)'; +$_['text_action_convert'] = 'Convert to JPG'; +$_['text_action_rename'] = 'Rename to translit'; +$_['text_action_delete'] = 'Delete'; +$_['text_action_restore'] = 'Restore'; +$_['text_db_backup_created'] = 'Database backup created: %s'; +$_['text_quarantine_created'] = 'Quarantined to: %s'; +$_['text_fix_status_success'] = 'Successfully executed'; +$_['text_fix_status_error'] = 'Error: %s'; +$_['text_log_exported'] = 'Log successfully exported!'; +$_['text_select_action'] = '-- Select Action --'; +$_['error_empty_selection'] = 'Error: No rows selected for fixing!'; +$_['text_link_admin'] = 'Admin'; +$_['text_link_catalog'] = 'Store'; +$_['entry_threads'] = 'Processing Threads'; +$_['text_threads_optimal'] = 'optimal'; +$_['button_download_zip'] = 'Download ZIP and Delete Folder'; +$_['confirm_download_and_delete'] = 'Did the ZIP download successfully? Can we delete the temporary copies folder on the server?'; +$_['button_clear_backups'] = 'Delete DB Backups'; +$_['button_clear_backups_short'] = 'Clear Backups'; +$_['confirm_clear_backups'] = 'Are you sure you want to delete all SQL database backups?'; +$_['text_compare_title'] = 'Compare Images BEFORE / AFTER'; +$_['text_compare_before'] = 'BEFORE'; +$_['text_compare_after'] = 'AFTER'; +$_['text_backups_cleared'] = 'All database backups deleted successfully!'; +$_['error_no_backups'] = 'No backups found or folder is empty.'; +$_['button_restore'] = 'Restore'; +$_['button_empty'] = 'Clear'; +$_['entry_wm_targets'] = 'Apply watermark to:'; +$_['text_wm_target_product'] = 'Product Photos'; +$_['text_wm_target_category'] = 'Category Photos'; +$_['text_wm_target_brand'] = 'Brand Logos'; +$_['text_wm_target_banner'] = 'Banners and Slides'; +$_['text_wm_target_blog'] = 'Articles / Blog'; +$_['error_no_scan_data'] = 'Error: No scan data found. Please run scan first!'; +$_['error_row_not_found'] = 'Error: Row not found in scan results!'; +$_['error_file_not_found'] = 'Error: File not found on server!'; +$_['text_folder_rules_title'] = 'Individual Folder Rules'; +$_['text_folder_rules_desc'] = 'You can override compression quality and maximum sizes for specific folders. Leave a field blank to use global settings. The path is relative to the image/ directory (e.g. catalog/banners/).'; +$_['entry_folder_path'] = 'Folder Path'; +$_['entry_jpg_quality'] = 'JPG Quality'; +$_['entry_webp_quality'] = 'WebP Quality'; +$_['button_add_rule'] = 'Add Rule'; +$_['button_remove'] = 'Remove'; +$_['entry_wm_angle'] = 'Watermark Rotation Angle (degrees)'; +$_['entry_wm_corner_radius'] = 'Watermark Image Corner Radius (px)'; +$_['entry_wm_size_type'] = 'Watermark Scaling'; +$_['entry_wm_size_percent'] = 'Watermark Size (% of image width)'; +$_['entry_wm_text_color'] = 'Watermark Text Color'; +$_['entry_wm_text_font'] = 'Watermark Font (TTF)'; +$_['entry_wm_filter_mode'] = 'Categories/Brands Filter Mode'; +$_['text_wm_filter_include'] = 'Only for Selected'; +$_['text_wm_filter_exclude'] = 'For All Except Selected'; +$_['text_wm_size_original'] = 'Original Size'; +$_['text_wm_size_percent'] = 'Proportional to image width (%)'; +$_['button_preview_watermark'] = 'Preview Watermark'; +$_['text_wm_preview_title'] = 'Watermark Preview'; +$_['text_cron_exclude_folders'] = 'Exclude folders from scan'; +$_['text_cron_entities'] = 'Which photo types to process?'; +$_['text_cron_quality_override'] = 'Override compression settings for Cron'; +$_['text_cron_recommendations'] = 'Recommended Cron Schedule'; +$_['text_cron_recommendations_desc'] = '
    • For small sites (up to 5000 products): run Cron every hour (0 * * * *).
    • For medium sites (5000-20000 products): run every 3-6 hours (0 */6 * * *).
    • For large sites (over 20000 products): run once a day at night (e.g. at 2:00 AM: 0 2 * * *).
    '; +$_['text_tool_broken_files'] = 'Server Broken/Empty Files Scanner'; +$_['help_tool_broken_files'] = 'Scans physical files in image/catalog/ for corrupted images or empty files of 0 bytes.'; +$_['text_grid_broken_file'] = 'Broken or empty file on server'; +$_['text_grid_broken_file_after'] = 'Delete or replace with placeholder'; +$_['text_description_broken_files'] = 'Scanning results of physical files in image/catalog/. Shows broken (corrupted) images or files with 0 bytes size.'; +$_['text_action_strip'] = 'Strip EXIF'; +$_['text_action_set_placeholder'] = 'Set Placeholder'; +$_['text_th_entity'] = 'Entity / Related Links'; +$_['text_cron_entity_product'] = 'Products'; +$_['text_cron_entity_category'] = 'Categories'; +$_['text_cron_entity_manufacturer'] = 'Manufacturers / Brands'; +$_['text_cron_entity_banner'] = 'Banners'; +$_['text_cron_entity_blog'] = 'Blog / Articles'; +$_['entry_cron_jpg_quality'] = 'JPG Quality (Cron)'; +$_['entry_cron_webp_quality'] = 'WebP Quality (Cron)'; +$_['entry_cron_max_width'] = 'Max Width px (Cron)'; +$_['entry_cron_max_height'] = 'Max Height px (Cron)'; +$_['text_cron_folder_scan'] = 'Folders to scan'; +$_['text_no_fonts'] = 'No TTF fonts found in system/library/font/'; +$_['button_apply'] = 'Apply'; +$_['text_backup_will_be_created'] = 'A database backup will be automatically created before running this action.'; +$_['text_bulk_action'] = 'Bulk action for selected'; +$_['text_grid_results'] = 'Scanner Results & Details'; +$_['text_quarantine_will_be_created'] = 'Original files will be moved to quarantine (image/catalog_trash/) before modification.'; +$_['text_success_apply'] = 'Changes successfully applied!'; +$_['text_description_broken'] = 'Database scan results for broken links (table records referencing non-existent files).'; +$_['text_description_cache_restore'] = 'Results of search for cache files whose originals are missing in the catalog.'; +$_['text_description_duplicates'] = 'Results of search for exact duplicate images by MD5 hash.'; +$_['text_description_empty_folders'] = 'List of empty folders in image/catalog/ directory for deletion.'; +$_['text_description_exif'] = 'Results of search for images containing EXIF metadata that can be stripped.'; +$_['text_description_folder_tree'] = 'Folder size statistics in image/catalog/ directory.'; +$_['text_description_html_broken'] = 'Results of search for non-existent image files referenced in HTML descriptions.'; +$_['text_description_placeholders'] = 'Results of search for entities without images to fill with placeholders.'; +$_['text_description_png_jpg'] = 'Results of search for heavy PNG images without transparency that can be compressed to JPG.'; +$_['text_description_small_photos'] = 'List of images with dimensions smaller than the minimum specified (%sx%s px).'; +$_['text_description_smart_cache'] = 'Results of search for cache files whose original images have already been deleted.'; +$_['text_description_translit'] = 'Results of search for files with Cyrillic characters or spaces in their names.'; +$_['text_grid_broken_db_after'] = 'Clear link or set placeholder'; +$_['text_grid_dupe_after'] = 'Original (remains)'; +$_['text_grid_dupe_before'] = 'Duplicate (will be deleted)'; +$_['text_grid_empty_folder'] = 'Empty folder'; +$_['text_grid_empty_folder_after'] = 'Delete empty folder'; +$_['text_grid_exif_after'] = 'Strip metadata'; +$_['text_grid_exif_before'] = 'Contains EXIF'; +$_['text_grid_html_after'] = 'Fix link manually'; +$_['text_grid_missing_file'] = 'File is missing'; +$_['text_grid_missing_html'] = 'Image not found in description'; +$_['text_grid_missing_orig'] = 'Original is missing'; +$_['text_grid_missing_orig_after'] = 'Restore original from cache'; +$_['text_grid_no_image'] = 'No image (empty)'; +$_['text_grid_orph_cache'] = 'Orphaned cache'; +$_['text_grid_orph_cache_after'] = 'Delete unneeded cache'; +$_['text_grid_ph_brand_after'] = 'Set brand placeholder'; +$_['text_grid_ph_category_after'] = 'Set category placeholder'; +$_['text_grid_ph_product_after'] = 'Set product placeholder'; +$_['text_grid_small_photo_after'] = 'Recommended to replace with larger one'; +$_['text_grid_too_small'] = 'Too small'; +$_['text_no_relations'] = 'No database relations'; +$_['text_action_archive'] = 'Archive (to ZIP)'; +$_['text_action_archive_delete'] = 'Archive + Delete'; +$_['text_tool_sanitizer'] = 'File Name Sanitizer'; +$_['help_tool_sanitizer'] = 'Bulk cleanup of file and folder names from Cyrillic, spaces and special characters, normalization of extensions.'; +$_['text_description_sanitizer'] = 'Results of file name analysis for sanitization based on selected rules.'; +$_['entry_sz_translit'] = 'Transliterate (Cyrillic to Latin)'; +$_['entry_sz_spaces'] = 'Replace spaces with "_"'; +$_['entry_sz_special'] = 'Remove special characters'; +$_['entry_sz_lowercase_name'] = 'Lowercase file name'; +$_['entry_sz_lowercase_ext'] = 'Lowercase extension'; +$_['entry_sz_normalize_ext'] = 'Normalize extension (jpeg -> jpg)'; +$_['entry_cron_folder_mode'] = 'Folder filtering mode'; +$_['text_cron_folder_mode_exclude'] = 'Scan all folders except selected (Exceptions)'; +$_['text_cron_folder_mode_include'] = 'Scan only selected folders'; +$_['button_select_all'] = 'Select All'; +$_['button_deselect_all'] = 'Deselect All'; +$_['entry_wm_upload_font'] = 'Upload custom TTF font'; +$_['button_upload_font'] = 'Upload Font'; +$_['text_success_font_upload'] = 'Font successfully uploaded!'; +$_['error_font_upload'] = 'Error uploading font! Only .ttf files up to 5 MB are allowed.'; +$_['entry_lazy_load'] = 'Progressive Lazy Load'; +$_['help_lazy_load'] = 'Enables a premium progressive "blur-up" lazy loading on the frontend. Images are replaced with tiny blurred versions and loaded fully when scrolled into view.'; +$_['confirm_replace_mode'] = 'WARNING! You have selected the "Replace originals" mode. All images will be overwritten directly on the server. We strongly recommend making a backup first. Are you sure you want to continue?'; +$_['text_compare'] = 'Compare'; +$_['text_home'] = 'Home'; +$_['text_tech_gd'] = 'GD Library'; +$_['text_tech_imagick'] = 'Imagick'; +$_['text_tech_webp'] = 'WebP Support'; +$_['text_position_1'] = '1 - Top Left'; +$_['text_position_2'] = '2 - Top Center'; +$_['text_position_3'] = '3 - Top Right'; +$_['text_position_4'] = '4 - Middle Left'; +$_['text_position_5'] = '5 - Center'; +$_['text_position_6'] = '6 - Middle Right'; +$_['text_position_7'] = '7 - Bottom Left'; +$_['text_position_8'] = '8 - Bottom Center'; +$_['text_position_9'] = '9 - Bottom Right'; + +$_['entry_log_level'] = 'Log Level'; +$_['text_log_changed_only'] = 'Changed files only'; +$_['text_log_all'] = 'All files (verbose)'; +$_['text_skipped_no_gain'] = 'Skipped (no improvement)'; +$_['text_reset_cache_hint'] = 'Reset optimization cache (force full rescan)'; +$_['confirm_reset_cache'] = 'Reset optimization cache? Next scan will check ALL files again.'; +$_['text_cache_cleared'] = 'Optimization cache cleared. Next scan will check all files.'; +$_['error_wm_preview'] = 'Failed to generate watermark preview.'; +$_['error_wm_too_large'] = 'Watermark image is too large (%dx%d px)! Maximum allowed size is %dx%d px.'; +$_['text_apply_errors_warning'] = 'WARNING: Some files could not be replaced (locked or permission denied):'; +$_['text_action_compress'] = 'Compress Images'; +$_['text_estimated'] = 'Estimated'; +$_['entry_min_savings_bytes'] = 'Min savings (bytes)'; +$_['entry_min_savings_percent'] = 'Min savings (%)'; +$_['text_optimized'] = 'Optimized'; +$_['entry_broken_fallback'] = 'Broken Image Fallback'; +$_['help_broken_fallback'] = 'Automatically replaces missing or corrupted catalog images on the storefront with a fallback image.'; +$_['entry_fallback_image'] = 'Fallback Image'; +$_['help_fallback_image'] = 'Select a custom image to display when a product or category image is broken. Defaults to no_image.png.'; +$_['text_tool_cmyk'] = 'CMYK to RGB Converter'; +$_['help_tool_cmyk'] = 'Convert JPEG images from CMYK color profile to sRGB to ensure correct display on Safari and iOS devices.'; +$_['text_tool_heavy_files'] = 'Top Heavy Files Scanner'; +$_['help_tool_heavy_files'] = 'Scan and find the heaviest original files in catalog directory and optimize them directly.'; +$_['entry_heavy_files_limit'] = 'Max files to find (total limit)'; +$_['text_action_convert_rgb'] = 'Convert to RGB'; +$_['text_description_cmyk'] = 'CMYK JPEGs found. Conversion to sRGB guarantees they are displayed correctly on all platforms, including iOS/macOS.'; +$_['text_description_heavy'] = 'List of the top %d heaviest files in the image directory. You can optimize them here directly.'; +$_['text_cron_folder_hint_exclude_all'] = 'Status: All folders will be scanned (no exclusions).'; +$_['text_cron_folder_hint_exclude_some'] = 'Status: All folders will be scanned EXCEPT the %d checked folders.'; +$_['text_cron_folder_hint_include_some'] = 'Status: Only the %d checked folders will be scanned.'; +$_['text_cron_folder_hint_include_none'] = 'WARNING: No folders are selected! The Cron job will scan nothing.'; +$_['text_action_apply_changes'] = 'Apply specified changes'; +$_['entry_wm_status'] = 'Watermark Status'; +$_['text_disabled'] = 'Disabled'; +$_['entry_rule_name'] = 'Rule Name'; +$_['entry_rule_status'] = 'Rule Status'; +$_['entry_action'] = 'Action'; +$_['entry_ph_status'] = 'Placeholder Status'; +$_['entry_sanitizer_limit'] = 'Max files to scan (limit)'; +$_['entry_heavy_files_min_size'] = 'Min Size Threshold'; +$_['entry_png_jpg_limit'] = 'Max files to scan (limit)'; +$_['entry_png_jpg_min_size'] = 'Min Size Threshold'; +$_['text_wm_rule_title'] = 'Watermark Rule'; +$_['button_close'] = 'Close'; +$_['text_ph_rule_title'] = 'Placeholder Rule'; +$_['entry_ph_image'] = 'Placeholder Image'; +$_['button_clear_cache_wm'] = 'Clear Cache (apply watermarks)'; +$_['text_active'] = 'Active'; +$_['text_error'] = 'Not Active'; +$_['text_th_dupe_delete'] = 'File to delete (Duplicate)'; +$_['text_th_dupe_keep'] = 'File to keep (Original)'; +$_['text_dupe_original_label'] = 'Original'; +$_['text_dupe_duplicate_label'] = 'Duplicate'; + +$_['text_disk_usage'] = 'Disk Usage'; +$_['text_cumulative_stats'] = 'Optimization Statistics'; +$_['text_disk_catalog'] = 'Catalog images'; +$_['text_disk_cache'] = 'OC Cache'; +$_['text_disk_trash'] = 'Module trash'; +$_['text_disk_other'] = 'Other'; +$_['text_disk_free'] = 'Free'; +$_['text_total_files_opt'] = 'Files optimized'; +$_['text_total_saved'] = 'Total saved'; +$_['text_avg_saving_pct'] = 'Avg. savings'; +$_['text_stats_hint'] = 'Statistics accumulate across all optimization sessions (stored locally)'; +$_['text_reset_stats'] = 'Reset statistics'; +$_['text_reset_stats_confirm']= 'Are you sure you want to reset optimization statistics?'; +$_['text_export_import'] = 'Export / Import Settings'; +$_['button_export_settings'] = 'Export settings (JSON)'; +$_['button_import_settings'] = 'Import settings'; +$_['help_export_settings'] = 'Download all module settings as a JSON file'; +$_['help_import_settings'] = 'Upload a previously exported JSON to restore settings'; +$_['text_import_success'] = 'Settings imported successfully. Reload the page to apply.'; +$_['error_import_file'] = 'No file uploaded or upload error'; +$_['error_import_invalid'] = 'Invalid settings file (wrong module or format)'; +$_['text_click_to_load'] = 'Click ↑ to load disk usage'; +$_['tab_formats'] = 'Image Formats'; + +$_['text_disk_quota_legend'] = 'Hosting Disk Quota Limits (manual)'; +$_['text_disk_quota_desc'] = 'If the disk space graph shows incorrect data (the server\'s physical partition instead of your hosting account limit), you can specify your hosting quota limits manually. Enter 0 for auto-detect.'; +$_['entry_disk_quota_total'] = 'Total Hosting Disk Space (GB)'; +$_['entry_disk_quota_used'] = 'Total Hosting Used Disk Space (GB)'; +$_['entry_auto_disk'] = 'Auto-update on page load'; + +// Cron Tasks +$_['entry_cron_task_name'] = 'Task Name'; +$_['entry_cron_task_summary'] = 'Compression settings'; +$_['button_add_cron_task'] = 'Add Cron Task'; +$_['button_copy_cron_url'] = 'Copy URL'; +$_['text_cron_recommended_url'] = 'Cron task execution URL:'; + +// Licensing +$_['text_license_required'] = 'License Key Required'; +$_['text_license_required_desc'] = 'Please enter your license key to activate the module. All configuration and optimization features are locked until a valid key is provided.'; +$_['entry_token_desc'] = 'Enter the activation token issued for your root domain. You can obtain it from your purchase page or by contacting support.'; +$_['button_activate'] = 'Activate Module'; +$_['text_support'] = 'Technical Support'; +$_['text_support_desc'] = 'If you don\'t have a key yet, or face issues, please provide your order ID and domain name.'; + +// New settings +$_['entry_wm_max_width'] = 'Watermark Max Width (px)'; +$_['entry_wm_max_height'] = 'Watermark Max Height (px)'; +$_['help_wm_max_width'] = 'If the source watermark image exceeds this width, it will be automatically scaled down. Set 0 or leave empty for default 800px.'; +$_['help_wm_max_height'] = 'If the source watermark image exceeds this height, it will be automatically scaled down. Set 0 or leave empty for default 800px.'; +$_['entry_disk_status'] = 'Enable Disk Space Scan & Graph'; +$_['help_disk_status'] = 'If enabled, the module will periodically scan the image folder size and display a pie chart. Disable this on large sites to improve settings page load speed.'; +$_['text_disk_settings_title'] = 'Disk Limits & Scanning Settings'; + diff --git a/upload/admin/language/ru-ru/module/img_opti.php b/upload/admin/language/ru-ru/module/img_opti.php new file mode 100644 index 0000000..28fd6a4 --- /dev/null +++ b/upload/admin/language/ru-ru/module/img_opti.php @@ -0,0 +1,565 @@ +Image Optimizer [NAT]: Сжатие оригиналов и удаление мусора'; +$_['text_extension'] = 'Расширения'; +$_['text_edit'] = 'Настройки модуля'; +$_['text_success'] = 'Настройки успешно сохранены!'; +$_['entry_warning'] = 'ВНИМАНИЕ: Обязательно сделайте бэкап файлов сайта и особенно папки /image/catalog перед началом работы!'; +$_['text_tech'] = 'Проверка технологий сервера:'; +$_['text_author'] = 'Техническая поддержка и пожелания: info@nat.od.ua'; +$_['tab_optimize'] = 'Оптимизация и Ресайз'; +$_['tab_cleaner'] = 'Очистка неиспользуемых изображений'; +$_['tab_info'] = 'Инфо / Экосистема [NAT]'; +$_['tab_settings'] = 'Настройки'; +$_['tab_tools'] = 'Инструменты (pro)'; +$_['tab_cron'] = 'Крон-задачи'; +$_['entry_mode'] = 'Режим работы'; +$_['text_copy'] = 'Создать копию (безопасно)'; +$_['text_replace'] = 'Заменять оригиналы (требует бэкап!)'; +$_['entry_folder'] = 'Имя папки для копии'; +$_['entry_batch'] = 'Файлов за проход'; +$_['entry_log_limit'] = 'Строк лога на экране'; +$_['entry_max_width'] = 'Макс. ширина (px) [0 - без лимита]'; +$_['entry_max_height'] = 'Макс. высота (px) [0 - без лимита]'; +$_['entry_jpg'] = 'Качество JPG (0-100)'; +$_['entry_png'] = 'Сжатие PNG (0-9)'; +$_['entry_webp'] = 'Качество WebP (0-100)'; +$_['entry_targets'] = 'Что оптимизировать?'; +$_['text_all_catalog'] = '[ Весь каталог image/catalog ]'; +$_['text_root_files'] = 'Файлы только в корне image/catalog'; +$_['entry_threshold'] = 'Порог замены (%)'; +$_['help_threshold'] = '0: заменять только если вес стал меньше. 10: разрешить увеличение веса до 10%.'; +$_['entry_token'] = 'Токен активации (на корень домена)'; +$_['entry_status'] = 'Статус модуля'; +$_['entry_menu_position'] = 'Позиция меню [NAT]'; +$_['text_menu_module_only'] = 'Только в модулях'; +$_['text_menu_header'] = 'В шапке (Header)'; +$_['text_menu_sidebar'] = 'В боковом меню (Sidebar)'; +$_['text_menu_both'] = 'Везде (Шапка + Сайдбар)'; +$_['text_status_on'] = 'Включено'; +$_['text_status_off'] = 'Отключено'; +$_['entry_engine'] = 'Движок обработки изображений'; +$_['text_engine_gd'] = 'GD (Стандарт)'; +$_['text_engine_imagick'] = 'Imagick (Высокое качество, сохранение профилей sRGB)'; +$_['entry_license'] = 'Лицензия'; +$_['text_cleaner_info'] = 'Этот инструмент ищет файлы, которые физически существуют на диске, но нигде не используются в базе данных или файлах шаблона.'; +$_['entry_clean_targets'] = 'В каких папках искать мусор?'; +$_['entry_extended_log'] = 'Расширенный лог (показать используемые фото и где они найдены)'; +$_['entry_process_broken'] = 'Учитывать файлы с битой кодировкой (имена с символом "?")'; +$_['button_scan_trash'] = 'НАЙТИ МУСОР'; +$_['button_quarantine'] = 'В Карантин (Безопасно)'; +$_['button_delete_trash'] = 'Удалить навсегда'; +$_['button_empty_quarantine'] = 'Очистить папку Карантина'; +$_['button_restore_quarantine'] = 'Вернуть из Карантина'; +$_['text_scan_result'] = 'Результаты сканирования'; +$_['text_trash_files'] = 'Мусорных файлов: '; +$_['text_trash_size'] = 'Потенциально освободится: '; +$_['button_start'] = 'СТАРТ ОПТИМИЗАЦИИ'; +$_['button_pause'] = 'ПАУЗА'; +$_['button_stop'] = 'ОСТАНОВИТЬ'; +$_['button_continue'] = 'ПРОДОЛЖИТЬ'; +$_['button_start_new'] = 'СТАРТ (Новый запуск)'; +$_['button_download_log'] = 'Скачать лог в TXT'; +$_['button_download'] = 'Скачать'; +$_['button_save'] = 'Сохранить'; +$_['button_cancel'] = 'Отмена'; +$_['text_progress'] = 'Прогресс:'; +$_['text_old_size'] = 'Было:'; +$_['text_new_size'] = 'Стало:'; +$_['text_savings'] = 'Экономия:'; +$_['text_detailed_log'] = 'Детальный лог'; +$_['text_page'] = 'Стр.'; +$_['text_waiting'] = 'Ожидание запуска...'; +$_['text_enabled'] = 'Включено'; +$_['text_disabled_error'] = 'Выключено (ОШИБКА)'; +$_['text_supported'] = 'Поддерживается'; +$_['text_no'] = 'Нет'; +$_['error_permission'] = 'У вас нет прав для управления модулем!'; +$_['error_license'] = 'Ошибка доступа: Неверный токен активации для этого домена!'; +$_['error_domain'] = ' (Домен: '; +$_['text_found'] = 'Найдено файлов: '; +$_['text_compressed'] = 'Сжат'; +$_['text_original'] = 'Оригинал'; +$_['text_error_process'] = 'Ошибка обработки: '; +$_['error_log_empty'] = 'Лог пуст! Сначала запустите процесс.'; +$_['text_report_header'] = '=== ОТЧЕТ ==='; +$_['text_total_files'] = 'Всего файлов: '; +$_['text_searching'] = 'Поиск файлов...'; +$_['error_ajax'] = 'Ошибка: '; +$_['text_resumed'] = '▶ ПРОЦЕСС ВОЗОБНОВЛЕН...'; +$_['text_paused'] = '⏸ ПАУЗА. Процесс приостановлен.'; +$_['confirm_stop'] = 'Вы уверены, что хотите полностью прервать процесс?'; +$_['confirm_delete'] = 'ВНИМАНИЕ! Файлы будут удалены с сервера навсегда без возможности восстановления! Продолжить?'; +$_['confirm_empty_q'] = 'Вы уверены, что хотите полностью очистить папку карантина?'; +$_['confirm_restore'] = 'Все файлы из папки карантина будут возвращены на свои места. Продолжить?'; +$_['button_stopping'] = 'Остановка...'; +$_['text_stopped_user'] = '🛑 ПРОЦЕСС ПРЕРВАН ПОЛЬЗОВАТЕЛЕМ!'; +$_['text_increase'] = 'Увеличение'; +$_['text_done'] = '✅ ГОТОВО! Процесс завершен.'; +$_['error_timeout'] = 'Таймаут сервера. Автоповтор через 3 секунды...'; +$_['text_building_index'] = 'Шаг 1/2: Индексация БД и шаблонов (поиск используемых фото)...'; +$_['text_comparing'] = 'Шаг 2/2: Сравнение физических файлов с индексом...'; +$_['text_quarantine_done'] = '✅ Процесс переноса в image/catalog_trash/ завершен!'; +$_['text_deleted_done'] = '✅ Процесс удаления мусора завершен!'; +$_['text_quarantine_empty'] = '✅ Папка карантина успешно удалена с сервера!'; +$_['text_restore_done'] = '✅ Файлы успешно возвращены из карантина в рабочую папку!'; +$_['text_quarantine_move'] = 'В карантине: '; +$_['text_deleted_file'] = 'Удален: '; +$_['text_no_trash'] = 'В выбранных папках мусор не найден! Все файлы используются.'; +$_['text_scanning_dirs'] = 'Сканируем директории: '; +$_['text_log_used'] = '[ИСПОЛЬЗУЕТСЯ]'; +$_['text_log_trash'] = '[МУСОР]'; +$_['text_log_deleted'] = '[УДАЛЕНО]'; +$_['text_log_del_err'] = '[ОШИБКА УДАЛЕНИЯ]'; +$_['text_log_quarantine'] = '[В КАРАНТИН]'; +$_['text_log_q_err'] = '[ОШИБКА ПЕРЕНОСА]'; +$_['text_log_restored'] = '[ВОССТАНОВЛЕНО]'; +$_['text_log_rest_err'] = '[ОШИБКА ВОССТАНОВЛЕНИЯ]'; +$_['text_used_in'] = 'в: '; +$_['text_analyzing'] = 'Анализ базы данных...'; +$_['text_clean_empty'] = 'Чисто! Мусора нет.'; +$_['text_processing'] = 'Обработка...'; +$_['text_restoring'] = 'Восстановление файлов...'; +$_['text_all_folders'] = 'image/catalog/ (И все вложенные)'; +$_['text_root_folder'] = 'image/catalog/ (Только корень)'; +$_['text_q_empty_err'] = 'Папка карантина пуста или не существует.'; +$_['error_token'] = 'Неверный токен. Перезагрузите страницу.'; +$_['text_apply_title'] = 'Оптимизация завершена в папку: '; +$_['text_apply_info'] = 'Проверьте результат. Если всё устраивает — примените изменения к основному каталогу.'; +$_['entry_apply_backup'] = 'Перенести с созданием бэкапа (в Карантин)'; +$_['entry_apply_replace'] = 'Заменить оригиналы безвозвратно'; +$_['button_apply_main'] = 'ПРИМЕНИТЬ К ОСНОВНОМУ КАТАЛОГУ'; +$_['button_delete_copies'] = 'Удалить временные копии (отмена)'; +$_['button_full_cleanup'] = 'Полная очистка всех временных папок и бэкапов'; +$_['text_apply_success'] = '✅ Файлы успешно перенесены. '; +$_['text_apply_backup_ok'] = 'Оригиналы сохранены в папку: '; +$_['text_apply_no_backup'] = 'Оригиналы удалены.'; +$_['text_error_source'] = 'Ошибка: Папка с оптимизированными файлами не найдена.'; +$_['confirm_full_cleanup'] = 'ВНИМАНИЕ! Это действие безвозвратно удалит ВСЕ папки бэкапов (backup_*) и временные папки с копиями. Продолжить?'; +$_['text_tools_desc'] = 'Глобальные сканеры и инструменты для работы с файловой системой и БД изображений.'; +$_['text_tool_select'] = 'Выберите инструмент:'; +$_['text_group_scanners'] = 'Сканеры и Анализ'; +$_['text_group_generators'] = 'Генераторы и Обработка'; +$_['text_group_experimental'] = 'Экспериментальные функции (BETA)'; +$_['text_experimental_warn'] = 'Внимание: Экспериментальные функции затрагивают системные файлы или базу данных. Настоятельно рекомендуется сделать бэкап!'; +$_['text_tool_broken_db'] = 'Сканер битых/пустых фото БД'; +$_['text_tool_watermark'] = 'Динамический Вотермарк (Только на кэш)'; +$_['text_tool_duplicates'] = 'Поиск дубликатов файлов (по MD5)'; +$_['text_tool_png_jpg'] = 'Конвертер тяжелых PNG в JPG (сканер файлов без прозрачности)'; +$_['text_tool_placeholders'] = 'Генератор заглушек (Вместо No Image)'; +$_['text_tool_exif'] = 'Очистка EXIF-данных (Геотеги/Мета)'; +$_['text_tool_small_photos'] = 'Сканер слишком мелких фото'; +$_['text_tool_html_broken'] = 'Поиск битых картинок в HTML (Описаниях)'; +$_['text_tool_empty_folders'] = 'Очистка пустых папок (image/catalog)'; +$_['text_tool_smart_cache'] = 'Умная очистка кэша изображений'; +$_['text_tool_folder_tree'] = 'Дерево папок (Статистика веса)'; +$_['text_tool_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load (Без БД)'; +$_['text_tool_translit'] = 'Транслитерация названий файлов'; +$_['text_tool_cache_restore'] = 'Воскрешение оригиналов из Кэша'; +$_['entry_wm_type'] = 'Тип вотермарка'; +$_['text_wm_image'] = 'Изображение (PNG)'; +$_['text_wm_text'] = 'Текст'; +$_['entry_wm_image_path'] = 'Путь к изображению'; +$_['entry_wm_text_val'] = 'Текст вотермарка'; +$_['entry_wm_position'] = 'Позиция (1-9)'; +$_['entry_wm_opacity'] = 'Прозрачность (0-100)'; +$_['entry_wm_category'] = 'Только для Категорий'; +$_['entry_wm_brand'] = 'Только для Брендов'; +$_['entry_min_width'] = 'Минимальная ширина (px)'; +$_['entry_min_height'] = 'Минимальная высота (px)'; +$_['entry_ph_product'] = 'Заглушка для Товаров'; +$_['entry_ph_category'] = 'Заглушка для Категорий'; +$_['entry_ph_brand'] = 'Заглушка для Брендов'; +$_['button_run_tool'] = 'Запустить инструмент'; +$_['button_scan_tool'] = 'Сканировать'; +$_['button_fix_tool'] = 'Исправить найденное'; +$_['text_th_path'] = 'Файл / Путь'; +$_['text_th_size'] = 'Размер'; +$_['text_th_action'] = 'Действие'; +$_['text_th_status'] = 'Статус'; +$_['text_th_problem'] = 'Проблема / Значение'; +$_['text_cron_title'] = 'Настройка Cron'; +$_['text_cron_desc'] = 'Добавьте команду в Cron хостинга для автоматического фонового сжатия.'; +$_['entry_cron_new_only'] = 'Обрабатывать только новые (необработанные) файлы'; +$_['entry_cron_folders'] = 'Папки для сканирования (оставьте пустым для всего каталога)'; +$_['text_promo_title'] = 'Другие модули серии [NAT]'; +$_['text_promo_desc'] = 'Модули серии [NAT] — это инструменты для глубокой оптимизации OpenCart. Мы фокусируемся на автоматизации рутины, ускорении базы данных и очистке серверов от мусора. Все модули имеют открытый исходный код и единую логику управления.'; +$_['text_support'] = 'Техническая поддержка и пожелания:'; +$_['button_more'] = 'Смотреть все модули на OpenCart.com'; +$_['button_more_forum'] = 'Opencartforum'; +$_['text_mod_cat_opt'] = 'Catalog Optimizer [NAT]'; +$_['text_mod_cat_opt_desc'] = 'Скрытие пустых категорий и брендов + SEO сортировка товаров (в наличии сверху) + Очистка мусора БД.'; +$_['text_mod_new_arrivals'] = 'New Arrivals [NAT]: Новинки по датам'; +$_['text_mod_new_arrivals_desc'] = 'Автоматическая SEO-страница новинок. Отображает новинки строго по реальной дате добавления. Слайдеры, сетка и умный сайдбар архива дат.'; +$_['text_mod_img_opt'] = 'Image Optimizer [NAT]'; +$_['text_mod_img_opt_desc'] = 'Текущий модуль. Сжатие и ресайз ОРИГИНАЛОВ фото (image/catalog) в WebP + Очистка хостинга от неиспользуемых изображений.'; +$_['text_mod_dom_scan'] = 'Domain Scanner [NAT]'; +$_['text_mod_dom_scan_desc'] = 'Поиск и чистка внешних ссылок и HTTP-картинок в БД и шаблонах (для переезда на HTTPS).'; +$_['text_mod_poll'] = 'Poll Widget [NAT]'; +$_['text_mod_poll_desc'] = 'Создание опросов и голосований с визуальным конструктором дизайна, глубокой статистикой и защитой от накруток.'; +$_['text_mod_db_opt'] = 'Database Optimizer [NAT]'; +$_['text_mod_db_opt_desc'] = 'Интеллектуальная очистка мусора в базе данных, управление системными логами и добавление критически важных индексов для ускорения.'; +$_['text_mod_im_ex'] = 'Import/Export [NAT]'; +$_['text_mod_im_ex_desc'] = 'Быстрый импорт/экспорт цен и остатков из файлов xls/xlsx/csv/xml/json или по ссылке. Маппинг полей, работа по Cron, текстовые функции.'; +$_['text_mod_admin_tools'] = 'Admin Tools [NAT]'; +$_['text_mod_admin_tools_desc'] = 'Удобные инструменты администратора: вход под клиентом, кастомизация админки, управление правами пользователей и скрытие ненужных пунктов меню.'; +$_['text_mod_utm_tracker'] = 'Order UTM Tracker [NAT]'; +$_['text_mod_utm_tracker_desc'] = 'Автоматическое отслеживание UTM-меток при оформлении заказа. Сохраняет источник перехода, кампанию, ключевые слова и выводит эти данные в заказе для аналитики рекламы.'; +$_['text_info_title_promo_top_bar'] = 'Promo Top Bar [NAT]: Верхний рекламный инфо-баннер, информационная полоса над шапкой, бегущая строка и топ-баннер'; +$_['text_info_desc_promo_top_bar'] = 'Создание интерактивных промо-панелей и слайд-шоу в шапке сайта с конструктором блоков, таймерами, формой подписки и гибким таргетингом.'; +$_['text_tool_no_image'] = 'Товары без главного фото (пусто в БД)'; +$_['text_problem_main_broken'] = 'Битое главное фото (файл удален)'; +$_['text_problem_add_broken'] = 'Битое доп. фото (файл удален)'; +$_['text_problem_no_image'] = 'Отсутствует главное фото'; +$_['text_err_dir'] = 'Ошибка: Директория не найдена или неверный путь.'; +$_['text_err_no_queue'] = 'Ошибка: Очередь не найдена. Сначала запустите сканирование.'; +$_['text_log_del_empty_folder'] = 'Удалена пустая папка: '; +$_['text_log_no_empty_folders'] = 'Пустых папок в image/catalog/ не найдено.'; +$_['text_log_total_empty_folders'] = 'Всего удалено пустых папок: '; +$_['text_log_cache_cleared'] = 'Кэш изображений успешно очищен.'; +$_['text_log_wm_smart_mode'] = 'Вотермарки работают в SMART-режиме: оригиналы не трогаются, логотип накладывается только при генерации кэша.'; +$_['text_log_scan_dupes'] = 'Сканирование image/catalog/ на дубликаты (по хешу MD5)... Это может занять время.'; +$_['text_log_found_dupe'] = 'Найден дубликат: '; +$_['text_log_no_dupes'] = 'Дубликаты не найдены.'; +$_['text_log_total_dupes'] = 'Найдено дубликатов: %s. Потенциальная экономия: %s'; +$_['text_log_fix_dupes_prompt'] = 'Нажмите "Исправить найденное" для перелинковки БД на оригинал и удаления дублей.'; +$_['text_log_fixed_dupe'] = 'Исправлен и удален дубликат: '; +$_['text_log_success_dupes'] = 'Успешно исправлено дубликатов: %s. Ссылки в БД обновлены.'; +$_['text_log_scan_png'] = 'Поиск тяжелых PNG файлов без прозрачности...'; +$_['text_log_no_png'] = 'Подходящих PNG файлов не найдено.'; +$_['text_log_total_png'] = 'Найдено PNG без прозрачности: %s. Нажмите "Исправить" для конвертации в JPG.'; +$_['text_log_converted_jpg'] = 'Сконвертировано в JPG: '; +$_['text_log_success_png'] = 'Успешно сконвертировано %s файлов. Освобождено: %s'; +$_['text_log_ph_prod'] = 'Обновлено товаров заглушкой: '; +$_['text_log_ph_cat'] = 'Обновлено категорий заглушкой: '; +$_['text_log_ph_brand'] = 'Обновлено брендов заглушкой: '; +$_['text_log_ph_empty'] = 'Заглушки не выбраны в настройках.'; +$_['text_log_scan_exif'] = 'Сканирование EXIF-данных (мета-теги камер)...'; +$_['text_log_stripped_exif'] = 'Очищен EXIF: '; +$_['text_log_no_exif'] = 'Файлов с EXIF для очистки не найдено.'; +$_['text_log_success_exif'] = 'Очищен EXIF у %s файлов. Освобождено: %s'; +$_['text_log_scan_small'] = 'Поиск фото размером менее %sx%s px...'; +$_['text_log_too_small'] = 'Слишком маленькое (%sx%s): '; +$_['text_log_no_small'] = 'Мелких фото не найдено.'; +$_['text_log_total_small'] = 'Найдено мелких фото: %s. Пожалуйста, замените их на качественные.'; +$_['text_log_html_broken'] = 'Битый IMG в HTML таблицы %s (ID: %s): '; +$_['text_log_no_html_broken'] = 'Битых картинок в HTML-описаниях не найдено.'; +$_['text_log_total_html_broken'] = 'Найдено битых ссылок в HTML: %s. Исправьте их вручную в редакторе.'; +$_['text_log_scan_smart_cache'] = 'Поиск потерянных файлов в image/cache/...'; +$_['text_log_del_orph_cache'] = 'Удален потерянный кэш: '; +$_['text_log_no_orph_cache'] = 'Потерянного кэша не найдено.'; +$_['text_log_success_smart_cache'] = 'Удалено файлов потерянного кэша: %s. Освобождено: %s'; +$_['text_log_scan_translit'] = 'Поиск файлов с кириллицей или пробелами...'; +$_['text_log_no_bad_names'] = 'Файлов с некорректными именами не найдено.'; +$_['text_log_total_bad_names'] = 'Найдено файлов с кириллицей/пробелами: %s. Нажмите "Исправить" для транслитерации и обновления БД.'; +$_['text_log_renamed'] = 'Переименовано: '; +$_['text_log_success_translit'] = 'Успешно транслитерировано файлов: %s. БД обновлена.'; +$_['text_log_scan_restore'] = 'Сканирование image/cache/ для восстановления утерянных оригиналов...'; +$_['text_log_restored_cache'] = 'Восстановлено из кэша: '; +$_['text_log_no_lost_orig'] = 'Отсутствующих оригиналов в кэше не найдено.'; +$_['text_log_success_restore'] = 'Успешно восстановлено оригиналов из кэша: %s.'; +$_['error_invalid_folder'] = 'Ошибка: Недопустимое имя папки!'; +$_['text_formats_title'] = 'Современные форматы (WebP & SVG)'; +$_['text_formats_desc'] = 'Настройки интеграции современных форматов в файловую систему и шаблоны OpenCart.'; +$_['entry_support_svg'] = 'Поддержка загрузки SVG'; +$_['help_support_svg'] = 'Разрешает загрузку векторных SVG-изображений через стандартный Filemanager и их безопасный вывод на витрине без искажений размеров.'; +$_['entry_support_webp'] = 'Поддержка загрузки WebP'; +$_['help_support_webp'] = 'Позволяет вручную загружать, подвязывать и выводить на сайте готовые файлы .webp через Filemanager.'; +$_['entry_truefilemanager_webp'] = 'WebP в TrueFileManager'; +$_['help_truefilemanager_webp'] = 'Добавляет поддержку загрузки, предпросмотра и эскизов WebP в TrueFileManager от sitecreator (если он установлен).'; +$_['entry_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load'; +$_['help_webp_on_fly'] = 'Сверхбыстрая подмена: автоматически меняет расширения .jpg/.png на .webp прямо в HTML-выводе витрины и добавляет атрибут loading="lazy". Не трогает базу данных!'; +$_['help_tool_broken_db'] = 'Ищет в базе данных картинки, физические файлы которых были удалены с сервера. Позволяет очистить битые ссылки.'; +$_['help_tool_empty_folders'] = 'Рекурсивно сканирует директорию image/catalog/ и безопасно удаляет все папки, в которых нет ни одного файла.'; +$_['help_tool_folder_tree'] = 'Строит дерево каталогов и показывает реальный вес каждой папки для поиска "пожирателей" места.'; +$_['help_tool_html_broken'] = 'Сканирует HTML-описания товаров и категорий на наличие тегов с несуществующими картинками.'; +$_['help_tool_small_photos'] = 'Находит картинки с разрешением ниже заданного (например, меньше 300x300 px). Добавлены ссылки на поиск качественных аналогов в Google Images и Яндекс Картинках.'; +$_['help_tool_duplicates'] = 'Ищет полные дубликаты файлов по MD5 хешу (разные имена, но одинаковая картинка). Позволяет склеить их в один файл и обновить БД.'; +$_['help_tool_watermark'] = 'Наложение водяных знаков в SMART-режиме (только на кэш витрины, оригиналы не изменяются).'; +$_['help_tool_placeholders'] = 'Массово устанавливает выбранную картинку-заглушку для товаров и категорий, у которых вообще нет фото.'; +$_['help_tool_png_jpg'] = 'Инструмент сканирует папку изображений и находит тяжелые PNG-файлы, которые не имеют прозрачного фона (альфа-канала). Поскольку прозрачность им не нужна, их можно безопасно конвертировать в формат JPG. Это значительно облегчает файлы (в среднем на 70-80%) и ускоряет загрузку сайта, а все ссылки в базе данных автоматически обновляются.'; +$_['help_tool_exif'] = 'Сканирует JPEG-файлы и удаляет из них скрытые EXIF-данные (геотеги, данные о камере), уменьшая вес на 10-15%.'; +$_['help_tool_smart_cache'] = 'Ищет в папке image/cache/ кэшированные файлы, оригиналы которых уже удалены из image/catalog/, и стирает этот мусор.'; +$_['help_tool_translit'] = 'Находит файлы с кириллицей или пробелами в названиях, переименовывает их транслитом (tovar_1.jpg) и обновляет пути в БД.'; +$_['help_tool_cache_restore'] = 'Если вы случайно удалили оригинал из catalog, инструмент попытается найти его копию в cache и восстановить обратно.'; +$_['entry_quarantine'] = 'Переместить оригиналы в Карантин (вместо удаления)'; +$_['entry_backup_db'] = 'Создать бэкап изменяемых таблиц в SQL перед запуском'; +$_['button_fix_selected'] = 'ИСПРАВИТЬ ВЫБРАННЫЕ'; +$_['button_export_log'] = 'Экспортировать в CSV'; +$_['text_th_before'] = 'Состояние ДО'; +$_['text_th_after'] = 'Ожидаемый результат ПОСЛЕ'; +$_['text_th_select'] = 'Действие'; +$_['text_th_preview'] = 'Предпросмотр'; +$_['text_th_name'] = 'Объект / Файл'; +$_['text_global_action'] = 'Групповое действие для выбранных:'; +$_['text_action_clear'] = 'Очистить ссылку в БД'; +$_['text_action_disable'] = 'Отключить (Статус = 0)'; +$_['text_action_stock'] = 'Установить "Нет в наличии"'; +$_['text_action_placeholder'] = 'Установить заглушку'; +$_['text_action_ignore'] = 'Ничего не делать'; +$_['text_action_merge'] = 'Склеить (удалить дубликат, оставить оригинал)'; +$_['text_action_convert'] = 'Конвертировать в JPG'; +$_['text_action_rename'] = 'Переименовать транслитом'; +$_['text_action_delete'] = 'Удалить'; +$_['text_action_restore'] = 'Восстановить'; +$_['text_db_backup_created'] = 'Создан бэкап БД: %s'; +$_['text_quarantine_created'] = 'Перенесено в карантин: %s'; +$_['text_fix_status_success'] = 'Успешно выполнено'; +$_['text_fix_status_error'] = 'Ошибка: %s'; +$_['text_log_exported'] = 'Лог успешно экспортирован!'; +$_['text_select_action'] = '-- Выберите действие --'; +$_['error_empty_selection'] = 'Ошибка: Не выбрано ни одной строки для исправления!'; +$_['text_link_admin'] = 'Админка'; +$_['text_link_catalog'] = 'Витрина'; +$_['entry_threads'] = 'Потоков обработки'; +$_['text_threads_optimal'] = 'оптимально'; +$_['button_download_zip'] = 'Скачать ZIP-архив и удалить папку'; +$_['confirm_download_and_delete'] = 'Архив успешно скачался? Можно удалить временную папку копий на сервере?'; +$_['button_clear_backups'] = 'Удалить бэкапы БД'; +$_['button_clear_backups_short'] = 'Очистить бэкапы'; +$_['confirm_clear_backups'] = 'Вы уверены, что хотите полностью удалить все SQL-бэкапы таблиц?'; +$_['text_compare_title'] = 'Сравнение изображений ДО / ПОСЛЕ'; +$_['text_compare_before'] = 'ДО'; +$_['text_compare_after'] = 'ПОСЛЕ'; +$_['text_backups_cleared'] = 'Все SQL-бэкапы таблиц успешно удалены!'; +$_['error_no_backups'] = 'Бэкапы не найдены или папка пуста.'; +$_['button_restore'] = 'Вернуть'; +$_['button_empty'] = 'Очистить'; +$_['entry_wm_targets'] = 'Применять водяной знак к:'; +$_['text_wm_target_product'] = 'Фото товаров'; +$_['text_wm_target_category'] = 'Фото категорий'; +$_['text_wm_target_brand'] = 'Логотипы брендов'; +$_['text_wm_target_banner'] = 'Баннеры и слайды'; +$_['text_wm_target_blog'] = 'Статьи / Блог'; +$_['error_no_scan_data'] = 'Ошибка: Данные сканирования не найдены. Сначала запустите сканирование!'; +$_['error_row_not_found'] = 'Ошибка: Объект не найден в результатах сканирования!'; +$_['error_file_not_found'] = 'Ошибка: Файл не найден на сервере!'; +$_['text_folder_rules_title'] = 'Индивидуальные правила сжатия для папок'; +$_['text_folder_rules_desc'] = 'Вы можете переопределить качество и максимальные размеры для конкретных папок. Если оставить поле пустым, будет использоваться глобальная настройка. Путь указывается относительно папки image (например, catalog/banners/).'; +$_['entry_folder_path'] = 'Путь к папке'; +$_['entry_jpg_quality'] = 'Качество JPG'; +$_['entry_webp_quality'] = 'Качество WebP'; +$_['button_add_rule'] = 'Добавить правило'; +$_['button_remove'] = 'Удалить'; +$_['entry_wm_angle'] = 'Угол наклона водяного знака (градусы)'; +$_['entry_wm_corner_radius'] = 'Скругление углов водяного знака (px)'; +$_['entry_wm_size_type'] = 'Масштабирование водяного знака'; +$_['entry_wm_size_percent'] = 'Размер водяного знака (% от ширины изображения)'; +$_['entry_wm_text_color'] = 'Цвет текста водяного знака'; +$_['entry_wm_text_font'] = 'Шрифт водяного знака (TTF)'; +$_['entry_wm_filter_mode'] = 'Режим фильтрации Категорий/Брендов'; +$_['text_wm_filter_include'] = 'Только для выбранных'; +$_['text_wm_filter_exclude'] = 'Для всех, кроме выбранных'; +$_['text_wm_size_original'] = 'Оригинальный размер'; +$_['text_wm_size_percent'] = 'Пропорционально ширине изображения (%)'; +$_['button_preview_watermark'] = 'Предпросмотр водяного знака'; +$_['text_wm_preview_title'] = 'Предпросмотр водяного знака'; +$_['text_cron_exclude_folders'] = 'Исключить папки из сканирования'; +$_['text_cron_entities'] = 'Какие типы фото обрабатывать?'; +$_['text_cron_quality_override'] = 'Переопределить параметры сжатия для Крона'; +$_['text_cron_recommendations'] = 'Рекомендации по расписанию запуска Cron'; +$_['text_cron_recommendations_desc'] = '
    • Для небольших сайтов (до 5000 товаров) рекомендуется запускать Крон каждый час (0 * * * *).
    • Для средних сайтов (5000-20000 товаров) рекомендуется запускать раз в 3-6 часов (0 */6 * * *).
    • Для крупных сайтов (более 20000 товаров) — один раз в сутки ночью (например, в 2:00: 0 2 * * *).
    '; +$_['text_tool_broken_files'] = 'Сканер битых/пустых файлов на сервере'; +$_['help_tool_broken_files'] = 'Сканирует физические файлы в папке image/catalog/ на наличие поврежденных изображений или пустых файлов размером 0 байт.'; +$_['text_grid_broken_file'] = 'Битый или пустой файл на сервере'; +$_['text_grid_broken_file_after'] = 'Удаление или замена заглушкой'; +$_['text_description_broken_files'] = 'Результаты сканирования физических файлов в папке image/catalog/. Здесь отображаются битые (поврежденные) изображения или файлы размером 0 байт.'; +$_['text_action_strip'] = 'Очистить EXIF'; +$_['text_action_set_placeholder'] = 'Установить заглушку'; +$_['text_th_entity'] = 'Объект / Связанные ссылки'; +$_['text_cron_entity_product'] = 'Товары'; +$_['text_cron_entity_category'] = 'Категории'; +$_['text_cron_entity_manufacturer'] = 'Производители / Бренды'; +$_['text_cron_entity_banner'] = 'Баннеры'; +$_['text_cron_entity_blog'] = 'Блог / Статьи'; +$_['entry_cron_jpg_quality'] = 'Качество JPG (Крон)'; +$_['entry_cron_webp_quality'] = 'Качество WebP (Крон)'; +$_['entry_cron_max_width'] = 'Макс. ширина px (Крон)'; +$_['entry_cron_max_height'] = 'Макс. высота px (Крон)'; +$_['text_cron_folder_scan'] = 'Папки для сканирования'; +$_['text_no_fonts'] = 'TTF-шрифты не найдены в system/library/font/'; +$_['button_apply'] = 'Применить'; +$_['text_backup_will_be_created'] = 'Резервная копия базы данных будет автоматически создана перед выполнением этого действия.'; +$_['text_bulk_action'] = 'Групповое действие для выбранных'; +$_['text_grid_results'] = 'Результаты и детали сканирования'; +$_['text_quarantine_will_be_created'] = 'Оригинальные файлы будут перемещены в карантин (image/catalog_trash/) перед изменением.'; +$_['text_success_apply'] = 'Изменения успешно применены!'; +$_['text_description_broken'] = 'Результаты сканирования базы данных на наличие битых ссылок (записи в таблицах с несуществующими файлами).'; +$_['text_description_cache_restore'] = 'Результаты поиска файлов в кэше, оригиналы которых отсутствуют в каталоге.'; +$_['text_description_duplicates'] = 'Результаты поиска полных дубликатов изображений по MD5-хешу.'; +$_['text_description_empty_folders'] = 'Список пустых папок в директории image/catalog/ для удаления.'; +$_['text_description_exif'] = 'Результаты поиска изображений с метаданными EXIF, которые можно очистить.'; +$_['text_description_folder_tree'] = 'Статистика размера папок в каталоге image/catalog/.'; +$_['text_description_html_broken'] = 'Результаты поиска несуществующих файлов картинок, на которые ссылаются описания в HTML.'; +$_['text_description_placeholders'] = 'Результаты поиска сущностей без изображений для заполнения заглушками.'; +$_['text_description_png_jpg'] = 'Результаты поиска тяжелых изображений PNG без прозрачности, которые можно пережать в JPG.'; +$_['text_description_small_photos'] = 'Список изображений, размеры которых меньше минимально заданных (%sx%s px).'; +$_['text_description_smart_cache'] = 'Результаты поиска файлов кэша, оригиналы которых уже были удалены.'; +$_['text_description_translit'] = 'Результаты поиска файлов с кириллицей или пробелами в названиях.'; +$_['text_grid_broken_db_after'] = 'Очистить ссылку или установить заглушку'; +$_['text_grid_dupe_after'] = 'Оригинал (остаётся)'; +$_['text_grid_dupe_before'] = 'Дубликат (будет удалён)'; +$_['text_grid_empty_folder'] = 'Пустая папка'; +$_['text_grid_empty_folder_after'] = 'Удалить пустую папку'; +$_['text_grid_exif_after'] = 'Очистить метаданные'; +$_['text_grid_exif_before'] = 'Содержит EXIF'; +$_['text_grid_html_after'] = 'Исправить ссылку вручную'; +$_['text_grid_missing_file'] = 'Файл отсутствует'; +$_['text_grid_missing_html'] = 'Картинка не найдена в описании'; +$_['text_grid_missing_orig'] = 'Оригинал отсутствует'; +$_['text_grid_missing_orig_after'] = 'Восстановить оригинал из кэша'; +$_['text_grid_no_image'] = 'Нет изображения (пусто)'; +$_['text_grid_orph_cache'] = 'Осиротевший кэш'; +$_['text_grid_orph_cache_after'] = 'Удалить ненужный кэш'; +$_['text_grid_ph_brand_after'] = 'Установить заглушку бренда'; +$_['text_grid_ph_category_after'] = 'Установить заглушку категории'; +$_['text_grid_ph_product_after'] = 'Установить заглушку товара'; +$_['text_grid_small_photo_after'] = 'Рекомендуется заменить на более крупное'; +$_['text_grid_too_small'] = 'Слишком маленькое'; +$_['text_no_relations'] = 'Нет связей в БД'; +$_['text_action_archive'] = 'Архивировать (в ZIP)'; +$_['text_action_archive_delete'] = 'Архивировать + Удалить'; +$_['text_tool_sanitizer'] = 'Санитайзер имен файлов'; +$_['help_tool_sanitizer'] = 'Массовая очистка имен файлов и папок от кириллицы, пробелов и спецсимволов, приведение расширений к единому виду.'; +$_['text_description_sanitizer'] = 'Результаты анализа имен файлов для санитаризации по выбранным правилам.'; +$_['entry_sz_translit'] = 'Транслитерация (кириллица в латиницу)'; +$_['entry_sz_spaces'] = 'Заменять пробелы на символ "_"'; +$_['entry_sz_special'] = 'Удалять специальные символы'; +$_['entry_sz_lowercase_name'] = 'Имя файла в нижний регистр'; +$_['entry_sz_lowercase_ext'] = 'Расширение в нижний регистр'; +$_['entry_sz_normalize_ext'] = 'Нормализовать расширения (jpeg -> jpg)'; +$_['entry_cron_folder_mode'] = 'Режим фильтрации папок'; +$_['text_cron_folder_mode_exclude'] = 'Сканировать все, кроме выбранных (Исключения)'; +$_['text_cron_folder_mode_include'] = 'Сканировать только выбранные папки'; +$_['button_select_all'] = 'Выбрать все'; +$_['button_deselect_all'] = 'Снять все'; +$_['entry_wm_upload_font'] = 'Загрузить свой шрифт TTF'; +$_['button_upload_font'] = 'Загрузить шрифт'; +$_['text_success_font_upload'] = 'Шрифт успешно загружен!'; +$_['error_font_upload'] = 'Ошибка загрузки шрифта! Разрешены только файлы .ttf весом до 5 МБ.'; +$_['entry_lazy_load'] = 'Прогрессивная ленивая загрузка'; +$_['help_lazy_load'] = 'Включает премиальную прогрессивную отложенную загрузку на витрине. Изображения заменяются микро-превью и плавно размываются (blur-up), подгружаясь по мере прокрутки.'; +$_['confirm_replace_mode'] = 'ВНИМАНИЕ! Вы выбрали режим «Заменять оригиналы». Все изображения будут перезаписаны прямо на сервере. Настоятельно рекомендуем сделать резервную копию. Вы уверены, что хотите продолжить?'; +$_['text_compare'] = 'Сравнить'; +$_['text_home'] = 'Главная'; +$_['text_tech_gd'] = 'Библиотека GD'; +$_['text_tech_imagick'] = 'Imagick'; +$_['text_tech_webp'] = 'Поддержка WebP'; +$_['text_position_1'] = '1 - Слева вверху'; +$_['text_position_2'] = '2 - По центру вверху'; +$_['text_position_3'] = '3 - Справа вверху'; +$_['text_position_4'] = '4 - Слева посередине'; +$_['text_position_5'] = '5 - По центру'; +$_['text_position_6'] = '6 - Справа посередине'; +$_['text_position_7'] = '7 - Слева внизу'; +$_['text_position_8'] = '8 - По центру внизу'; +$_['text_position_9'] = '9 - Справа внизу'; + +$_['entry_log_level'] = 'Уровень лога'; +$_['text_log_changed_only'] = 'Только изменённые'; +$_['text_log_all'] = 'Все файлы (подробно)'; +$_['text_skipped_no_gain'] = 'Пропущен (нет улучшения)'; +$_['text_reset_cache_hint'] = 'Сбросить кэш оптимизации (полный пересканирование)'; +$_['confirm_reset_cache'] = 'Сбросить кэш оптимизации? Следующее сканирование проверит ВСЕ файлы заново.'; +$_['text_cache_cleared'] = 'Кэш оптимизации очищен. Следующее сканирование проверит все файлы.'; +$_['error_wm_preview'] = 'Ошибка генерации превью водяного знака.'; +$_['error_wm_too_large'] = 'Изображение водяного знака слишком большое (%dx%d px)! Максимально допустимый размер: %dx%d px.'; +$_['text_apply_errors_warning'] = 'ВНИМАНИЕ: Некоторые файлы не удалось заменить (заблокированы или нет прав):'; +$_['text_action_compress'] = 'Сжать изображения'; +$_['text_estimated'] = 'Прогноз'; +$_['entry_min_savings_bytes'] = 'Мин. сжатие (байт)'; +$_['entry_min_savings_percent'] = 'Мин. сжатие (%)'; +$_['text_optimized'] = 'Оптимизировано'; +$_['entry_broken_fallback'] = 'Заглушка для битых фото'; +$_['help_broken_fallback'] = 'Автоматически заменяет отсутствующие или поврежденные изображения каталога на витрине на изображение-заглушку.'; +$_['entry_fallback_image'] = 'Изображение-заглушка'; +$_['help_fallback_image'] = 'Выберите изображение, которое будет показано, если оригинальный файл картинки товара или категории отсутствует. По умолчанию используется no_image.png.'; +$_['text_tool_cmyk'] = 'Конвертер CMYK в RGB'; +$_['help_tool_cmyk'] = 'Конвертация JPEG изображений из цветового профиля CMYK в sRGB для корректного отображения в браузерах Safari и на устройствах iOS.'; +$_['text_tool_heavy_files'] = 'Поиск тяжелых файлов'; +$_['help_tool_heavy_files'] = 'Поиск самых тяжелых оригинальных изображений в каталоге с возможностью их быстрого сжатия прямо здесь.'; +$_['entry_heavy_files_limit'] = 'Макс. количество найденных файлов (всего)'; +$_['text_action_convert_rgb'] = 'Конвертировать в RGB'; +$_['text_description_cmyk'] = 'Найдены файлы в цветовом пространстве CMYK. Конвертация в sRGB гарантирует корректное отображение на всех платформах, включая iOS/macOS.'; +$_['text_description_heavy'] = 'Список из %d самых тяжелых файлов в каталоге изображений. Вы можете оптимизировать их прямо здесь.'; +$_['text_cron_folder_hint_exclude_all'] = 'Статус: Будут сканироваться все папки (исключения не выбраны).'; +$_['text_cron_folder_hint_exclude_some'] = 'Статус: Будут сканироваться все папки, КРОМЕ %d отмеченных.'; +$_['text_cron_folder_hint_include_some'] = 'Статус: Будут сканироваться только %d отмеченных папок.'; +$_['text_cron_folder_hint_include_none'] = 'ВНИМАНИЕ: Ни одна папка не выбрана! Крон ничего не будет сканировать.'; +$_['text_action_apply_changes'] = 'Применить указанные изменения'; +$_['entry_wm_status'] = 'Статус водяного знака'; +$_['text_disabled'] = 'Отключено'; +$_['entry_rule_name'] = 'Название правила'; +$_['entry_rule_status'] = 'Статус правила'; +$_['entry_action'] = 'Действие'; +$_['entry_ph_status'] = 'Статус заглушек'; +$_['entry_sanitizer_limit'] = 'Макс. файлов для поиска (лимит)'; +$_['entry_heavy_files_min_size'] = 'Минимальный размер'; +$_['entry_png_jpg_limit'] = 'Макс. файлов для поиска (лимит)'; +$_['entry_png_jpg_min_size'] = 'Минимальный размер'; +$_['text_wm_rule_title'] = 'Правило водяного знака'; +$_['button_close'] = 'Закрыть'; +$_['text_ph_rule_title'] = 'Правило заглушки'; +$_['entry_ph_image'] = 'Изображение заглушки'; +$_['button_clear_cache_wm'] = 'Очистить кэш (применить водяной знак)'; +$_['text_active'] = 'Активна'; +$_['text_error'] = 'Не активна'; +$_['text_th_dupe_delete'] = 'Файл для удаления (Дубликат)'; +$_['text_th_dupe_keep'] = 'Файл для сохранения (Оригинал)'; +$_['text_dupe_original_label'] = 'Оригинал'; +$_['text_dupe_duplicate_label'] = 'Дубликат'; + +$_['text_disk_usage'] = 'Использование диска'; +$_['text_cumulative_stats'] = 'Статистика оптимизации'; +$_['text_disk_catalog'] = 'Каталог изображений'; +$_['text_disk_cache'] = 'Кэш OC'; +$_['text_disk_trash'] = 'Корзина модуля'; +$_['text_disk_other'] = 'Прочее'; +$_['text_disk_free'] = 'Свободно'; +$_['text_total_files_opt'] = 'Файлов обработано'; +$_['text_total_saved'] = 'Всего сэкономлено'; +$_['text_avg_saving_pct'] = 'Средняя экономия'; +$_['text_stats_hint'] = 'Статистика накапливается по всем сессиям (хранится локально в браузере)'; +$_['text_reset_stats'] = 'Сбросить статистику'; +$_['text_reset_stats_confirm']= 'Сбросить накопленную статистику оптимизации?'; +$_['text_export_import'] = 'Экспорт / Импорт настроек'; +$_['button_export_settings'] = 'Экспорт настроек (JSON)'; +$_['button_import_settings'] = 'Импортировать настройки'; +$_['help_export_settings'] = 'Скачать все настройки модуля в JSON-файл'; +$_['help_import_settings'] = 'Загрузить ранее экспортированный JSON для восстановления настроек'; +$_['text_import_success'] = 'Настройки успешно импортированы. Перезагрузите страницу для применения.'; +$_['error_import_file'] = 'Файл не загружен или ошибка загрузки'; +$_['error_import_invalid'] = 'Некорректный файл настроек (неверный модуль или формат)'; +$_['text_click_to_load'] = 'Нажмите ↑ для загрузки данных диска'; +$_['tab_formats'] = 'Форматы изображений'; + +$_['text_disk_quota_legend'] = 'Лимиты диска хостинга (вручную)'; +$_['text_disk_quota_desc'] = 'Если график дискового пространства показывает неверные данные (физический диск сервера вместо лимита вашего хостинга), вы можете указать лимиты вашего тарифа вручную. Укажите 0, чтобы использовать автоопределение.'; +$_['entry_disk_quota_total'] = 'Выделено диска на хостинге (ГБ)'; +$_['entry_disk_quota_used'] = 'Занято диска на хостинге всего (ГБ)'; +$_['entry_auto_disk'] = 'Автообновление при входе'; + +// Cron Tasks +$_['entry_cron_task_name'] = 'Название задачи'; +$_['entry_cron_task_summary'] = 'Параметры сжатия'; +$_['button_add_cron_task'] = 'Добавить задачу'; +$_['button_copy_cron_url'] = 'Копировать URL'; +$_['text_cron_recommended_url'] = 'URL для запуска задачи крона:'; + +// Licensing +$_['text_license_required'] = 'Требуется лицензионный ключ'; +$_['text_license_required_desc'] = 'Пожалуйста, введите лицензионный ключ для активации модуля. Все функции настроек и оптимизации заблокированы до ввода валидного ключа.'; +$_['entry_token_desc'] = 'Введите токен активации, выданный для вашего основного домена. Вы можете получить его в личном кабинете на форуме или обратившись в поддержку.'; +$_['button_activate'] = 'Активировать модуль'; +$_['text_support'] = 'Техническая поддержка'; +$_['text_support_desc'] = 'Если у вас ещё нет ключа или возникли проблемы, пожалуйста, сообщите номер вашего заказа и домен.'; + +// New settings +$_['entry_wm_max_width'] = 'Макс. ширина водяного знака (px)'; +$_['entry_wm_max_height'] = 'Макс. высота водяного знака (px)'; +$_['help_wm_max_width'] = 'Если исходное изображение водяного знака превышает эту ширину, оно будет автоматически масштабировано. Укажите 0 или оставьте пустым для значения по умолчанию (800px).'; +$_['help_wm_max_height'] = 'Если исходное изображение водяного знака превышает эту высоту, оно будет автоматически масштабировано. Укажите 0 или оставьте пустым для значения по умолчанию (800px).'; +$_['entry_disk_status'] = 'Включить сканирование диска и график'; +$_['help_disk_status'] = 'Если включено, модуль будет периодически сканировать объем папки картинок и отображать круговую диаграмму. Рекомендуется выключить на больших сайтах для ускорения загрузки панели.'; +$_['text_disk_settings_title'] = 'Настройка лимитов и сканирования диска'; + diff --git a/upload/admin/language/russian/module/img_opti.php b/upload/admin/language/russian/module/img_opti.php new file mode 100644 index 0000000..62bdf87 --- /dev/null +++ b/upload/admin/language/russian/module/img_opti.php @@ -0,0 +1,564 @@ +Image Optimizer [NAT]: Сжатие оригиналов и удаление мусора'; +$_['text_extension'] = 'Расширения'; +$_['text_edit'] = 'Настройки модуля'; +$_['text_success'] = 'Настройки успешно сохранены!'; +$_['entry_warning'] = 'ВНИМАНИЕ: Обязательно сделайте бэкап файлов сайта и особенно папки /image/catalog перед началом работы!'; +$_['text_tech'] = 'Проверка технологий сервера:'; +$_['text_author'] = 'Техническая поддержка и пожелания: info@nat.od.ua'; +$_['tab_optimize'] = 'Оптимизация и Ресайз'; +$_['tab_cleaner'] = 'Очистка неиспользуемых изображений'; +$_['tab_info'] = 'Инфо / Экосистема [NAT]'; +$_['tab_settings'] = 'Настройки'; +$_['tab_tools'] = 'Инструменты (pro)'; +$_['tab_cron'] = 'Крон-задачи'; +$_['entry_mode'] = 'Режим работы'; +$_['text_copy'] = 'Создать копию (безопасно)'; +$_['text_replace'] = 'Заменять оригиналы (требует бэкап!)'; +$_['entry_folder'] = 'Имя папки для копии'; +$_['entry_batch'] = 'Файлов за проход'; +$_['entry_log_limit'] = 'Строк лога на экране'; +$_['entry_max_width'] = 'Макс. ширина (px) [0 - без лимита]'; +$_['entry_max_height'] = 'Макс. высота (px) [0 - без лимита]'; +$_['entry_jpg'] = 'Качество JPG (0-100)'; +$_['entry_png'] = 'Сжатие PNG (0-9)'; +$_['entry_webp'] = 'Качество WebP (0-100)'; +$_['entry_targets'] = 'Что оптимизировать?'; +$_['text_all_catalog'] = '[ Весь каталог image/catalog ]'; +$_['text_root_files'] = 'Файлы только в корне image/catalog'; +$_['entry_threshold'] = 'Порог замены (%)'; +$_['help_threshold'] = '0: заменять только если вес стал меньше. 10: разрешить увеличение веса до 10%.'; +$_['entry_token'] = 'Токен активации (на корень домена)'; +$_['entry_status'] = 'Статус модуля'; +$_['entry_menu_position'] = 'Позиция меню [NAT]'; +$_['text_menu_module_only'] = 'Только в модулях'; +$_['text_menu_header'] = 'В шапке (Header)'; +$_['text_menu_sidebar'] = 'В боковом меню (Sidebar)'; +$_['text_menu_both'] = 'Везде (Шапка + Сайдбар)'; +$_['text_status_on'] = 'Включено'; +$_['text_status_off'] = 'Отключено'; +$_['entry_engine'] = 'Движок обработки изображений'; +$_['text_engine_gd'] = 'GD (Стандарт)'; +$_['text_engine_imagick'] = 'Imagick (Высокое качество, сохранение профилей sRGB)'; +$_['entry_license'] = 'Лицензия'; +$_['text_cleaner_info'] = 'Этот инструмент ищет файлы, которые физически существуют на диске, но нигде не используются в базе данных или файлах шаблона.'; +$_['entry_clean_targets'] = 'В каких папках искать мусор?'; +$_['entry_extended_log'] = 'Расширенный лог (показать используемые фото и где они найдены)'; +$_['entry_process_broken'] = 'Учитывать файлы с битой кодировкой (имена с символом "?")'; +$_['button_scan_trash'] = 'НАЙТИ МУСОР'; +$_['button_quarantine'] = 'В Карантин (Безопасно)'; +$_['button_delete_trash'] = 'Удалить навсегда'; +$_['button_empty_quarantine'] = 'Очистить папку Карантина'; +$_['button_restore_quarantine'] = 'Вернуть из Карантина'; +$_['text_scan_result'] = 'Результаты сканирования'; +$_['text_trash_files'] = 'Мусорных файлов: '; +$_['text_trash_size'] = 'Потенциально освободится: '; +$_['button_start'] = 'СТАРТ ОПТИМИЗАЦИИ'; +$_['button_pause'] = 'ПАУЗА'; +$_['button_stop'] = 'ОСТАНОВИТЬ'; +$_['button_continue'] = 'ПРОДОЛЖИТЬ'; +$_['button_start_new'] = 'СТАРТ (Новый запуск)'; +$_['button_download_log'] = 'Скачать лог в TXT'; +$_['button_download'] = 'Скачать'; +$_['button_save'] = 'Сохранить'; +$_['button_cancel'] = 'Отмена'; +$_['text_progress'] = 'Прогресс:'; +$_['text_old_size'] = 'Было:'; +$_['text_new_size'] = 'Стало:'; +$_['text_savings'] = 'Экономия:'; +$_['text_detailed_log'] = 'Детальный лог'; +$_['text_page'] = 'Стр.'; +$_['text_waiting'] = 'Ожидание запуска...'; +$_['text_enabled'] = 'Включено'; +$_['text_disabled_error'] = 'Выключено (ОШИБКА)'; +$_['text_supported'] = 'Поддерживается'; +$_['text_no'] = 'Нет'; +$_['error_permission'] = 'У вас нет прав для управления модулем!'; +$_['error_license'] = 'Ошибка доступа: Неверный токен активации для этого домена!'; +$_['error_domain'] = ' (Домен: '; +$_['text_found'] = 'Найдено файлов: '; +$_['text_compressed'] = 'Сжат'; +$_['text_original'] = 'Оригинал'; +$_['text_error_process'] = 'Ошибка обработки: '; +$_['error_log_empty'] = 'Лог пуст! Сначала запустите процесс.'; +$_['text_report_header'] = '=== ОТЧЕТ ==='; +$_['text_total_files'] = 'Всего файлов: '; +$_['text_searching'] = 'Поиск файлов...'; +$_['error_ajax'] = 'Ошибка: '; +$_['text_resumed'] = '▶ ПРОЦЕСС ВОЗОБНОВЛЕН...'; +$_['text_paused'] = '⏸ ПАУЗА. Процесс приостановлен.'; +$_['confirm_stop'] = 'Вы уверены, что хотите полностью прервать процесс?'; +$_['confirm_delete'] = 'ВНИМАНИЕ! Файлы будут удалены с сервера навсегда без возможности восстановления! Продолжить?'; +$_['confirm_empty_q'] = 'Вы уверены, что хотите полностью очистить папку карантина?'; +$_['confirm_restore'] = 'Все файлы из папки карантина будут возвращены на свои места. Продолжить?'; +$_['button_stopping'] = 'Остановка...'; +$_['text_stopped_user'] = '🛑 ПРОЦЕСС ПРЕРВАН ПОЛЬЗОВАТЕЛЕМ!'; +$_['text_increase'] = 'Увеличение'; +$_['text_done'] = '✅ ГОТОВО! Процесс завершен.'; +$_['error_timeout'] = 'Таймаут сервера. Автоповтор через 3 секунды...'; +$_['text_building_index'] = 'Шаг 1/2: Индексация БД и шаблонов (поиск используемых фото)...'; +$_['text_comparing'] = 'Шаг 2/2: Сравнение физических файлов с индексом...'; +$_['text_quarantine_done'] = '✅ Процесс переноса в image/catalog_trash/ завершен!'; +$_['text_deleted_done'] = '✅ Процесс удаления мусора завершен!'; +$_['text_quarantine_empty'] = '✅ Папка карантина успешно удалена с сервера!'; +$_['text_restore_done'] = '✅ Файлы успешно возвращены из карантина в рабочую папку!'; +$_['text_quarantine_move'] = 'В карантине: '; +$_['text_deleted_file'] = 'Удален: '; +$_['text_no_trash'] = 'В выбранных папках мусор не найден! Все файлы используются.'; +$_['text_scanning_dirs'] = 'Сканируем директории: '; +$_['text_log_used'] = '[ИСПОЛЬЗУЕТСЯ]'; +$_['text_log_trash'] = '[МУСОР]'; +$_['text_log_deleted'] = '[УДАЛЕНО]'; +$_['text_log_del_err'] = '[ОШИБКА УДАЛЕНИЯ]'; +$_['text_log_quarantine'] = '[В КАРАНТИН]'; +$_['text_log_q_err'] = '[ОШИБКА ПЕРЕНОСА]'; +$_['text_log_restored'] = '[ВОССТАНОВЛЕНО]'; +$_['text_log_rest_err'] = '[ОШИБКА ВОССТАНОВЛЕНИЯ]'; +$_['text_used_in'] = 'в: '; +$_['text_analyzing'] = 'Анализ базы данных...'; +$_['text_clean_empty'] = 'Чисто! Мусора нет.'; +$_['text_processing'] = 'Обработка...'; +$_['text_restoring'] = 'Восстановление файлов...'; +$_['text_all_folders'] = 'image/catalog/ (И все вложенные)'; +$_['text_root_folder'] = 'image/catalog/ (Только корень)'; +$_['text_q_empty_err'] = 'Папка карантина пуста или не существует.'; +$_['error_token'] = 'Неверный токен. Перезагрузите страницу.'; +$_['text_apply_title'] = 'Оптимизация завершена в папку: '; +$_['text_apply_info'] = 'Проверьте результат. Если всё устраивает — примените изменения к основному каталогу.'; +$_['entry_apply_backup'] = 'Перенести с созданием бэкапа (в Карантин)'; +$_['entry_apply_replace'] = 'Заменить оригиналы безвозвратно'; +$_['button_apply_main'] = 'ПРИМЕНИТЬ К ОСНОВНОМУ КАТАЛОГУ'; +$_['button_delete_copies'] = 'Удалить временные копии (отмена)'; +$_['button_full_cleanup'] = 'Полная очистка всех временных папок и бэкапов'; +$_['text_apply_success'] = '✅ Файлы успешно перенесены. '; +$_['text_apply_backup_ok'] = 'Оригиналы сохранены в папку: '; +$_['text_apply_no_backup'] = 'Оригиналы удалены.'; +$_['text_error_source'] = 'Ошибка: Папка с оптимизированными файлами не найдена.'; +$_['confirm_full_cleanup'] = 'ВНИМАНИЕ! Это действие безвозвратно удалит ВСЕ папки бэкапов (backup_*) и временные папки с копиями. Продолжить?'; +$_['text_tools_desc'] = 'Глобальные сканеры и инструменты для работы с файловой системой и БД изображений.'; +$_['text_tool_select'] = 'Выберите инструмент:'; +$_['text_group_scanners'] = 'Сканеры и Анализ'; +$_['text_group_generators'] = 'Генераторы и Обработка'; +$_['text_group_experimental'] = 'Экспериментальные функции (BETA)'; +$_['text_experimental_warn'] = 'Внимание: Экспериментальные функции затрагивают системные файлы или базу данных. Настоятельно рекомендуется сделать бэкап!'; +$_['text_tool_broken_db'] = 'Сканер битых/пустых фото БД'; +$_['text_tool_watermark'] = 'Динамический Вотермарк (Только на кэш)'; +$_['text_tool_duplicates'] = 'Поиск дубликатов файлов (по MD5)'; +$_['text_tool_png_jpg'] = 'Конвертер тяжелых PNG в JPG (сканер файлов без прозрачности)'; +$_['text_tool_placeholders'] = 'Генератор заглушек (Вместо No Image)'; +$_['text_tool_exif'] = 'Очистка EXIF-данных (Геотеги/Мета)'; +$_['text_tool_small_photos'] = 'Сканер слишком мелких фото'; +$_['text_tool_html_broken'] = 'Поиск битых картинок в HTML (Описаниях)'; +$_['text_tool_empty_folders'] = 'Очистка пустых папок (image/catalog)'; +$_['text_tool_smart_cache'] = 'Умная очистка кэша изображений'; +$_['text_tool_folder_tree'] = 'Дерево папок (Статистика веса)'; +$_['text_tool_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load (Без БД)'; +$_['text_tool_translit'] = 'Транслитерация названий файлов'; +$_['text_tool_cache_restore'] = 'Воскрешение оригиналов из Кэша'; +$_['entry_wm_type'] = 'Тип вотермарка'; +$_['text_wm_image'] = 'Изображение (PNG)'; +$_['text_wm_text'] = 'Текст'; +$_['entry_wm_image_path'] = 'Путь к изображению'; +$_['entry_wm_text_val'] = 'Текст вотермарка'; +$_['entry_wm_position'] = 'Позиция (1-9)'; +$_['entry_wm_opacity'] = 'Прозрачность (0-100)'; +$_['entry_wm_category'] = 'Только для Категорий'; +$_['entry_wm_brand'] = 'Только для Брендов'; +$_['entry_min_width'] = 'Минимальная ширина (px)'; +$_['entry_min_height'] = 'Минимальная высота (px)'; +$_['entry_ph_product'] = 'Заглушка для Товаров'; +$_['entry_ph_category'] = 'Заглушка для Категорий'; +$_['entry_ph_brand'] = 'Заглушка для Брендов'; +$_['button_run_tool'] = 'Запустить инструмент'; +$_['button_scan_tool'] = 'Сканировать'; +$_['button_fix_tool'] = 'Исправить найденное'; +$_['text_th_path'] = 'Файл / Путь'; +$_['text_th_size'] = 'Размер'; +$_['text_th_action'] = 'Действие'; +$_['text_th_status'] = 'Статус'; +$_['text_th_problem'] = 'Проблема / Значение'; +$_['text_cron_title'] = 'Настройка Cron'; +$_['text_cron_desc'] = 'Добавьте команду в Cron хостинга для автоматического фонового сжатия.'; +$_['entry_cron_new_only'] = 'Обрабатывать только новые (необработанные) файлы'; +$_['entry_cron_folders'] = 'Папки для сканирования (оставьте пустым для всего каталога)'; +$_['text_promo_title'] = 'Другие модули серии [NAT]'; +$_['text_promo_desc'] = 'Модули серии [NAT] — это инструменты для глубокой оптимизации OpenCart. Мы фокусируемся на автоматизации рутины, ускорении базы данных и очистке серверов от мусора. Все модули имеют открытый исходный код и единую логику управления.'; +$_['text_support'] = 'Техническая поддержка и пожелания:'; +$_['button_more'] = 'Смотреть все модули на OpenCart.com'; +$_['button_more_forum'] = 'Opencartforum'; +$_['text_mod_cat_opt'] = 'Catalog Optimizer [NAT]'; +$_['text_mod_cat_opt_desc'] = 'Скрытие пустых категорий и брендов + SEO сортировка товаров (в наличии сверху) + Очистка мусора БД.'; +$_['text_mod_new_arrivals'] = 'New Arrivals [NAT]: Новинки по датам'; +$_['text_mod_new_arrivals_desc'] = 'Автоматическая SEO-страница новинок. Отображает новинки строго по реальной дате добавления. Слайдеры, сетка и умный сайдбар архива дат.'; +$_['text_mod_img_opt'] = 'Image Optimizer [NAT]'; +$_['text_mod_img_opt_desc'] = 'Текущий модуль. Сжатие и ресайз ОРИГИНАЛОВ фото (image/catalog) в WebP + Очистка хостинга от неиспользуемых изображений.'; +$_['text_mod_dom_scan'] = 'Domain Scanner [NAT]'; +$_['text_mod_dom_scan_desc'] = 'Поиск и чистка внешних ссылок и HTTP-картинок в БД и шаблонах (для переезда на HTTPS).'; +$_['text_mod_poll'] = 'Poll Widget [NAT]'; +$_['text_mod_poll_desc'] = 'Создание опросов и голосований с визуальным конструктором дизайна, глубокой статистикой и защитой от накруток.'; +$_['text_mod_db_opt'] = 'Database Optimizer [NAT]'; +$_['text_mod_db_opt_desc'] = 'Интеллектуальная очистка мусора в базе данных, управление системными логами и добавление критически важных индексов для ускорения.'; +$_['text_mod_im_ex'] = 'Import/Export [NAT]'; +$_['text_mod_im_ex_desc'] = 'Быстрый импорт/экспорт цен и остатков из файлов xls/xlsx/csv/xml/json или по ссылке. Маппинг полей, работа по Cron, текстовые функции.'; +$_['text_mod_admin_tools'] = 'Admin Tools [NAT]'; +$_['text_mod_admin_tools_desc'] = 'Удобные инструменты администратора: вход под клиентом, кастомизация админки, управление правами пользователей и скрытие ненужных пунктов меню.'; +$_['text_mod_utm_tracker'] = 'Order UTM Tracker [NAT]'; +$_['text_mod_utm_tracker_desc'] = 'Автоматическое отслеживание UTM-меток при оформлении заказа. Сохраняет источник перехода, кампанию, ключевые слова и выводит эти данные в заказе для аналитики рекламы.'; +$_['text_info_title_promo_top_bar'] = 'Promo Top Bar [NAT]: Верхний рекламный инфо-баннер, информационная полоса над шапкой, бегущая строка и топ-баннер'; +$_['text_info_desc_promo_top_bar'] = 'Создание интерактивных промо-панелей и слайд-шоу в шапке сайта с конструктором блоков, таймерами, формой подписки и гибким таргетингом.'; +$_['text_tool_no_image'] = 'Товары без главного фото (пусто в БД)'; +$_['text_problem_main_broken'] = 'Битое главное фото (файл удален)'; +$_['text_problem_add_broken'] = 'Битое доп. фото (файл удален)'; +$_['text_problem_no_image'] = 'Отсутствует главное фото'; +$_['text_err_dir'] = 'Ошибка: Директория не найдена или неверный путь.'; +$_['text_err_no_queue'] = 'Ошибка: Очередь не найдена. Сначала запустите сканирование.'; +$_['text_log_del_empty_folder'] = 'Удалена пустая папка: '; +$_['text_log_no_empty_folders'] = 'Пустых папок в image/catalog/ не найдено.'; +$_['text_log_total_empty_folders'] = 'Всего удалено пустых папок: '; +$_['text_log_cache_cleared'] = 'Кэш изображений успешно очищен.'; +$_['text_log_wm_smart_mode'] = 'Вотермарки работают в SMART-режиме: оригиналы не трогаются, логотип накладывается только при генерации кэша.'; +$_['text_log_scan_dupes'] = 'Сканирование image/catalog/ на дубликаты (по хешу MD5)... Это может занять время.'; +$_['text_log_found_dupe'] = 'Найден дубликат: '; +$_['text_log_no_dupes'] = 'Дубликаты не найдены.'; +$_['text_log_total_dupes'] = 'Найдено дубликатов: %s. Потенциальная экономия: %s'; +$_['text_log_fix_dupes_prompt'] = 'Нажмите "Исправить найденное" для перелинковки БД на оригинал и удаления дублей.'; +$_['text_log_fixed_dupe'] = 'Исправлен и удален дубликат: '; +$_['text_log_success_dupes'] = 'Успешно исправлено дубликатов: %s. Ссылки в БД обновлены.'; +$_['text_log_scan_png'] = 'Поиск тяжелых PNG файлов без прозрачности...'; +$_['text_log_no_png'] = 'Подходящих PNG файлов не найдено.'; +$_['text_log_total_png'] = 'Найдено PNG без прозрачности: %s. Нажмите "Исправить" для конвертации в JPG.'; +$_['text_log_converted_jpg'] = 'Сконвертировано в JPG: '; +$_['text_log_success_png'] = 'Успешно сконвертировано %s файлов. Освобождено: %s'; +$_['text_log_ph_prod'] = 'Обновлено товаров заглушкой: '; +$_['text_log_ph_cat'] = 'Обновлено категорий заглушкой: '; +$_['text_log_ph_brand'] = 'Обновлено брендов заглушкой: '; +$_['text_log_ph_empty'] = 'Заглушки не выбраны в настройках.'; +$_['text_log_scan_exif'] = 'Сканирование EXIF-данных (мета-теги камер)...'; +$_['text_log_stripped_exif'] = 'Очищен EXIF: '; +$_['text_log_no_exif'] = 'Файлов с EXIF для очистки не найдено.'; +$_['text_log_success_exif'] = 'Очищен EXIF у %s файлов. Освобождено: %s'; +$_['text_log_scan_small'] = 'Поиск фото размером менее %sx%s px...'; +$_['text_log_too_small'] = 'Слишком маленькое (%sx%s): '; +$_['text_log_no_small'] = 'Мелких фото не найдено.'; +$_['text_log_total_small'] = 'Найдено мелких фото: %s. Пожалуйста, замените их на качественные.'; +$_['text_log_html_broken'] = 'Битый IMG в HTML таблицы %s (ID: %s): '; +$_['text_log_no_html_broken'] = 'Битых картинок в HTML-описаниях не найдено.'; +$_['text_log_total_html_broken'] = 'Найдено битых ссылок в HTML: %s. Исправьте их вручную в редакторе.'; +$_['text_log_scan_smart_cache'] = 'Поиск потерянных файлов в image/cache/...'; +$_['text_log_del_orph_cache'] = 'Удален потерянный кэш: '; +$_['text_log_no_orph_cache'] = 'Потерянного кэша не найдено.'; +$_['text_log_success_smart_cache'] = 'Удалено файлов потерянного кэша: %s. Освобождено: %s'; +$_['text_log_scan_translit'] = 'Поиск файлов с кириллицей или пробелами...'; +$_['text_log_no_bad_names'] = 'Файлов с некорректными именами не найдено.'; +$_['text_log_total_bad_names'] = 'Найдено файлов с кириллицей/пробелами: %s. Нажмите "Исправить" для транслитерации и обновления БД.'; +$_['text_log_renamed'] = 'Переименовано: '; +$_['text_log_success_translit'] = 'Успешно транслитерировано файлов: %s. БД обновлена.'; +$_['text_log_scan_restore'] = 'Сканирование image/cache/ для восстановления утерянных оригиналов...'; +$_['text_log_restored_cache'] = 'Восстановлено из кэша: '; +$_['text_log_no_lost_orig'] = 'Отсутствующих оригиналов в кэше не найдено.'; +$_['text_log_success_restore'] = 'Успешно восстановлено оригиналов из кэша: %s.'; +$_['error_invalid_folder'] = 'Ошибка: Недопустимое имя папки!'; +$_['text_formats_title'] = 'Современные форматы (WebP & SVG)'; +$_['text_formats_desc'] = 'Настройки интеграции современных форматов в файловую систему и шаблоны OpenCart.'; +$_['entry_support_svg'] = 'Поддержка загрузки SVG'; +$_['help_support_svg'] = 'Разрешает загрузку векторных SVG-изображений через стандартный Filemanager и их безопасный вывод на витрине без искажений размеров.'; +$_['entry_support_webp'] = 'Поддержка загрузки WebP'; +$_['help_support_webp'] = 'Позволяет вручную загружать, подвязывать и выводить на сайте готовые файлы .webp через Filemanager.'; +$_['entry_truefilemanager_webp'] = 'WebP в TrueFileManager'; +$_['help_truefilemanager_webp'] = 'Добавляет поддержку загрузки, предпросмотра и эскизов WebP в TrueFileManager от sitecreator (если он установлен).'; +$_['entry_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load'; +$_['help_webp_on_fly'] = 'Сверхбыстрая подмена: автоматически меняет расширения .jpg/.png на .webp прямо в HTML-выводе витрины и добавляет атрибут loading="lazy". Не трогает базу данных!'; +$_['help_tool_broken_db'] = 'Ищет в базе данных картинки, физические файлы которых были удалены с сервера. Позволяет очистить битые ссылки.'; +$_['help_tool_empty_folders'] = 'Рекурсивно сканирует директорию image/catalog/ и безопасно удаляет все папки, в которых нет ни одного файла.'; +$_['help_tool_folder_tree'] = 'Строит дерево каталогов и показывает реальный вес каждой папки для поиска "пожирателей" места.'; +$_['help_tool_html_broken'] = 'Сканирует HTML-описания товаров и категорий на наличие тегов с несуществующими картинками.'; +$_['help_tool_small_photos'] = 'Находит картинки с разрешением ниже заданного (например, меньше 300x300 px). Добавлены ссылки на поиск качественных аналогов в Google Images и Яндекс Картинках.'; +$_['help_tool_duplicates'] = 'Ищет полные дубликаты файлов по MD5 хешу (разные имена, но одинаковая картинка). Позволяет склеить их в один файл и обновить БД.'; +$_['help_tool_watermark'] = 'Наложение водяных знаков в SMART-режиме (только на кэш витрины, оригиналы не изменяются).'; +$_['help_tool_placeholders'] = 'Массово устанавливает выбранную картинку-заглушку для товаров и категорий, у которых вообще нет фото.'; +$_['help_tool_png_jpg'] = 'Инструмент сканирует папку изображений и находит тяжелые PNG-файлы, которые не имеют прозрачного фона (альфа-канала). Поскольку прозрачность им не нужна, их можно безопасно конвертировать в формат JPG. Это значительно облегчает файлы (в среднем на 70-80%) и ускоряет загрузку сайта, а все ссылки в базе данных автоматически обновляются.'; +$_['help_tool_exif'] = 'Сканирует JPEG-файлы и удаляет из них скрытые EXIF-данные (геотеги, данные о камере), уменьшая вес на 10-15%.'; +$_['help_tool_smart_cache'] = 'Ищет в папке image/cache/ кэшированные файлы, оригиналы которых уже удалены из image/catalog/, и стирает этот мусор.'; +$_['help_tool_translit'] = 'Находит файлы с кириллицей или пробелами в названиях, переименовывает их транслитом (tovar_1.jpg) и обновляет пути в БД.'; +$_['help_tool_cache_restore'] = 'Если вы случайно удалили оригинал из catalog, инструмент попытается найти его копию в cache и восстановить обратно.'; +$_['entry_quarantine'] = 'Переместить оригиналы в Карантин (вместо удаления)'; +$_['entry_backup_db'] = 'Создать бэкап изменяемых таблиц в SQL перед запуском'; +$_['button_fix_selected'] = 'ИСПРАВИТЬ ВЫБРАННЫЕ'; +$_['button_export_log'] = 'Экспортировать в CSV'; +$_['text_th_before'] = 'Состояние ДО'; +$_['text_th_after'] = 'Ожидаемый результат ПОСЛЕ'; +$_['text_th_select'] = 'Действие'; +$_['text_th_preview'] = 'Предпросмотр'; +$_['text_th_name'] = 'Объект / Файл'; +$_['text_global_action'] = 'Групповое действие для выбранных:'; +$_['text_action_clear'] = 'Очистить ссылку в БД'; +$_['text_action_disable'] = 'Отключить (Статус = 0)'; +$_['text_action_stock'] = 'Установить "Нет в наличии"'; +$_['text_action_placeholder'] = 'Установить заглушку'; +$_['text_action_ignore'] = 'Ничего не делать'; +$_['text_action_merge'] = 'Склеить (удалить дубликат, оставить оригинал)'; +$_['text_action_convert'] = 'Конвертировать в JPG'; +$_['text_action_rename'] = 'Переименовать транслитом'; +$_['text_action_delete'] = 'Удалить'; +$_['text_action_restore'] = 'Восстановить'; +$_['text_db_backup_created'] = 'Создан бэкап БД: %s'; +$_['text_quarantine_created'] = 'Перенесено в карантин: %s'; +$_['text_fix_status_success'] = 'Успешно выполнено'; +$_['text_fix_status_error'] = 'Ошибка: %s'; +$_['text_log_exported'] = 'Лог успешно экспортирован!'; +$_['text_select_action'] = '-- Выберите действие --'; +$_['error_empty_selection'] = 'Ошибка: Не выбрано ни одной строки для исправления!'; +$_['text_link_admin'] = 'Админка'; +$_['text_link_catalog'] = 'Витрина'; +$_['entry_threads'] = 'Потоков обработки'; +$_['text_threads_optimal'] = 'оптимально'; +$_['button_download_zip'] = 'Скачать ZIP-архив и удалить папку'; +$_['confirm_download_and_delete'] = 'Архив успешно скачался? Можно удалить временную папку копий на сервере?'; +$_['button_clear_backups'] = 'Удалить бэкапы БД'; +$_['button_clear_backups_short'] = 'Очистить бэкапы'; +$_['confirm_clear_backups'] = 'Вы уверены, что хотите полностью удалить все SQL-бэкапы таблиц?'; +$_['text_compare_title'] = 'Сравнение изображений ДО / ПОСЛЕ'; +$_['text_compare_before'] = 'ДО'; +$_['text_compare_after'] = 'ПОСЛЕ'; +$_['text_backups_cleared'] = 'Все SQL-бэкапы таблиц успешно удалены!'; +$_['error_no_backups'] = 'Бэкапы не найдены или папка пуста.'; +$_['button_restore'] = 'Вернуть'; +$_['button_empty'] = 'Очистить'; +$_['entry_wm_targets'] = 'Применять водяной знак к:'; +$_['text_wm_target_product'] = 'Фото товаров'; +$_['text_wm_target_category'] = 'Фото категорий'; +$_['text_wm_target_brand'] = 'Логотипы брендов'; +$_['text_wm_target_banner'] = 'Баннеры и слайды'; +$_['text_wm_target_blog'] = 'Статьи / Блог'; +$_['error_no_scan_data'] = 'Ошибка: Данные сканирования не найдены. Сначала запустите сканирование!'; +$_['error_row_not_found'] = 'Ошибка: Объект не найден в результатах сканирования!'; +$_['error_file_not_found'] = 'Ошибка: Файл не найден на сервере!'; +$_['text_folder_rules_title'] = 'Индивидуальные правила сжатия для папок'; +$_['text_folder_rules_desc'] = 'Вы можете переопределить качество и максимальные размеры для конкретных папок. Если оставить поле пустым, будет использоваться глобальная настройка. Путь указывается относительно папки image (например, catalog/banners/).'; +$_['entry_folder_path'] = 'Путь к папке'; +$_['entry_jpg_quality'] = 'Качество JPG'; +$_['entry_webp_quality'] = 'Качество WebP'; +$_['button_add_rule'] = 'Добавить правило'; +$_['button_remove'] = 'Удалить'; +$_['entry_wm_angle'] = 'Угол наклона водяного знака (градусы)'; +$_['entry_wm_corner_radius'] = 'Скругление углов водяного знака (px)'; +$_['entry_wm_size_type'] = 'Масштабирование водяного знака'; +$_['entry_wm_size_percent'] = 'Размер водяного знака (% от ширины изображения)'; +$_['entry_wm_text_color'] = 'Цвет текста водяного знака'; +$_['entry_wm_text_font'] = 'Шрифт водяного знака (TTF)'; +$_['entry_wm_filter_mode'] = 'Режим фильтрации Категорий/Брендов'; +$_['text_wm_filter_include'] = 'Только для выбранных'; +$_['text_wm_filter_exclude'] = 'Для всех, кроме выбранных'; +$_['text_wm_size_original'] = 'Оригинальный размер'; +$_['text_wm_size_percent'] = 'Пропорционально ширине изображения (%)'; +$_['button_preview_watermark'] = 'Предпросмотр водяного знака'; +$_['text_wm_preview_title'] = 'Предпросмотр водяного знака'; +$_['text_cron_exclude_folders'] = 'Исключить папки из сканирования'; +$_['text_cron_entities'] = 'Какие типы фото обрабатывать?'; +$_['text_cron_quality_override'] = 'Переопределить параметры сжатия для Крона'; +$_['text_cron_recommendations'] = 'Рекомендации по расписанию запуска Cron'; +$_['text_cron_recommendations_desc'] = '
    • Для небольших сайтов (до 5000 товаров) рекомендуется запускать Крон каждый час (0 * * * *).
    • Для средних сайтов (5000-20000 товаров) рекомендуется запускать раз в 3-6 часов (0 */6 * * *).
    • Для крупных сайтов (более 20000 товаров) — один раз в сутки ночью (например, в 2:00: 0 2 * * *).
    '; +$_['text_tool_broken_files'] = 'Сканер битых/пустых файлов на сервере'; +$_['help_tool_broken_files'] = 'Сканирует физические файлы в папке image/catalog/ на наличие поврежденных изображений или пустых файлов размером 0 байт.'; +$_['text_grid_broken_file'] = 'Битый или пустой файл на сервере'; +$_['text_grid_broken_file_after'] = 'Удаление или замена заглушкой'; +$_['text_description_broken_files'] = 'Результаты сканирования физических файлов в папке image/catalog/. Здесь отображаются битые (поврежденные) изображения или файлы размером 0 байт.'; +$_['text_action_strip'] = 'Очистить EXIF'; +$_['text_action_set_placeholder'] = 'Установить заглушку'; +$_['text_th_entity'] = 'Объект / Связанные ссылки'; +$_['text_cron_entity_product'] = 'Товары'; +$_['text_cron_entity_category'] = 'Категории'; +$_['text_cron_entity_manufacturer'] = 'Производители / Бренды'; +$_['text_cron_entity_banner'] = 'Баннеры'; +$_['text_cron_entity_blog'] = 'Блог / Статьи'; +$_['entry_cron_jpg_quality'] = 'Качество JPG (Крон)'; +$_['entry_cron_webp_quality'] = 'Качество WebP (Крон)'; +$_['entry_cron_max_width'] = 'Макс. ширина px (Крон)'; +$_['entry_cron_max_height'] = 'Макс. высота px (Крон)'; +$_['text_cron_folder_scan'] = 'Папки для сканирования'; +$_['text_no_fonts'] = 'TTF-шрифты не найдены в system/library/font/'; +$_['button_apply'] = 'Применить'; +$_['text_backup_will_be_created'] = 'Резервная копия базы данных будет автоматически создана перед выполнением этого действия.'; +$_['text_bulk_action'] = 'Групповое действие для выбранных'; +$_['text_grid_results'] = 'Результаты и детали сканирования'; +$_['text_quarantine_will_be_created'] = 'Оригинальные файлы будут перемещены в карантин (image/catalog_trash/) перед изменением.'; +$_['text_success_apply'] = 'Изменения успешно применены!'; +$_['text_description_broken'] = 'Результаты сканирования базы данных на наличие битых ссылок (записи в таблицах с несуществующими файлами).'; +$_['text_description_cache_restore'] = 'Результаты поиска файлов в кэше, оригиналы которых отсутствуют в каталоге.'; +$_['text_description_duplicates'] = 'Результаты поиска полных дубликатов изображений по MD5-хешу.'; +$_['text_description_empty_folders'] = 'Список пустых папок в директории image/catalog/ для удаления.'; +$_['text_description_exif'] = 'Результаты поиска изображений с метаданными EXIF, которые можно очистить.'; +$_['text_description_folder_tree'] = 'Статистика размера папок в каталоге image/catalog/.'; +$_['text_description_html_broken'] = 'Результаты поиска несуществующих файлов картинок, на которые ссылаются описания в HTML.'; +$_['text_description_placeholders'] = 'Результаты поиска сущностей без изображений для заполнения заглушками.'; +$_['text_description_png_jpg'] = 'Результаты поиска тяжелых изображений PNG без прозрачности, которые можно пережать в JPG.'; +$_['text_description_small_photos'] = 'Список изображений, размеры которых меньше минимально заданных (%sx%s px).'; +$_['text_description_smart_cache'] = 'Результаты поиска файлов кэша, оригиналы которых уже были удалены.'; +$_['text_description_translit'] = 'Результаты поиска файлов с кириллицей или пробелами в названиях.'; +$_['text_grid_broken_db_after'] = 'Очистить ссылку или установить заглушку'; +$_['text_grid_dupe_after'] = 'Оригинал (остаётся)'; +$_['text_grid_dupe_before'] = 'Дубликат (будет удалён)'; +$_['text_grid_empty_folder'] = 'Пустая папка'; +$_['text_grid_empty_folder_after'] = 'Удалить пустую папку'; +$_['text_grid_exif_after'] = 'Очистить метаданные'; +$_['text_grid_exif_before'] = 'Содержит EXIF'; +$_['text_grid_html_after'] = 'Исправить ссылку вручную'; +$_['text_grid_missing_file'] = 'Файл отсутствует'; +$_['text_grid_missing_html'] = 'Картинка не найдена в описании'; +$_['text_grid_missing_orig'] = 'Оригинал отсутствует'; +$_['text_grid_missing_orig_after'] = 'Восстановить оригинал из кэша'; +$_['text_grid_no_image'] = 'Нет изображения (пусто)'; +$_['text_grid_orph_cache'] = 'Осиротевший кэш'; +$_['text_grid_orph_cache_after'] = 'Удалить ненужный кэш'; +$_['text_grid_ph_brand_after'] = 'Установить заглушку бренда'; +$_['text_grid_ph_category_after'] = 'Установить заглушку категории'; +$_['text_grid_ph_product_after'] = 'Установить заглушку товара'; +$_['text_grid_small_photo_after'] = 'Рекомендуется заменить на более крупное'; +$_['text_grid_too_small'] = 'Слишком маленькое'; +$_['text_no_relations'] = 'Нет связей в БД'; +$_['text_action_archive'] = 'Архивировать (в ZIP)'; +$_['text_action_archive_delete'] = 'Архивировать + Удалить'; +$_['text_tool_sanitizer'] = 'Санитайзер имен файлов'; +$_['help_tool_sanitizer'] = 'Массовая очистка имен файлов и папок от кириллицы, пробелов и спецсимволов, приведение расширений к единому виду.'; +$_['text_description_sanitizer'] = 'Результаты анализа имен файлов для санитаризации по выбранным правилам.'; +$_['entry_sz_translit'] = 'Транслитерация (кириллица в латиницу)'; +$_['entry_sz_spaces'] = 'Заменять пробелы на символ "_"'; +$_['entry_sz_special'] = 'Удалять специальные символы'; +$_['entry_sz_lowercase_name'] = 'Имя файла в нижний регистр'; +$_['entry_sz_lowercase_ext'] = 'Расширение в нижний регистр'; +$_['entry_sz_normalize_ext'] = 'Нормализовать расширения (jpeg -> jpg)'; +$_['entry_cron_folder_mode'] = 'Режим фильтрации папок'; +$_['text_cron_folder_mode_exclude'] = 'Сканировать все, кроме выбранных (Исключения)'; +$_['text_cron_folder_mode_include'] = 'Сканировать только выбранные папки'; +$_['button_select_all'] = 'Выбрать все'; +$_['button_deselect_all'] = 'Снять все'; +$_['entry_wm_upload_font'] = 'Загрузить свой шрифт TTF'; +$_['button_upload_font'] = 'Загрузить шрифт'; +$_['text_success_font_upload'] = 'Шрифт успешно загружен!'; +$_['error_font_upload'] = 'Ошибка загрузки шрифта! Разрешены только файлы .ttf весом до 5 МБ.'; +$_['entry_lazy_load'] = 'Прогрессивная ленивая загрузка'; +$_['help_lazy_load'] = 'Включает премиальную прогрессивную отложенную загрузку на витрине. Изображения заменяются микро-превью и плавно размываются (blur-up), подгружаясь по мере прокрутки.'; +$_['confirm_replace_mode'] = 'ВНИМАНИЕ! Вы выбрали режим «Заменять оригиналы». Все изображения будут перезаписаны прямо на сервере. Настоятельно рекомендуем сделать резервную копию. Вы уверены, что хотите продолжить?'; +$_['text_compare'] = 'Сравнить'; +$_['text_home'] = 'Главная'; +$_['text_tech_gd'] = 'Библиотека GD'; +$_['text_tech_imagick'] = 'Imagick'; +$_['text_tech_webp'] = 'Поддержка WebP'; +$_['text_position_1'] = '1 - Слева вверху'; +$_['text_position_2'] = '2 - По центру вверху'; +$_['text_position_3'] = '3 - Справа вверху'; +$_['text_position_4'] = '4 - Слева посередине'; +$_['text_position_5'] = '5 - По центру'; +$_['text_position_6'] = '6 - Справа посередине'; +$_['text_position_7'] = '7 - Слева внизу'; +$_['text_position_8'] = '8 - По центру внизу'; +$_['text_position_9'] = '9 - Справа внизу'; + +$_['entry_log_level'] = 'Уровень лога'; +$_['text_log_changed_only'] = 'Только изменённые'; +$_['text_log_all'] = 'Все файлы (подробно)'; +$_['text_skipped_no_gain'] = 'Пропущен (нет улучшения)'; +$_['text_reset_cache_hint'] = 'Сбросить кэш оптимизации (полный пересканирование)'; +$_['confirm_reset_cache'] = 'Сбросить кэш оптимизации? Следующее сканирование проверит ВСЕ файлы заново.'; +$_['text_cache_cleared'] = 'Кэш оптимизации очищен. Следующее сканирование проверит все файлы.'; +$_['error_wm_preview'] = 'Ошибка генерации превью водяного знака.'; +$_['error_wm_too_large'] = 'Изображение водяного знака слишком большое (%dx%d px)! Максимально допустимый размер: %dx%d px.'; +$_['text_apply_errors_warning'] = 'ВНИМАНИЕ: Некоторые файлы не удалось заменить (заблокированы или нет прав):'; +$_['text_action_compress'] = 'Сжать изображения'; +$_['text_estimated'] = 'Прогноз'; +$_['entry_min_savings_bytes'] = 'Мин. сжатие (байт)'; +$_['entry_min_savings_percent'] = 'Мин. сжатие (%)'; +$_['text_optimized'] = 'Оптимизировано'; +$_['entry_broken_fallback'] = 'Заглушка для битых фото'; +$_['help_broken_fallback'] = 'Автоматически заменяет отсутствующие или поврежденные изображения каталога на витрине на изображение-заглушку.'; +$_['entry_fallback_image'] = 'Изображение-заглушка'; +$_['help_fallback_image'] = 'Выберите изображение, которое будет показано, если оригинальный файл картинки товара или категории отсутствует. По умолчанию используется no_image.png.'; +$_['text_tool_cmyk'] = 'Конвертер CMYK в RGB'; +$_['help_tool_cmyk'] = 'Конвертация JPEG изображений из цветового профиля CMYK в sRGB для корректного отображения в браузерах Safari и на устройствах iOS.'; +$_['text_tool_heavy_files'] = 'Поиск тяжелых файлов'; +$_['help_tool_heavy_files'] = 'Поиск самых тяжелых оригинальных изображений в каталоге с возможностью их быстрого сжатия прямо здесь.'; +$_['entry_heavy_files_limit'] = 'Макс. количество найденных файлов (всего)'; +$_['text_action_convert_rgb'] = 'Конвертировать в RGB'; +$_['text_description_cmyk'] = 'Найдены файлы в цветовом пространстве CMYK. Конвертация в sRGB гарантирует корректное отображение на всех платформах, включая iOS/macOS.'; +$_['text_description_heavy'] = 'Список из %d самых тяжелых файлов в каталоге изображений. Вы можете оптимизировать их прямо здесь.'; +$_['text_cron_folder_hint_exclude_all'] = 'Статус: Будут сканироваться все папки (исключения не выбраны).'; +$_['text_cron_folder_hint_exclude_some'] = 'Статус: Будут сканироваться все папки, КРОМЕ %d отмеченных.'; +$_['text_cron_folder_hint_include_some'] = 'Статус: Будут сканироваться только %d отмеченных папок.'; +$_['text_cron_folder_hint_include_none'] = 'ВНИМАНИЕ: Ни одна папка не выбрана! Крон ничего не будет сканировать.'; +$_['text_action_apply_changes'] = 'Применить указанные изменения'; +$_['entry_wm_status'] = 'Статус водяного знака'; +$_['text_disabled'] = 'Отключено'; +$_['entry_rule_name'] = 'Название правила'; +$_['entry_rule_status'] = 'Статус правила'; +$_['entry_action'] = 'Действие'; +$_['entry_ph_status'] = 'Статус заглушек'; +$_['entry_sanitizer_limit'] = 'Макс. файлов для поиска (лимит)'; +$_['entry_heavy_files_min_size'] = 'Минимальный размер'; +$_['entry_png_jpg_limit'] = 'Макс. файлов для поиска (лимит)'; +$_['entry_png_jpg_min_size'] = 'Минимальный размер'; +$_['text_wm_rule_title'] = 'Правило водяного знака'; +$_['button_close'] = 'Закрыть'; +$_['text_ph_rule_title'] = 'Правило заглушки'; +$_['entry_ph_image'] = 'Изображение заглушки'; +$_['button_clear_cache_wm'] = 'Очистить кэш (применить водяной знак)'; +$_['text_active'] = 'Активна'; +$_['text_error'] = 'Не активна'; +$_['text_th_dupe_delete'] = 'Файл для удаления (Дубликат)'; +$_['text_th_dupe_keep'] = 'Файл для сохранения (Оригинал)'; +$_['text_dupe_original_label'] = 'Оригинал'; +$_['text_dupe_duplicate_label'] = 'Дубликат'; + +$_['text_disk_usage'] = 'Использование диска'; +$_['text_cumulative_stats'] = 'Статистика оптимизации'; +$_['text_disk_catalog'] = 'Каталог изображений'; +$_['text_disk_cache'] = 'Кэш OC'; +$_['text_disk_trash'] = 'Корзина модуля'; +$_['text_disk_other'] = 'Прочее'; +$_['text_disk_free'] = 'Свободно'; +$_['text_total_files_opt'] = 'Файлов обработано'; +$_['text_total_saved'] = 'Всего сэкономлено'; +$_['text_avg_saving_pct'] = 'Средняя экономия'; +$_['text_stats_hint'] = 'Статистика накапливается по всем сессиям (хранится локально в браузере)'; +$_['text_reset_stats'] = 'Сбросить статистику'; +$_['text_reset_stats_confirm']= 'Сбросить накопленную статистику оптимизации?'; +$_['text_export_import'] = 'Экспорт / Импорт настроек'; +$_['button_export_settings'] = 'Экспорт настроек (JSON)'; +$_['button_import_settings'] = 'Импортировать настройки'; +$_['help_export_settings'] = 'Скачать все настройки модуля в JSON-файл'; +$_['help_import_settings'] = 'Загрузить ранее экспортированный JSON для восстановления настроек'; +$_['text_import_success'] = 'Настройки успешно импортированы. Перезагрузите страницу для применения.'; +$_['error_import_file'] = 'Файл не загружен или ошибка загрузки'; +$_['error_import_invalid'] = 'Некорректный файл настроек (неверный модуль или формат)'; +$_['text_click_to_load'] = 'Нажмите ↑ для загрузки данных диска'; +$_['tab_formats'] = 'Форматы изображений'; + +$_['text_disk_quota_legend'] = 'Лимиты диска хостинга (вручную)'; +$_['text_disk_quota_desc'] = 'Если график дискового пространства показывает неверные данные (физический диск сервера вместо лимита вашего хостинга), вы можете указать лимиты вашего тарифа вручную. Укажите 0, чтобы использовать автоопределение.'; +$_['entry_disk_quota_total'] = 'Выделено диска на хостинге (ГБ)'; +$_['entry_disk_quota_used'] = 'Занято диска на хостинге всего (ГБ)'; +$_['entry_auto_disk'] = 'Автообновление при входе'; + +// Cron Tasks +$_['entry_cron_task_name'] = 'Название задачи'; +$_['entry_cron_task_summary'] = 'Параметры сжатия'; +$_['button_add_cron_task'] = 'Добавить задачу'; +$_['button_copy_cron_url'] = 'Копировать URL'; +$_['text_cron_recommended_url'] = 'URL для запуска задачи крона:'; + +// Licensing +$_['text_license_required'] = 'Требуется лицензионный ключ'; +$_['text_license_required_desc'] = 'Пожалуйста, введите лицензионный ключ для активации модуля. Все функции настроек и оптимизации заблокированы до ввода валидного ключа.'; +$_['entry_token_desc'] = 'Введите токен активации, выданный для вашего основного домена. Вы можете получить его в личном кабинете на форуме или обратившись в поддержку.'; +$_['button_activate'] = 'Активировать модуль'; +$_['text_support'] = 'Техническая поддержка'; +$_['text_support_desc'] = 'Если у вас ещё нет ключа или возникли проблемы, пожалуйста, сообщите номер вашего заказа и домен.'; + +// New settings +$_['entry_wm_max_width'] = 'Макс. ширина водяного знака (px)'; +$_['entry_wm_max_height'] = 'Макс. высота водяного знака (px)'; +$_['help_wm_max_width'] = 'Если исходное изображение водяного знака превышает эту ширину, оно будет автоматически масштабировано. Укажите 0 или оставьте пустым для значения по умолчанию (800px).'; +$_['help_wm_max_height'] = 'Если исходное изображение водяного знака превышает эту высоту, оно будет автоматически масштабировано. Укажите 0 или оставьте пустым для значения по умолчанию (800px).'; +$_['entry_disk_status'] = 'Включить сканирование диска и график'; +$_['help_disk_status'] = 'Если включено, модуль будет периодически сканировать объем папки картинок и отображать круговую диаграмму. Рекомендуется выключить на больших сайтах для ускорения загрузки панели.'; +$_['text_disk_settings_title'] = 'Настройка лимитов и сканирования диска'; diff --git a/upload/admin/language/uk-ua/module/img_opti.php b/upload/admin/language/uk-ua/module/img_opti.php new file mode 100644 index 0000000..a563c9d --- /dev/null +++ b/upload/admin/language/uk-ua/module/img_opti.php @@ -0,0 +1,567 @@ +Image Optimizer [NAT]: Стиснення оригіналів та видалення сміття'; +$_['text_extension'] = 'Розширення'; +$_['text_edit'] = 'Налаштування модуля'; +$_['text_success'] = 'Налаштування успішно збережено!'; +$_['entry_warning'] = 'УВАГА: Обов`язково зробіть бекап файлів сайту і особливо папки /image/catalog перед початком роботи!'; +$_['text_tech'] = 'Перевірка технологій сервера:'; +$_['text_author'] = 'Технічна підтримка та побажання: info@nat.od.ua'; +$_['tab_optimize'] = 'Оптимізація та Ресайз'; +$_['tab_cleaner'] = 'Очищення невикористовуваних зображень'; +$_['tab_info'] = 'Інфо / Екосистема [NAT]'; +$_['tab_settings'] = 'Налаштування'; +$_['tab_tools'] = 'Інструменти (pro)'; +$_['tab_cron'] = 'Крон-завдання'; +$_['entry_mode'] = 'Режим роботи'; +$_['text_copy'] = 'Створити копію (безпечно)'; +$_['text_replace'] = 'Замінювати оригінали (потребує бекап!)'; +$_['entry_folder'] = 'Ім`я папки для копії'; +$_['entry_batch'] = 'Файлів за прохід'; +$_['entry_log_limit'] = 'Рядків логу на екрані'; +$_['entry_max_width'] = 'Макс. ширина (px) [0 - без ліміту]'; +$_['entry_max_height'] = 'Макс. висота (px) [0 - без ліміту]'; +$_['entry_jpg'] = 'Якість JPG (0-100)'; +$_['entry_png'] = 'Стиснення PNG (0-9)'; +$_['entry_webp'] = 'Якість WebP (0-100)'; +$_['entry_targets'] = 'Що оптимізувати?'; +$_['text_all_catalog'] = '[ Весь каталог image/catalog ]'; +$_['text_root_files'] = 'Файли тільки в корені image/catalog'; +$_['entry_threshold'] = 'Поріг заміни (%)'; +$_['help_threshold'] = '0: замінювати тільки якщо вага стала меншою. 10: дозволити збільшення ваги до 10%.'; +$_['entry_token'] = 'Токен активації (на корінь домену)'; +$_['entry_status'] = 'Статус модуля'; +$_['entry_menu_position'] = 'Позиція меню [NAT]'; +$_['text_menu_module_only'] = 'Тільки в модулях'; +$_['text_menu_header'] = 'У шапці (Header)'; +$_['text_menu_sidebar'] = 'У бічному меню (Sidebar)'; +$_['text_menu_both'] = 'Скрізь (Шапка + Сайдбар)'; +$_['text_status_on'] = 'Увімкнено'; +$_['text_status_off'] = 'Вимкнено'; +$_['entry_engine'] = 'Рушій обробки зображень'; +$_['text_engine_gd'] = 'GD (Стандарт)'; +$_['text_engine_imagick'] = 'Imagick (Висока якість, збереження профілів sRGB)'; +$_['entry_license'] = 'Ліцензія'; +$_['text_cleaner_info'] = 'Цей інструмент шукає файли, які фізично існують на диску, але ніде не використовуються в базі даних або файлах шаблону.'; +$_['entry_clean_targets'] = 'В яких папках шукати сміття?'; +$_['entry_extended_log'] = 'Розширений лог (показати використовувані photo і де вони знайдені)'; +$_['entry_process_broken'] = 'Враховувати файли з битим кодуванням (імена з символом "?")'; +$_['button_scan_trash'] = 'ЗНАЙТИ СМІТТЯ'; +$_['button_quarantine'] = 'В Карантин (Безпечно)'; +$_['button_delete_trash'] = 'Видалити назавжди'; +$_['button_empty_quarantine'] = 'Очистити папку Карантину'; +$_['button_restore_quarantine'] = 'Повернути з Карантину'; +$_['text_scan_result'] = 'Результати сканування'; +$_['text_trash_files'] = 'Сміттєвих файлів: '; +$_['text_trash_size'] = 'Потенційно звільниться: '; +$_['button_start'] = 'СТАРТ ОПТИМІЗАЦІЇ'; +$_['button_pause'] = 'ПАУЗА'; +$_['button_stop'] = 'ЗУПИНИТИ'; +$_['button_continue'] = 'ПРОДОВЖИТИ'; +$_['button_start_new'] = 'СТАРТ (Новий запуск)'; +$_['button_download_log'] = 'Завантажити лог в TXT'; +$_['button_download'] = 'Завантажити'; +$_['button_save'] = 'Зберегти'; +$_['button_cancel'] = 'Скасувати'; +$_['text_progress'] = 'Прогрес:'; +$_['text_old_size'] = 'Було:'; +$_['text_new_size'] = 'Стало:'; +$_['text_savings'] = 'Економія:'; +$_['text_detailed_log'] = 'Детальний лог'; +$_['text_page'] = 'Стор.'; +$_['text_waiting'] = 'Очікування запуску...'; +$_['text_enabled'] = 'Увімкнено'; +$_['text_disabled_error'] = 'Вимкнено (ПОМИЛКА)'; +$_['text_supported'] = 'Підтримується'; +$_['text_no'] = 'Ні'; +$_['error_permission'] = 'У вас немає прав для керування модулем!'; +$_['error_license'] = 'Помилка доступу: Невірний токен активації для цього домену!'; +$_['error_domain'] = ' (Домен: '; +$_['text_found'] = 'Знайдено файлів: '; +$_['text_compressed'] = 'Стиснуто'; +$_['text_original'] = 'Оригінал'; +$_['text_error_process'] = 'Помилка обробки: '; +$_['error_log_empty'] = 'Лог порожній! Спочатку запустіть процес.'; +$_['text_report_header'] = '=== ЗВІТ ==='; +$_['text_total_files'] = 'Всього файлів: '; +$_['text_searching'] = 'Пошук файлів...'; +$_['error_ajax'] = 'Помилка: '; +$_['text_resumed'] = '▶ ПРОЦЕС ВІДНОВЛЕНО...'; +$_['text_paused'] = '⏸ ПАУЗА. Процес призупинено.'; +$_['confirm_stop'] = 'Ви впевнені, що хочете повністю перервати процес?'; +$_['confirm_delete'] = 'УВАГА! Файли будуть видалені з сервера назавжди без можливості відновлення! Продовжити?'; +$_['confirm_empty_q'] = 'Ви впевнені, що хочете повністю очистити папку карантину?'; +$_['confirm_restore'] = 'Всі файли з папки карантину будуть повернуті на свої місця. Продовжити?'; +$_['button_stopping'] = 'Зупинка...'; +$_['text_stopped_user'] = '🛑 ПРОЦЕС ПЕРЕРВАНО КОРИСТУВАЧЕМ!'; +$_['text_increase'] = 'Збільшення'; +$_['text_done'] = '✅ ГОТОВО! Процес завершено.'; +$_['error_timeout'] = 'Таймаут сервера. Автоповтор через 3 секунди...'; +$_['text_building_index'] = 'Крок 1/2: Індексація БД та шаблонів (пошук фото, що використовуються)...'; +$_['text_comparing'] = 'Крок 2/2: Порівняння фізичних файлів з індексом...'; +$_['text_quarantine_done'] = '✅ Процес перенесення в image/catalog_trash/ завершено!'; +$_['text_deleted_done'] = '✅ Процес видалення сміття завершено!'; +$_['text_quarantine_empty'] = '✅ Папку карантину успішно видалено з сервера!'; +$_['text_restore_done'] = '✅ Файли успішно повернуто з карантину в робочу папку!'; +$_['text_quarantine_move'] = 'В карантині: '; +$_['text_deleted_file'] = 'Видалено: '; +$_['text_no_trash'] = 'У вибраних папках сміття не знайдено! Всі файли використовуються.'; +$_['text_scanning_dirs'] = 'Скануємо директорії: '; +$_['text_log_used'] = '[ВИКОРИСТОВУЄТЬСЯ]'; +$_['text_log_trash'] = '[СМІТТЯ]'; +$_['text_log_deleted'] = '[ВИДАЛЕНО]'; +$_['text_log_del_err'] = '[ПОМИЛКА ВИДАЛЕННЯ]'; +$_['text_log_quarantine'] = '[В КАРАНТИН]'; +$_['text_log_q_err'] = '[ПОМИЛКА ПЕРЕНЕСЕННЯ]'; +$_['text_log_restored'] = '[ВІДНОВЛЕНО]'; +$_['text_log_rest_err'] = '[ПОМИЛКА ВІДНОВЛЕННЯ]'; +$_['text_used_in'] = 'у: '; +$_['text_analyzing'] = 'Аналіз бази даних...'; +$_['text_clean_empty'] = 'Чисто! Сміття немає.'; +$_['text_processing'] = 'Обробка...'; +$_['text_restoring'] = 'Відновлення файлів...'; +$_['text_all_folders'] = 'image/catalog/ (Та всі вкладені)'; +$_['text_root_folder'] = 'image/catalog/ (Тільки корінь)'; +$_['text_q_empty_err'] = 'Папка карантину порожня або не існує.'; +$_['error_token'] = 'Невірний токен. Перезавантажте сторінку.'; +$_['text_apply_title'] = 'Оптимізацію завершено в папку: '; +$_['text_apply_info'] = 'Перевірте результат. Якщо все влаштовує — застосуйте зміни до основного каталогу.'; +$_['entry_apply_backup'] = 'Перенести зі створенням бекапу (в Карантин)'; +$_['entry_apply_replace'] = 'Замінити оригінали безповоротно'; +$_['button_apply_main'] = 'ЗАСТОСУВАТИ ДО ОСНОВНОГО КАТАЛОГУ'; +$_['button_delete_copies'] = 'Видалити тимчасові копії (скасування)'; +$_['button_full_cleanup'] = 'Повне очищення всіх тимчасових папок та бекапів'; +$_['text_apply_success'] = '✅ Файли успішно перенесено. '; +$_['text_apply_backup_ok'] = 'Оригінали збережено в папку: '; +$_['text_apply_no_backup'] = 'Оригінали видалено.'; +$_['text_error_source'] = 'Помилка: Папку з оптимізованими файлами не знайдено.'; +$_['confirm_full_cleanup'] = 'УВАГА! Ця дія безповоротно видалить УСІ папки бекапів (backup_*) та тимчасові папки з копіями. Продовжити?'; +$_['text_tools_desc'] = 'Глобальні сканери та інструменти для роботи з файловою системою і БД зображень.'; +$_['text_tool_select'] = 'Виберіть інструмент:'; +$_['text_group_scanners'] = 'Сканери та Аналіз'; +$_['text_group_generators'] = 'Генератори та Обробка'; +$_['text_group_experimental'] = 'Експериментальні функції (BETA)'; +$_['text_experimental_warn'] = 'Увага: Експериментальні функції зачіпають системні файли або базу даних. Настійно рекомендується зробити бекап!'; +$_['text_tool_broken_db'] = 'Сканер битих/порожніх фото БД'; +$_['text_tool_watermark'] = 'Динамічний Вотермарк (Тільки на кеш)'; +$_['text_tool_duplicates'] = 'Поиск дубликатов файлов (за MD5)'; +$_['text_tool_png_jpg'] = 'Конвертер важких PNG у JPG (сканер файлів без прозорості)'; +$_['text_tool_placeholders'] = 'Генератор заглушок (Замість No Image)'; +$_['text_tool_exif'] = 'Очищення EXIF-даних (Геотеги/Мета)'; +$_['text_tool_small_photos'] = 'Сканер занадто дрібних фото'; +$_['text_tool_html_broken'] = 'Пошук битих картинок в HTML (Описах)'; +$_['text_tool_empty_folders'] = 'Очищення порожніх папок (image/catalog)'; +$_['text_tool_smart_cache'] = 'Розумне очищення кешу зображень'; +$_['text_tool_folder_tree'] = 'Дерево папок (Статистика ваги)'; +$_['text_tool_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load (Без БД)'; +$_['text_tool_translit'] = 'Транслітерація назв файлів'; +$_['text_tool_cache_restore'] = 'Відновлення оригіналів із Кешу'; +$_['entry_wm_type'] = 'Тип водяного знака'; +$_['text_wm_image'] = 'Зображення (PNG)'; +$_['text_wm_text'] = 'Текст'; +$_['entry_wm_image_path'] = 'Шлях до зображення'; +$_['entry_wm_text_val'] = 'Текст водяного знака'; +$_['entry_wm_position'] = 'Позиція (1-9)'; +$_['entry_wm_opacity'] = 'Прозорість (0-100)'; +$_['entry_wm_category'] = 'Тільки для Категорій'; +$_['entry_wm_brand'] = 'Тільки для Брендів'; +$_['entry_min_width'] = 'Мінімальна ширина (px)'; +$_['entry_min_height'] = 'Мінімальна висота (px)'; +$_['entry_ph_product'] = 'Заглушка для Товарів'; +$_['entry_ph_category'] = 'Заглушка для Категорій'; +$_['entry_ph_brand'] = 'Заглушка для Брендів'; +$_['button_run_tool'] = 'Запустити інструмент'; +$_['button_scan_tool'] = 'Сканувати'; +$_['button_fix_tool'] = 'Виправити знайдене'; +$_['text_th_path'] = 'Файл / Шлях'; +$_['text_th_size'] = 'Розмір'; +$_['text_th_action'] = 'Дія'; +$_['text_th_status'] = 'Статус'; +$_['text_th_problem'] = 'Проблема / Значення'; +$_['text_cron_title'] = 'Налаштування Cron'; +$_['text_cron_desc'] = 'Додайте команду в Cron хостингу для автоматичного фонового стиснення.'; +$_['entry_cron_new_only'] = 'Обробляти тільки нові (необроблені) файли'; +$_['entry_cron_folders'] = 'Папки для сканування (залиште порожнім для всього каталогу)'; +$_['text_promo_title'] = 'Інші модулі серії [NAT]'; +$_['text_promo_desc'] = 'Модулі серії [NAT] – це інструменти для глибокої оптимізації OpenCart. Ми фокусуємося на автоматизації рутини, прискоренні бази даних та очищенні серверів від сміття. Усі модулі мають відкритий вихідний код та єдину логіку управління.'; +$_['text_support'] = 'Технічна підтримка та побажання:'; +$_['button_more'] = 'Дивитися всі модулі на OpenCart.com'; +$_['button_more_forum'] = 'Opencartforum'; +$_['text_mod_cat_opt'] = 'Catalog Optimizer [NAT]'; +$_['text_mod_cat_opt_desc'] = 'Приховування порожніх категорій і брендів + SEO сортування товарів (у наявності зверху) + Очищення сміття БД.'; +$_['text_mod_new_arrivals'] = 'New Arrivals [NAT]: Новинки за датами'; +$_['text_mod_new_arrivals_desc'] = 'Автоматична SEO-сторінка новинок. Відображає новинки суворо за реальною датою додавання. Слайдери, сітка та розумний сайдбар архіву дат.'; +$_['text_mod_img_opt'] = 'Image Optimizer [NAT]'; +$_['text_mod_img_opt_desc'] = 'Поточний модуль. Стиснення та ресайз ОРИГІНАЛІВ фото (image/catalog) у WebP + Очищення хостингу від невикористаних зображень.'; +$_['text_mod_dom_scan'] = 'Domain Scanner [NAT]'; +$_['text_mod_dom_scan_desc'] = 'Пошук і чищення зовнішніх посилань і HTTP-картинок у БД та файлах теми. Ідеально для HTTPS або очищення після парсингу.'; +$_['text_mod_poll'] = 'Poll Widget [NAT]'; +$_['text_mod_poll_desc'] = 'Створення опитувань та голосувань із візуальним конструктором дизайну, глибокою статистикою та захистом від накруток.'; +$_['text_mod_db_opt'] = 'Database Optimizer [NAT]'; +$_['text_mod_db_opt_desc'] = 'Інтелектуальне очищення сміття в базі даних, управління системними логами та додавання критично важливих індексів для прискорення.'; +$_['text_mod_im_ex'] = 'Import/Export [NAT]'; +$_['text_mod_im_ex_desc'] = 'Швидкий імпорт/експорт цін і залишків з файлів xls/xlsx/csv/xml/json або за посиланням. Маппінг полів, робота по Cron, текстові функції.'; +$_['text_mod_admin_tools'] = 'Admin Tools [NAT]'; +$_['text_mod_admin_tools_desc'] = 'Зручні інструменти адміністратора: вхід під клієнтом, кастомізація адмінки, управління правами користувачів та приховування непотрібних пунктів меню.'; +$_['text_mod_utm_tracker'] = 'Order UTM Tracker [NAT]'; +$_['text_mod_utm_tracker_desc'] = 'Автоматичне відстеження UTM-міток при оформленні замовлення. Зберігає джерело переходу, кампанію, ключові слова та виводить ці дані в замовленні для аналітики реклами.'; +$_['text_info_title_promo_top_bar'] = 'Promo Top Bar [NAT]: Верхній рекламний інфо-баннер, інформаційна смуга над шапкою, рухомий рядок та топ-баннер'; +$_['text_info_desc_promo_top_bar'] = 'Створення інтерактивних промо-панелей та слайд-шоу в шапці сайту з конструктором блоків, таймерами, формою підписки та гнучким націлюванням.'; +$_['text_tool_no_image'] = 'Товари без головного фото (порожньо в БД)'; +$_['text_problem_main_broken'] = 'Бите головне фото (файл видалено)'; +$_['text_problem_add_broken'] = 'Бите дод. фото (файл видалено)'; +$_['text_problem_no_image'] = 'Відсутнє головне фото'; +$_['text_err_dir'] = 'Помилка: Директорію не знайдено або невірний шлях.'; +$_['text_err_no_queue'] = 'Помилка: Чергу не знайдено. Спочатку запустіть сканування.'; +$_['text_log_del_empty_folder'] = 'Видалено порожню папку: '; +$_['text_log_no_empty_folders'] = 'Порожніх папок в image/catalog/ не знайдено.'; +$_['text_log_total_empty_folders'] = 'Всього видалено порожніх папок: '; +$_['text_log_cache_cleared'] = 'Кеш зображень успішно очищено.'; +$_['text_log_wm_smart_mode'] = 'Вотермарки працюють в SMART-режимі: оригінали не зачіпаються, логотип накладається лише при генерації кешу.'; +$_['text_log_scan_dupes'] = 'Сканування image/catalog/ на дублікати (за хешем MD5)... Це може зайняти час.'; +$_['text_log_found_dupe'] = 'Знайдено дублікат: '; +$_['text_log_no_dupes'] = 'Дублікатів не знайдено.'; +$_['text_log_total_dupes'] = 'Знайдено дублікатів: %s. Потенційна економія: %s'; +$_['text_log_fix_dupes_prompt'] = 'Натисніть "Виправити знайдене" для перелінкування БД на оригинал та видалення дублів.'; +$_['text_log_fixed_dupe'] = 'Виправлено та видалено дублікат: '; +$_['text_log_success_dupes'] = 'Успішно виправлено дублікатів: %s. Посилання в БД оновлено.'; +$_['text_log_scan_png'] = 'Пошук важких PNG файлів без прозорості...'; +$_['text_log_no_png'] = 'Відповідних PNG файлів не знайдено.'; +$_['text_log_total_png'] = 'Знайдено PNG без прозорості: %s. Натисніть "Виправити" для конвертації в JPG.'; +$_['text_log_converted_jpg'] = 'Сконвертовано в JPG: '; +$_['text_log_success_png'] = 'Успішно сконвертовано %s файлів. Звільнено: %s'; +$_['text_log_ph_prod'] = 'Оновлено товарів заглушкою: '; +$_['text_log_ph_cat'] = 'Оновлено категорій заглушкою: '; +$_['text_log_ph_brand'] = 'Оновлено брендів заглушкою: '; +$_['text_log_ph_empty'] = 'Заглушки не вибрані в налаштуваннях.'; +$_['text_log_scan_exif'] = 'Сканування EXIF-даних (мета-теги камер)...'; +$_['text_log_stripped_exif'] = 'Очищено EXIF: '; +$_['text_log_no_exif'] = 'Файлів з EXIF для очищення не знайдено.'; +$_['text_log_success_exif'] = 'Очищено EXIF у %s файлів. Звільнено: %s'; +$_['text_log_scan_small'] = 'Пошук фото розміром менше %sx%s px...'; +$_['text_log_too_small'] = 'Занадто мале (%sx%s): '; +$_['text_log_no_small'] = 'Дрібних фото не знайдено.'; +$_['text_log_total_small'] = 'Знайдено дрібних фото: %s. Будь ласка, замініть їх на якісні.'; +$_['text_log_html_broken'] = 'Битией IMG в HTML таблиці %s (ID: %s): '; +$_['text_log_no_html_broken'] = 'Битих картинок в HTML-описах не знайдено.'; +$_['text_log_total_html_broken'] = 'Знайдено битих посилань в HTML: %s. Виправте їх вручну.'; +$_['text_log_scan_smart_cache'] = 'Пошук втрачених файлів у image/cache/...'; +$_['text_log_del_orph_cache'] = 'Видалено втрачений кеш: '; +$_['text_log_no_orph_cache'] = 'Втраченого кешу не знайдено.'; +$_['text_log_success_smart_cache'] = 'Видалено файлів втраченого кешу: %s. Звільнено: %s'; +$_['text_log_scan_translit'] = 'Пошук файлів з кирилицею або пробілами...'; +$_['text_log_no_bad_names'] = 'Файлів з некоректними іменами не знайдено.'; +$_['text_log_total_bad_names'] = 'Знайдено файлів з кирилицею/пробілами: %s. Натисніть "Виправити" для транслітерації та оновлення БД.'; +$_['text_log_renamed'] = 'Перейменовано: '; +$_['text_log_success_translit'] = 'Успішно транслітеровано файлів: %s. БД оновлено.'; +$_['text_log_scan_restore'] = 'Сканирование image/cache/ для відновлення втрачених оригіналів...'; +$_['text_log_restored_cache'] = 'Відновлено з кешу: '; +$_['text_log_no_lost_orig'] = 'Відсутніх оригіналів у кеші не знайдено.'; +$_['text_log_success_restore'] = 'Успішно відновлено оригіналів з кешу: %s.'; +$_['error_invalid_folder'] = 'Помилка: Неприпустиме ім\'я папки!'; +$_['text_formats_title'] = 'Сучасні формати (WebP & SVG)'; +$_['text_formats_desc'] = 'Налаштування інтеграції сучасних форматів у файлову систему та шаблони OpenCart.'; +$_['entry_support_svg'] = 'Підтримка завантаження SVG'; +$_['help_support_svg'] = 'Дозволяє завантаження векторних SVG-зображень через стандартний Filemanager та їх безпечне виведення на вітрині без спотворення розмірів.'; +$_['entry_support_webp'] = 'Підтримка завантаження WebP'; +$_['help_support_webp'] = 'Дозволяє вручну завантажувати, підв\'язувати та виводити на сайті готові файли .webp через Filemanager.'; +$_['entry_truefilemanager_webp'] = 'WebP у TrueFileManager'; +$_['help_truefilemanager_webp'] = 'Додає підтримку завантаження, попереднього перегляду та ескізів WebP у TrueFileManager від sitecreator (якщо його встановлено).'; +$_['entry_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load'; +$_['help_webp_on_fly'] = 'Надшвидка підміна: автоматично змінює розширення .jpg/.png на .webp прямо в HTML-виводі вітрини та додає атрибут loading="lazy". Не чіпає базу даних!'; +$_['help_tool_broken_db'] = 'Шукає в базі даних зображення, фізичні файли яких були видалені з сервера. Дозволяє очистити биті посилання.'; +$_['help_tool_empty_folders'] = 'Рекурсивно сканує директорію image/catalog/ та безпечно видаляє всі папки, в яких немає жодного файлу.'; +$_['help_tool_folder_tree'] = 'Будує дерево каталогів та показує реальну вагу кожної папки для пошуку "пожирачів" місця.'; +$_['help_tool_html_broken'] = 'Сканує HTML-описи товарів та категорій на наявність тегів з неіснуючими зображеннями.'; +$_['help_tool_small_photos'] = 'Знаходить зображення з роздільною здатністю нижче заданої. Додано посилання на пошук якісних аналогів у Google Images та Яндекс Зображеннях.'; +$_['help_tool_duplicates'] = 'Шукає повні дублікати файлів за MD5-хешем (різні назви, але однакова картинка). Дозволяє склеїти їх в один файл та оновити БД.'; +$_['help_tool_watermark'] = 'Накладення водяних знаків у SMART-режимі (тільки на кеш вітрини, оригінали не змінюються).'; +$_['help_tool_placeholders'] = 'Масово встановлює вибране зображення-заглушку для товарів та категорій, у яких взагалі немає фото.'; +$_['help_tool_png_jpg'] = 'Інструмент сканує папку зображень і знаходить важкі PNG-файли, які не мають прозорого фону (альфа-каналу). Оскільки прозорість їм не потрібна, їх можна безпечно конвертувати у формат JPG. Це значно полегшує файли (в середньому на 70-80%) та прискорює завантаження сайту, а всі посилання в базі даних автоматично оновлюються.'; +$_['help_tool_exif'] = 'Сканує EXIF-дані та видаляє з них приховані теги, зменшуючи вагу на 10-15%.'; +$_['help_tool_smart_cache'] = 'Шукає в папці image/cache/ кешовані файли, оригінали яких уже видалені, та очищає неактуальний кеш.'; +$_['help_tool_translit'] = 'Знаходить файли з кирилицею або пробілами, перейменовує їх транслітом та оновлює шляхи в БД.'; +$_['help_tool_cache_restore'] = 'Якщо ви випадково видалили оригінал з catalog, інструмент спробує знайти його копію в cache та відновити назад.'; +$_['entry_quarantine'] = 'Перемістити оригінали в Карантин (замість видалення)'; +$_['entry_backup_db'] = 'Створити бекап змінюваних таблиць у SQL перед запуском'; +$_['button_fix_selected'] = 'ВИПРАВИТИ ВИБРАНІ'; +$_['button_export_log'] = 'Експортувати в CSV'; +$_['text_th_before'] = 'Стан ДО'; +$_['text_th_after'] = 'Очікуваний результат ПІСЛЯ'; +$_['text_th_select'] = 'Дія'; +$_['text_th_preview'] = 'Передперегляд'; +$_['text_th_name'] = 'Об`єкт / Файл'; +$_['text_global_action'] = 'Групова дія для вибраних:'; +$_['text_action_clear'] = 'Очистити посилання в БД'; +$_['text_action_disable'] = 'Вимкнути (Статус = 0)'; +$_['text_action_stock'] = 'Установити "Немає в наявності"'; +$_['text_action_placeholder'] = 'Встановити заглушку'; +$_['text_action_ignore'] = 'Нічого не робити'; +$_['text_action_merge'] = 'Склеїти (видалити дублікат, залишити оригінал)'; +$_['text_action_convert'] = 'Конвертувати в JPG'; +$_['text_action_rename'] = 'Перейменувати транслітом'; +$_['text_action_delete'] = 'Видалити'; +$_['text_action_restore'] = 'Відновити'; +$_['text_db_backup_created'] = 'Створено бекап БД: %s'; +$_['text_quarantine_created'] = 'Перенесено в карантин: %s'; +$_['text_fix_status_success'] = 'Успішно виконано'; +$_['text_fix_status_error'] = 'Помилка: %s'; +$_['text_log_exported'] = 'Лог успішно експортовано!'; +$_['text_select_action'] = '-- Виберіть дію --'; +$_['error_empty_selection'] = 'Помилка: Не вибрано жодного рядка для виправлення!'; +$_['text_link_admin'] = 'Адмінка'; +$_['text_link_catalog'] = 'Вітрина'; +$_['entry_threads'] = 'Потоків обробки'; +$_['text_threads_optimal'] = 'оптимально'; +$_['button_download_zip'] = 'Завантажити ZIP-архів та видалити папку'; +$_['confirm_download_and_delete'] = 'Архів успішно завантажився? Можна видалити тимчасову папку копій на сервері?'; +$_['button_clear_backups'] = 'Видалити бекапи БД'; +$_['button_clear_backups_short'] = 'Очистити бекапи'; +$_['confirm_clear_backups'] = 'Ви впевнені, що хочете повністю видалити всі SQL-бекапи таблиць?'; +$_['text_compare_title'] = 'Порівняння зображень ДО / ПІСЛЯ'; +$_['text_compare_before'] = 'ДО'; +$_['text_compare_after'] = 'ПІСЛЯ'; +$_['text_backups_cleared'] = 'Усі SQL-бекапи таблиць успішно видалені!'; +$_['error_no_backups'] = 'Бекапи не знайдені або папка порожня.'; +$_['button_restore'] = 'Повернути'; +$_['button_empty'] = 'Очистити'; +$_['entry_wm_targets'] = 'Застосовувати водяний знак до:'; +$_['text_wm_target_product'] = 'Фото товарів'; +$_['text_wm_target_category'] = 'Фото категорій'; +$_['text_wm_target_brand'] = 'Логотипи брендів'; +$_['text_wm_target_banner'] = 'Банери та слайди'; +$_['text_wm_target_blog'] = 'Статті / Блог'; +$_['error_no_scan_data'] = 'Помилка: Дані сканування не знайдені. Спочатку запустіть сканування!'; +$_['error_row_not_found'] = 'Помилка: Об`єкт не знайдено в результатах сканування!'; +$_['error_file_not_found'] = 'Помилка: Файл не знайдено на сервері!'; +$_['text_folder_rules_title'] = 'Індивідуальні правила стиснення для папок'; +$_['text_folder_rules_desc'] = 'Ви можете перевизначити якість та максимальні розміри для конкретних папок. Якщо залишити поле порожнім, використовуватиметься глобальне налаштування. Шлях вказується відносно папки image (наприклад, catalog/banners/).'; +$_['entry_folder_path'] = 'Шлях до папки'; +$_['entry_jpg_quality'] = 'Якість JPG'; +$_['entry_webp_quality'] = 'Якість WebP'; +$_['button_add_rule'] = 'Додати правило'; +$_['button_remove'] = 'Вилучити'; +$_['entry_wm_angle'] = 'Кут нахилу водяного знака (градуси)'; +$_['entry_wm_corner_radius'] = 'Закруглення кутів водяного знака (px)'; +$_['entry_wm_size_type'] = 'Масштабування водяного знака'; +$_['entry_wm_size_percent'] = 'Розмір водяного знака (% від ширини зображення)'; +$_['entry_wm_text_color'] = 'Колір тексту водяного знака'; +$_['entry_wm_text_font'] = 'Шрифт водяного знака (TTF)'; +$_['entry_wm_filter_mode'] = 'Режим фільтрації Категорій/Брендів'; +$_['text_wm_filter_include'] = 'Тільки для вибраних'; +$_['text_wm_filter_exclude'] = 'Для всіх, крім вибраних'; +$_['text_wm_size_original'] = 'Оригінальний розмір'; +$_['text_wm_size_percent'] = 'Пропорційно ширині зображення (%)'; +$_['button_preview_watermark'] = 'Попередній перегляд'; +$_['text_wm_preview_title'] = 'Попередній перегляд водяного знака'; +$_['text_cron_exclude_folders'] = 'Виключити папки зі сканування'; +$_['text_cron_entities'] = 'Які типи фото обробляти?'; +$_['text_cron_quality_override'] = 'Перевизначити параметри стиснення для Крона'; +$_['text_cron_recommendations'] = 'Рекомендації щодо розкладу Cron'; +$_['text_cron_recommendations_desc'] = '
    • Для невеликих сайтів (до 5000 товарів) рекомендується запускати Крон кожну годину (0 * * * *).
    • Для середніх сайтів (5000-20000 товарів) рекомендується запускати раз на 3-6 годин (0 */6 * * *).
    • Для великих сайтів (понад 20000 товарів) — один раз на добу вночі (наприклад, о 2:00: 0 2 * * *).
    '; +$_['text_tool_broken_files'] = 'Сканер битих/порожніх файлів на сервері'; +$_['help_tool_broken_files'] = 'Сканує фізичні файли в папці image/catalog/ на наявність пошкоджених зображень або порожніх файлів розміром 0 байт.'; +$_['text_grid_broken_file'] = 'Битий або порожній файл на сервері'; +$_['text_grid_broken_file_after'] = 'Вилучення або заміна заглушкою'; +$_['text_description_broken_files'] = 'Результати сканування фізичних файлів у папці image/catalog/. Тут відображаються биті (пошкоджені) зображення або файли розміром 0 байт.'; +$_['text_action_strip'] = 'Очистити EXIF'; +$_['text_action_set_placeholder'] = 'Встановити заглушку'; +$_['text_th_entity'] = 'Об`єкт / Пов`язані посилання'; +$_['text_cron_entity_product'] = 'Товари'; +$_['text_cron_entity_category'] = 'Категорії'; +$_['text_cron_entity_manufacturer'] = 'Виробники / Бренди'; +$_['text_cron_entity_banner'] = 'Банери'; +$_['text_cron_entity_blog'] = 'Блог / Статті'; +$_['entry_cron_jpg_quality'] = 'Якість JPG (Крон)'; +$_['entry_cron_webp_quality'] = 'Якість WebP (Крон)'; +$_['entry_cron_max_width'] = 'Макс. ширина px (Крон)'; +$_['entry_cron_max_height'] = 'Макс. висота px (Крон)'; +$_['text_cron_folder_scan'] = 'Папки для сканування'; +$_['text_no_fonts'] = 'TTF-шрифти не знайдено в system/library/font/'; +$_['button_apply'] = 'Застосувати'; +$_['text_backup_will_be_created'] = 'Резервна копія бази даних буде автоматично створена перед виконанням цієї дії.'; +$_['text_bulk_action'] = 'Групова дія для вибраних'; +$_['text_grid_results'] = 'Результати та деталі сканування'; +$_['text_quarantine_will_be_created'] = 'Оригінальні файли будуть переміщені в карантин (image/catalog_trash/) перед зміною.'; +$_['text_success_apply'] = 'Зміни успішно застосовані!'; +$_['text_description_broken'] = 'Результати сканування бази даних на наявність битих посилань (записи в таблицях з неіснуючими файлами).'; +$_['text_description_cache_restore'] = 'Результати пошуку файлів у кеші, оригінали яких відсутні в каталозі.'; +$_['text_description_duplicates'] = 'Результати пошуку повних дублікатів зображень за MD5-хешем.'; +$_['text_description_empty_folders'] = 'Список порожніх папок в директорії image/catalog/ для видалення.'; +$_['text_description_exif'] = 'Результати пошуку зображень з метаданими EXIF, які можна очистити.'; +$_['text_description_folder_tree'] = 'Статистика розміру папок у каталозі image/catalog/.'; +$_['text_description_html_broken'] = 'Результати пошуку неіснуючих файлів картинок, на які посилаються описи в HTML.'; +$_['text_description_placeholders'] = 'Результати пошуку сутностей без зображень для заповнення заглушками.'; +$_['text_description_png_jpg'] = 'Результати пошуку важких зображень PNG без прозорості, які можна перетиснути в JPG.'; +$_['text_description_small_photos'] = 'Список зображень, розміри яких менше мінімально заданих (%sx%s px).'; +$_['text_description_smart_cache'] = 'Результати пошуку файлів кешу, оригінали яких уже було видалено.'; +$_['text_description_translit'] = 'Результати пошуку файлів з кирилицею або пробілами в назвах.'; +$_['text_grid_broken_db_after'] = 'Очистити посилання або встановити заглушку'; +$_['text_grid_dupe_after'] = 'Оригінал (залишається)'; +$_['text_grid_dupe_before'] = 'Дублікат (буде видалено)'; +$_['text_grid_empty_folder'] = 'Порожня папка'; +$_['text_grid_empty_folder_after'] = 'Вилучити порожню папку'; +$_['text_grid_exif_after'] = 'Очистити метадані'; +$_['text_grid_exif_before'] = 'Містить EXIF'; +$_['text_grid_html_after'] = 'Виправити посилання вручну'; +$_['text_grid_missing_file'] = 'Файл відсутній'; +$_['text_grid_missing_html'] = 'Картинка не знайдена в описі'; +$_['text_grid_missing_orig'] = 'Оригінал відсутній'; +$_['text_grid_missing_orig_after'] = 'Відновити оригінал з кешу'; +$_['text_grid_no_image'] = 'Немає зображення (порожньо)'; +$_['text_grid_orph_cache'] = 'Осиротілий кеш'; +$_['text_grid_orph_cache_after'] = 'Видалити непотрібний кеш'; +$_['text_grid_ph_brand_after'] = 'Встановити заглушку бренду'; +$_['text_grid_ph_category_after'] = 'Встановити заглушку категорії'; +$_['text_grid_ph_product_after'] = 'Встановити заглушку товару'; +$_['text_grid_small_photo_after'] = 'Рекомендується замінити на більше'; +$_['text_grid_too_small'] = 'Занадто мале'; +$_['text_no_relations'] = 'Немає зв’язків у БД'; +$_['text_action_archive'] = 'Архівувати (в ZIP)'; +$_['text_action_archive_delete'] = 'Архівувати + Видалити'; +$_['text_tool_sanitizer'] = 'Санитайзер імен файлів'; +$_['help_tool_sanitizer'] = 'Масове очищення імен файлів і папок від кирилиці, пробілів та спецсимволів, приведення розширень до єдиного вигляду.'; +$_['text_description_sanitizer'] = 'Результати аналізу імен файлів для санітаризації за вибраними правилами.'; +$_['entry_sz_translit'] = 'Транслітерація (кирилиця в латиницю)'; +$_['entry_sz_spaces'] = 'Замінювати пробіли на символ "_"'; +$_['entry_sz_special'] = 'Видаляти спеціальні символи'; +$_['entry_sz_lowercase_name'] = 'Ім`я файлу в нижній регістр'; +$_['entry_sz_lowercase_ext'] = 'Розширення в нижній регістр'; +$_['entry_sz_normalize_ext'] = 'Нормалізувати розширення (jpeg -> jpg)'; +$_['entry_cron_folder_mode'] = 'Режим фільтрації папок'; +$_['text_cron_folder_mode_exclude'] = 'Сканувати все, крім вибраних (Винятки)'; +$_['text_cron_folder_mode_include'] = 'Сканирувати тільки вибрані папки'; +$_['button_select_all'] = 'Вибрати всі'; +$_['button_deselect_all'] = 'Зняти всі'; +$_['entry_wm_upload_font'] = 'Завантажити свій шрифт TTF'; +$_['button_upload_font'] = 'Завантажити шрифт'; +$_['text_success_font_upload'] = 'Шрифт успішно завантажено!'; +$_['error_font_upload'] = 'Помилка завантаження шрифту! Дозволені тільки файли .ttf вагою до 5 МБ.'; +$_['entry_lazy_load'] = 'Прогресивне лениве завантаження'; +$_['help_lazy_load'] = 'Вмикає преміальне прогресивне відкладене завантаження на вітрині. Зображення замінюються на мікро-прев\'ю та плавно розмиваються (blur-up), підвантажуючись у міру прокручування.'; +$_['confirm_replace_mode'] = 'УВАГА! Ви обрали режим «Замінювати оригінали». Усі зображення будуть перезаписані безпосередньо на сервері. Наполегливо рекомендуємо зробити резервну копію. Ви впевнені, що хочете продовжити?'; +$_['text_compare'] = 'Порівняти'; +$_['text_home'] = 'Головна'; +$_['text_tech_gd'] = 'Бібліотека GD'; +$_['text_tech_imagick'] = 'Imagick'; +$_['text_tech_webp'] = 'Підтримка WebP'; +$_['text_position_1'] = '1 - Зліва вгорі'; +$_['text_position_2'] = '2 - По центру вгорі'; +$_['text_position_3'] = '3 - Справа вгорі'; +$_['text_position_4'] = '4 - Зліва посередині'; +$_['text_position_5'] = '5 - По центру'; +$_['text_position_6'] = '6 - Справа посередині'; +$_['text_position_7'] = '7 - Зліва внизу'; +$_['text_position_8'] = '8 - По центру внизу'; +$_['text_position_9'] = '9 - Справа внизу'; + +$_['entry_log_level'] = 'Рівень логу'; +$_['text_log_changed_only'] = 'Тільки змінені'; +$_['text_log_all'] = 'Всі файли (детально)'; +$_['text_skipped_no_gain'] = 'Пропущено (без покращення)'; +$_['text_reset_cache_hint'] = 'Скинути кеш оптимізації (повне пересканування)'; +$_['confirm_reset_cache'] = 'Скинути кеш оптимізації? Наступне сканування перевірить ВСІ файли знову.'; +$_['text_cache_cleared'] = 'Кеш оптимізації очищено. Наступне сканування перевірить всі файли.'; +$_['error_wm_preview'] = 'Помилка генерації прев\'ю водяного знаку.'; +$_['error_wm_too_large'] = 'Зображення водяного знаку занадто велике (%dx%d px)! Максимально допустимий розмір: %dx%d px.'; +$_['text_apply_errors_warning'] = 'УВАГА: Деякі файли не вдалося замінити (заблоковані або немає прав):'; +$_['text_action_compress'] = 'Стиснути зображення'; +$_['text_estimated'] = 'Прогноз'; +$_['entry_min_savings_bytes'] = 'Мін. стиснення (байт)'; +$_['entry_min_savings_percent'] = 'Мін. стиснення (%)'; +$_['text_optimized'] = 'Оптимізовано'; +$_['entry_broken_fallback'] = 'Заглушка для битих фото'; +$_['help_broken_fallback'] = 'Автоматично замінює відсутні або пошкоджені зображення каталогу на вітрині на зображення-заглушку.'; +$_['entry_fallback_image'] = 'Зображення-заглушка'; +$_['help_fallback_image'] = 'Виберіть зображення, яке буде показано, якщо оригінальний файл картинки товару чи категорії відсутній. За замовчуванням використовується no_image.png.'; +$_['text_tool_cmyk'] = 'Конвертер CMYK в RGB'; +$_['help_tool_cmyk'] = 'Конвертація JPEG зображень з колірного профілю CMYK в sRGB для коректного відображення в браузерах Safari та на пристроях iOS.'; +$_['text_tool_heavy_files'] = 'Пошук важких файлів'; +$_['help_tool_heavy_files'] = 'Пошук найважчих оригінальних зображень у каталозі з можливістю їх швидкого стиснення безпосередньо тут.'; +$_['entry_heavy_files_limit'] = 'Макс. кількість знайдених файлів (всього)'; +$_['text_action_convert_rgb'] = 'Конвертувати в RGB'; +$_['text_description_cmyk'] = 'Знайдено файли в колірному просторі CMYK. Конвертація в sRGB гарантує колірне відображення на всіх платформах, включаючи iOS/macOS.'; +$_['text_description_heavy'] = 'Список із %d найважчих файлів у каталозі зображень. Ви можете оптимізувати їх безпосередньо тут.'; +$_['text_cron_folder_hint_exclude_all'] = 'Статус: Скануватимуться всі папки (винятки не обрані).'; +$_['text_cron_folder_hint_exclude_some'] = 'Статус: Скануватимуться всі папки, ОКРІМ %d позначених.'; +$_['text_cron_folder_hint_include_some'] = 'Статус: Скануватимуться тільки %d позначених папок.'; +$_['text_cron_folder_hint_include_none'] = 'УВАГА: Жодної папки не обрано! Крон нічого не буде сканувати.'; +$_['text_action_apply_changes'] = 'Застосувати вказані зміни'; +$_['entry_wm_status'] = 'Статус водяного знака'; +$_['text_disabled'] = 'Відключено'; +$_['entry_rule_name'] = 'Назва правила'; +$_['entry_rule_status'] = 'Статус правила'; +$_['entry_action'] = 'Дія'; +$_['entry_ph_status'] = 'Статус заглушок'; +$_['entry_sanitizer_limit'] = 'Макс. файлів для пошуку (ліміт)'; +$_['entry_heavy_files_min_size'] = 'Мінимальний розмір'; +$_['entry_png_jpg_limit'] = 'Макс. файлів для пошуку (ліміт)'; +$_['entry_png_jpg_min_size'] = 'Мінімальний розмір'; +$_['text_wm_rule_title'] = 'Правило водяного знака'; +$_['button_close'] = 'Закрити'; +$_['text_ph_rule_title'] = 'Правило заглушки'; +$_['entry_ph_image'] = 'Зображення заглушки'; +$_['button_clear_cache_wm'] = 'Очистити кеш (застосувати водяний знак)'; +$_['text_active'] = 'Активна'; +$_['text_error'] = 'Не активна'; +$_['text_th_dupe_delete'] = 'Файл для видалення (Дублікат)'; +$_['text_th_dupe_keep'] = 'Файл для збереження (Оригінал)'; +$_['text_dupe_original_label'] = 'Оригінал'; +$_['text_dupe_duplicate_label'] = 'Дублікат'; + +$_['text_disk_usage'] = 'Використання диска'; +$_['text_cumulative_stats'] = 'Статистика оптимізації'; +$_['text_disk_catalog'] = 'Каталог зображень'; +$_['text_disk_cache'] = 'Кеш OC'; +$_['text_disk_trash'] = 'Кошик модуля'; +$_['text_disk_other'] = 'Інше'; +$_['text_disk_free'] = 'Вільно'; +$_['text_total_files_opt'] = 'Файлів оброблено'; +$_['text_total_saved'] = 'Всього зекономлено'; +$_['text_avg_saving_pct'] = 'Середня економія'; +$_['text_stats_hint'] = 'Статистика накопичується по всіх сесіях (зберігається локально в браузері)'; +$_['text_reset_stats'] = 'Скинути статистику'; +$_['text_reset_stats_confirm']= 'Скинути накопичену статистику оптимізації?'; +$_['text_export_import'] = 'Експорт / Імпорт налаштувань'; +$_['button_export_settings'] = 'Експорт налаштувань (JSON)'; +$_['button_import_settings'] = 'Імпортувати налаштування'; +$_['help_export_settings'] = 'Завантажити всі налаштування модуля в JSON-файл'; +$_['help_import_settings'] = 'Завантажити раніше експортований JSON для відновлення налаштувань'; +$_['text_import_success'] = 'Налаштування успішно імпортовані. Перезавантажте сторінку для застосування.'; +$_['error_import_file'] = 'Файл не завантажено або помилка завантаження'; +$_['error_import_invalid'] = 'Некоректний файл налаштувань (невірний модуль або формат)'; +$_['text_click_to_load'] = 'Натисніть ↑ для завантаження даних диска'; + + +$_['tab_formats'] = 'Формати зображень'; + +$_['text_disk_quota_legend'] = 'Ліміти диска хостингу (вручну)'; +$_['text_disk_quota_desc'] = 'Якщо графік дискового простору показує некоректні дані (фізичний диск сервера замість ліміту вашого хостингу), ви можете вказати ліміти вашого тарифу вручну. Вкажіть 0, щоб використовувати автовизначення.'; +$_['entry_disk_quota_total'] = 'Виділено диска на хостингу (ГБ)'; +$_['entry_disk_quota_used'] = 'Зайнято диска на хостингу всього (ГБ)'; +$_['entry_auto_disk'] = 'Автооновлення при вході'; + +// Cron Tasks +$_['entry_cron_task_name'] = 'Назва задачі'; +$_['entry_cron_task_summary'] = 'Параметри стиснення'; +$_['button_add_cron_task'] = 'Додати задачу'; +$_['button_copy_cron_url'] = 'Копіювати URL'; +$_['text_cron_recommended_url'] = 'URL для запуску задачі крону:'; + +// Licensing +$_['text_license_required'] = 'Необхідний ліцензійний ключ'; +$_['text_license_required_desc'] = 'Будь ласка, введіть ліцензійний ключ для активації модуля. Усі функції налаштувань та оптимізації заблоковані до введення валідного ключа.'; +$_['entry_token_desc'] = 'Введіть токен активації, виданий для вашого основного домену. Ви можете отримати його в особистому кабінеті на форумі або звернувшись у підтримку.'; +$_['button_activate'] = 'Активувати модуль'; +$_['text_support'] = 'Технічна підтримка'; +$_['text_support_desc'] = 'Якщо у вас ще немає ключа або виникли проблеми, будь ласка, повідомте номер вашого замовлення та домен.'; + +// New settings +$_['entry_wm_max_width'] = 'Макс. ширина водяного знака (px)'; +$_['entry_wm_max_height'] = 'Макс. висота водяного знака (px)'; +$_['help_wm_max_width'] = 'Якщо вихідне зображення водяного знака перевищує цю ширину, воно буде автоматично масштабоване. Вкажіть 0 або залиште порожнім для значення за замовчуванням (800px).'; +$_['help_wm_max_height'] = 'Якщо вихідне зображення водяного знака перевищує цю висоту, воно буде автоматично масштабоване. Вкажіть 0 або залиште порожнім для значення за замовчуванням (800px).'; +$_['entry_disk_status'] = 'Увімкнути сканування диска та графік'; +$_['help_disk_status'] = 'Якщо увімкнено, модуль періодично скануватиме обсяг папки картинок та відображатиме круговую діаграму. Рекомендується вимкнути на великих сайтах для прискорення завантаження панелі.'; +$_['text_disk_settings_title'] = 'Налаштування лімітів та сканування диска'; + diff --git a/upload/admin/language/ukrainian/module/img_opti.php b/upload/admin/language/ukrainian/module/img_opti.php new file mode 100644 index 0000000..646d271 --- /dev/null +++ b/upload/admin/language/ukrainian/module/img_opti.php @@ -0,0 +1,564 @@ +Image Optimizer [NAT]: Стиснення оригіналів та видалення сміття'; +$_['text_extension'] = 'Розширення'; +$_['text_edit'] = 'Налаштування модуля'; +$_['text_success'] = 'Налаштування успішно збережено!'; +$_['entry_warning'] = 'УВАГА: Обов`язково зробіть бекап файлів сайту і особливо папки /image/catalog перед початком роботи!'; +$_['text_tech'] = 'Перевірка технологій сервера:'; +$_['text_author'] = 'Технічна підтримка та побажання: info@nat.od.ua'; +$_['tab_optimize'] = 'Оптимізація та Ресайз'; +$_['tab_cleaner'] = 'Очищення невикористовуваних зображень'; +$_['tab_info'] = 'Інфо / Екосистема [NAT]'; +$_['tab_settings'] = 'Налаштування'; +$_['tab_tools'] = 'Інструменти (pro)'; +$_['tab_cron'] = 'Крон-завдання'; +$_['entry_mode'] = 'Режим роботи'; +$_['text_copy'] = 'Створити копію (безпечно)'; +$_['text_replace'] = 'Замінювати оригінали (потребує бекап!)'; +$_['entry_folder'] = 'Ім`я папки для копії'; +$_['entry_batch'] = 'Файлів за прохід'; +$_['entry_log_limit'] = 'Рядків логу на екрані'; +$_['entry_max_width'] = 'Макс. ширина (px) [0 - без ліміту]'; +$_['entry_max_height'] = 'Макс. висота (px) [0 - без ліміту]'; +$_['entry_jpg'] = 'Якість JPG (0-100)'; +$_['entry_png'] = 'Стиснення PNG (0-9)'; +$_['entry_webp'] = 'Якість WebP (0-100)'; +$_['entry_targets'] = 'Що оптимізувати?'; +$_['text_all_catalog'] = '[ Весь каталог image/catalog ]'; +$_['text_root_files'] = 'Файли тільки в корені image/catalog'; +$_['entry_threshold'] = 'Поріг заміни (%)'; +$_['help_threshold'] = '0: замінювати тільки якщо вага стала меншою. 10: дозволити збільшення ваги до 10%.'; +$_['entry_token'] = 'Токен активації (на корінь домену)'; +$_['entry_status'] = 'Статус модуля'; +$_['entry_menu_position'] = 'Позиція меню [NAT]'; +$_['text_menu_module_only'] = 'Тільки в модулях'; +$_['text_menu_header'] = 'У шапці (Header)'; +$_['text_menu_sidebar'] = 'У бічному меню (Sidebar)'; +$_['text_menu_both'] = 'Скрізь (Шапка + Сайдбар)'; +$_['text_status_on'] = 'Увімкнено'; +$_['text_status_off'] = 'Вимкнено'; +$_['entry_engine'] = 'Рушій обробки зображень'; +$_['text_engine_gd'] = 'GD (Стандарт)'; +$_['text_engine_imagick'] = 'Imagick (Висока якість, збереження профілів sRGB)'; +$_['entry_license'] = 'Ліцензія'; +$_['text_cleaner_info'] = 'Цей інструмент шукає файли, які фізично існують на диску, але ніде не використовуються в базі даних або файлах шаблону.'; +$_['entry_clean_targets'] = 'В яких папках шукати сміття?'; +$_['entry_extended_log'] = 'Розширений лог (показати використовувані photo і де вони знайдені)'; +$_['entry_process_broken'] = 'Враховувати файли з битим кодуванням (імена з символом "?")'; +$_['button_scan_trash'] = 'ЗНАЙТИ СМІТТЯ'; +$_['button_quarantine'] = 'В Карантин (Безпечно)'; +$_['button_delete_trash'] = 'Видалити назавжди'; +$_['button_empty_quarantine'] = 'Очистити папку Карантину'; +$_['button_restore_quarantine'] = 'Повернути з Карантину'; +$_['text_scan_result'] = 'Результати сканування'; +$_['text_trash_files'] = 'Сміттєвих файлів: '; +$_['text_trash_size'] = 'Потенційно звільниться: '; +$_['button_start'] = 'СТАРТ ОПТИМІЗАЦІЇ'; +$_['button_pause'] = 'ПАУЗА'; +$_['button_stop'] = 'ЗУПИНИТИ'; +$_['button_continue'] = 'ПРОДОВЖИТИ'; +$_['button_start_new'] = 'СТАРТ (Новий запуск)'; +$_['button_download_log'] = 'Завантажити лог в TXT'; +$_['button_download'] = 'Завантажити'; +$_['button_save'] = 'Зберегти'; +$_['button_cancel'] = 'Скасувати'; +$_['text_progress'] = 'Прогрес:'; +$_['text_old_size'] = 'Було:'; +$_['text_new_size'] = 'Стало:'; +$_['text_savings'] = 'Економія:'; +$_['text_detailed_log'] = 'Детальний лог'; +$_['text_page'] = 'Стор.'; +$_['text_waiting'] = 'Очікування запуску...'; +$_['text_enabled'] = 'Увімкнено'; +$_['text_disabled_error'] = 'Вимкнено (ПОМИЛКА)'; +$_['text_supported'] = 'Підтримується'; +$_['text_no'] = 'Ні'; +$_['error_permission'] = 'У вас немає прав для керування модулем!'; +$_['error_license'] = 'Помилка доступу: Невірний токен активації для цього домену!'; +$_['error_domain'] = ' (Домен: '; +$_['text_found'] = 'Знайдено файлів: '; +$_['text_compressed'] = 'Стиснуто'; +$_['text_original'] = 'Оригінал'; +$_['text_error_process'] = 'Помилка обробки: '; +$_['error_log_empty'] = 'Лог порожній! Спочатку запустіть процес.'; +$_['text_report_header'] = '=== ЗВІТ ==='; +$_['text_total_files'] = 'Всього файлів: '; +$_['text_searching'] = 'Пошук файлів...'; +$_['error_ajax'] = 'Помилка: '; +$_['text_resumed'] = '▶ ПРОЦЕС ВІДНОВЛЕНО...'; +$_['text_paused'] = '⏸ ПАУЗА. Процес призупинено.'; +$_['confirm_stop'] = 'Ви впевнені, що хочете повністю перервати процес?'; +$_['confirm_delete'] = 'УВАГА! Файли будуть видалені з сервера назавжди без можливості відновлення! Продовжити?'; +$_['confirm_empty_q'] = 'Ви впевнені, що хочете повністю очистити папку карантину?'; +$_['confirm_restore'] = 'Всі файли з папки карантину будуть повернуті на свої місця. Продовжити?'; +$_['button_stopping'] = 'Зупинка...'; +$_['text_stopped_user'] = '🛑 ПРОЦЕС ПЕРЕРВАНО КОРИСТУВАЧЕМ!'; +$_['text_increase'] = 'Збільшення'; +$_['text_done'] = '✅ ГОТОВО! Процес завершено.'; +$_['error_timeout'] = 'Таймаут сервера. Автоповтор через 3 секунди...'; +$_['text_building_index'] = 'Крок 1/2: Індексація БД та шаблонів (пошук фото, що використовуються)...'; +$_['text_comparing'] = 'Крок 2/2: Порівняння фізичних файлів з індексом...'; +$_['text_quarantine_done'] = '✅ Процес перенесення в image/catalog_trash/ завершено!'; +$_['text_deleted_done'] = '✅ Процес видалення сміття завершено!'; +$_['text_quarantine_empty'] = '✅ Папку карантину успішно видалено з сервера!'; +$_['text_restore_done'] = '✅ Файли успішно повернуто з карантину в робочу папку!'; +$_['text_quarantine_move'] = 'В карантині: '; +$_['text_deleted_file'] = 'Видалено: '; +$_['text_no_trash'] = 'У вибраних папках сміття не знайдено! Всі файли використовуються.'; +$_['text_scanning_dirs'] = 'Скануємо директорії: '; +$_['text_log_used'] = '[ВИКОРИСТОВУЄТЬСЯ]'; +$_['text_log_trash'] = '[СМІТТЯ]'; +$_['text_log_deleted'] = '[ВИДАЛЕНО]'; +$_['text_log_del_err'] = '[ПОМИЛКА ВИДАЛЕННЯ]'; +$_['text_log_quarantine'] = '[В КАРАНТИН]'; +$_['text_log_q_err'] = '[ПОМИЛКА ПЕРЕНЕСЕННЯ]'; +$_['text_log_restored'] = '[ВІДНОВЛЕНО]'; +$_['text_log_rest_err'] = '[ПОМИЛКА ВІДНОВЛЕННЯ]'; +$_['text_used_in'] = 'у: '; +$_['text_analyzing'] = 'Аналіз бази даних...'; +$_['text_clean_empty'] = 'Чисто! Сміття немає.'; +$_['text_processing'] = 'Обробка...'; +$_['text_restoring'] = 'Відновлення файлів...'; +$_['text_all_folders'] = 'image/catalog/ (Та всі вкладені)'; +$_['text_root_folder'] = 'image/catalog/ (Тільки корінь)'; +$_['text_q_empty_err'] = 'Папка карантину порожня або не існує.'; +$_['error_token'] = 'Невірний токен. Перезавантажте сторінку.'; +$_['text_apply_title'] = 'Оптимізацію завершено в папку: '; +$_['text_apply_info'] = 'Перевірте результат. Якщо все влаштовує — застосуйте зміни до основного каталогу.'; +$_['entry_apply_backup'] = 'Перенести зі створенням бекапу (в Карантин)'; +$_['entry_apply_replace'] = 'Замінити оригінали безповоротно'; +$_['button_apply_main'] = 'ЗАСТОСУВАТИ ДО ОСНОВНОГО КАТАЛОГУ'; +$_['button_delete_copies'] = 'Видалити тимчасові копії (скасування)'; +$_['button_full_cleanup'] = 'Повне очищення всіх тимчасових папок та бекапів'; +$_['text_apply_success'] = '✅ Файли успішно перенесено. '; +$_['text_apply_backup_ok'] = 'Оригінали збережено в папку: '; +$_['text_apply_no_backup'] = 'Оригінали видалено.'; +$_['text_error_source'] = 'Помилка: Папку з оптимізованими файлами не знайдено.'; +$_['confirm_full_cleanup'] = 'УВАГА! Ця дія безповоротно видалить УСІ папки бекапів (backup_*) та тимчасові папки з копіями. Продовжити?'; +$_['text_tools_desc'] = 'Глобальні сканери та інструменти для роботи з файловою системою і БД зображень.'; +$_['text_tool_select'] = 'Виберіть інструмент:'; +$_['text_group_scanners'] = 'Сканери та Аналіз'; +$_['text_group_generators'] = 'Генератори та Обробка'; +$_['text_group_experimental'] = 'Експериментальні функції (BETA)'; +$_['text_experimental_warn'] = 'Увага: Експериментальні функції зачіпають системні файли або базу даних. Настійно рекомендується зробити бекап!'; +$_['text_tool_broken_db'] = 'Сканер битих/порожніх фото БД'; +$_['text_tool_watermark'] = 'Динамічний Вотермарк (Тільки на кеш)'; +$_['text_tool_duplicates'] = 'Поиск дубликатов файлов (за MD5)'; +$_['text_tool_png_jpg'] = 'Конвертер важких PNG у JPG (сканер файлів без прозорості)'; +$_['text_tool_placeholders'] = 'Генератор заглушок (Замість No Image)'; +$_['text_tool_exif'] = 'Очищення EXIF-даних (Геотеги/Мета)'; +$_['text_tool_small_photos'] = 'Сканер занадто дрібних фото'; +$_['text_tool_html_broken'] = 'Пошук битих картинок в HTML (Описах)'; +$_['text_tool_empty_folders'] = 'Очищення порожніх папок (image/catalog)'; +$_['text_tool_smart_cache'] = 'Розумне очищення кешу зображень'; +$_['text_tool_folder_tree'] = 'Дерево папок (Статистика ваги)'; +$_['text_tool_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load (Без БД)'; +$_['text_tool_translit'] = 'Транслітерація назв файлів'; +$_['text_tool_cache_restore'] = 'Відновлення оригіналів із Кешу'; +$_['entry_wm_type'] = 'Тип водяного знака'; +$_['text_wm_image'] = 'Зображення (PNG)'; +$_['text_wm_text'] = 'Текст'; +$_['entry_wm_image_path'] = 'Шлях до зображення'; +$_['entry_wm_text_val'] = 'Текст водяного знака'; +$_['entry_wm_position'] = 'Позиція (1-9)'; +$_['entry_wm_opacity'] = 'Прозорість (0-100)'; +$_['entry_wm_category'] = 'Тільки для Категорій'; +$_['entry_wm_brand'] = 'Тільки для Брендів'; +$_['entry_min_width'] = 'Мінімальна ширина (px)'; +$_['entry_min_height'] = 'Мінімальна висота (px)'; +$_['entry_ph_product'] = 'Заглушка для Товарів'; +$_['entry_ph_category'] = 'Заглушка для Категорій'; +$_['entry_ph_brand'] = 'Заглушка для Брендів'; +$_['button_run_tool'] = 'Запустити інструмент'; +$_['button_scan_tool'] = 'Сканувати'; +$_['button_fix_tool'] = 'Виправити знайдене'; +$_['text_th_path'] = 'Файл / Шлях'; +$_['text_th_size'] = 'Розмір'; +$_['text_th_action'] = 'Дія'; +$_['text_th_status'] = 'Статус'; +$_['text_th_problem'] = 'Проблема / Значення'; +$_['text_cron_title'] = 'Налаштування Cron'; +$_['text_cron_desc'] = 'Додайте команду в Cron хостингу для автоматичного фонового стиснення.'; +$_['entry_cron_new_only'] = 'Обробляти тільки нові (необроблені) файли'; +$_['entry_cron_folders'] = 'Папки для сканування (залиште порожнім для всього каталогу)'; +$_['text_promo_title'] = 'Інші модулі серії [NAT]'; +$_['text_promo_desc'] = 'Модулі серії [NAT] – це інструменти для глибокої оптимізації OpenCart. Ми фокусуємося на автоматизації рутини, прискоренні бази даних та очищенні серверів від сміття. Усі модулі мають відкритий вихідний код та єдину логіку управління.'; +$_['text_support'] = 'Технічна підтримка та побажання:'; +$_['button_more'] = 'Дивитися всі модулі на OpenCart.com'; +$_['button_more_forum'] = 'Opencartforum'; +$_['text_mod_cat_opt'] = 'Catalog Optimizer [NAT]'; +$_['text_mod_cat_opt_desc'] = 'Приховування порожніх категорій і брендів + SEO сортування товарів (у наявності зверху) + Очищення сміття БД.'; +$_['text_mod_new_arrivals'] = 'New Arrivals [NAT]: Новинки за датами'; +$_['text_mod_new_arrivals_desc'] = 'Автоматична SEO-сторінка новинок. Відображає новинки суворо за реальною датою додавання. Слайдери, сітка та розумний сайдбар архіву дат.'; +$_['text_mod_img_opt'] = 'Image Optimizer [NAT]'; +$_['text_mod_img_opt_desc'] = 'Поточний модуль. Стиснення та ресайз ОРИГІНАЛІВ фото (image/catalog) у WebP + Очищення хостингу від невикористаних зображень.'; +$_['text_mod_dom_scan'] = 'Domain Scanner [NAT]'; +$_['text_mod_dom_scan_desc'] = 'Пошук і чищення зовнішніх посилань і HTTP-картинок у БД та файлах теми. Ідеально для HTTPS або очищення після парсингу.'; +$_['text_mod_poll'] = 'Poll Widget [NAT]'; +$_['text_mod_poll_desc'] = 'Створення опитувань та голосувань із візуальним конструктором дизайну, глибокою статистикою та захистом від накруток.'; +$_['text_mod_db_opt'] = 'Database Optimizer [NAT]'; +$_['text_mod_db_opt_desc'] = 'Інтелектуальне очищення сміття в базі даних, управління системними логами та додавання критично важливих індексів для прискорення.'; +$_['text_mod_im_ex'] = 'Import/Export [NAT]'; +$_['text_mod_im_ex_desc'] = 'Швидкий імпорт/експорт цін і залишків з файлів xls/xlsx/csv/xml/json або за посиланням. Маппінг полів, робота по Cron, текстові функції.'; +$_['text_mod_admin_tools'] = 'Admin Tools [NAT]'; +$_['text_mod_admin_tools_desc'] = 'Зручні інструменти адміністратора: вхід під клієнтом, кастомізація адмінки, управління правами користувачів та приховування непотрібних пунктів меню.'; +$_['text_mod_utm_tracker'] = 'Order UTM Tracker [NAT]'; +$_['text_mod_utm_tracker_desc'] = 'Автоматичне відстеження UTM-міток при оформленні замовлення. Зберігає джерело переходу, кампанію, ключові слова та виводить ці дані в замовленні для аналітики реклами.'; +$_['text_info_title_promo_top_bar'] = 'Promo Top Bar [NAT]: Верхній рекламний інфо-баннер, інформаційна смуга над шапкою, рухомий рядок та топ-баннер'; +$_['text_info_desc_promo_top_bar'] = 'Створення інтерактивних промо-панелей та слайд-шоу в шапці сайту з конструктором блоків, таймерами, формою підписки та гнучким націлюванням.'; +$_['text_tool_no_image'] = 'Товари без головного фото (порожньо в БД)'; +$_['text_problem_main_broken'] = 'Бите головне фото (файл видалено)'; +$_['text_problem_add_broken'] = 'Бите дод. фото (файл видалено)'; +$_['text_problem_no_image'] = 'Відсутнє головне фото'; +$_['text_err_dir'] = 'Помилка: Директорію не знайдено або невірний шлях.'; +$_['text_err_no_queue'] = 'Помилка: Чергу не знайдено. Спочатку запустіть сканування.'; +$_['text_log_del_empty_folder'] = 'Видалено порожню папку: '; +$_['text_log_no_empty_folders'] = 'Порожніх папок в image/catalog/ не знайдено.'; +$_['text_log_total_empty_folders'] = 'Всього видалено порожніх папок: '; +$_['text_log_cache_cleared'] = 'Кеш зображень успішно очищено.'; +$_['text_log_wm_smart_mode'] = 'Вотермарки працюють в SMART-режимі: оригінали не зачіпаються, логотип накладається лише при генерації кешу.'; +$_['text_log_scan_dupes'] = 'Сканування image/catalog/ на дублікати (за хешем MD5)... Це може зайняти час.'; +$_['text_log_found_dupe'] = 'Знайдено дублікат: '; +$_['text_log_no_dupes'] = 'Дублікатів не знайдено.'; +$_['text_log_total_dupes'] = 'Знайдено дублікатів: %s. Потенційна економія: %s'; +$_['text_log_fix_dupes_prompt'] = 'Натисніть "Виправити знайдене" для перелінкування БД на оригинал та видалення дублів.'; +$_['text_log_fixed_dupe'] = 'Виправлено та видалено дублікат: '; +$_['text_log_success_dupes'] = 'Успішно виправлено дублікатів: %s. Посилання в БД оновлено.'; +$_['text_log_scan_png'] = 'Пошук важких PNG файлів без прозорості...'; +$_['text_log_no_png'] = 'Відповідних PNG файлів не знайдено.'; +$_['text_log_total_png'] = 'Знайдено PNG без прозорості: %s. Натисніть "Виправити" для конвертації в JPG.'; +$_['text_log_converted_jpg'] = 'Сконвертовано в JPG: '; +$_['text_log_success_png'] = 'Успішно сконвертовано %s файлів. Звільнено: %s'; +$_['text_log_ph_prod'] = 'Оновлено товарів заглушкою: '; +$_['text_log_ph_cat'] = 'Оновлено категорій заглушкою: '; +$_['text_log_ph_brand'] = 'Оновлено брендів заглушкою: '; +$_['text_log_ph_empty'] = 'Заглушки не вибрані в налаштуваннях.'; +$_['text_log_scan_exif'] = 'Сканування EXIF-даних (мета-теги камер)...'; +$_['text_log_stripped_exif'] = 'Очищено EXIF: '; +$_['text_log_no_exif'] = 'Файлів з EXIF для очищення не знайдено.'; +$_['text_log_success_exif'] = 'Очищено EXIF у %s файлів. Звільнено: %s'; +$_['text_log_scan_small'] = 'Пошук фото розміром менше %sx%s px...'; +$_['text_log_too_small'] = 'Занадто мале (%sx%s): '; +$_['text_log_no_small'] = 'Дрібних фото не знайдено.'; +$_['text_log_total_small'] = 'Знайдено дрібних фото: %s. Будь ласка, замініть їх на якісні.'; +$_['text_log_html_broken'] = 'Битией IMG в HTML таблиці %s (ID: %s): '; +$_['text_log_no_html_broken'] = 'Битих картинок в HTML-описах не знайдено.'; +$_['text_log_total_html_broken'] = 'Знайдено битих посилань в HTML: %s. Виправте їх вручну.'; +$_['text_log_scan_smart_cache'] = 'Пошук втрачених файлів у image/cache/...'; +$_['text_log_del_orph_cache'] = 'Видалено втрачений кеш: '; +$_['text_log_no_orph_cache'] = 'Втраченого кешу не знайдено.'; +$_['text_log_success_smart_cache'] = 'Видалено файлів втраченого кешу: %s. Звільнено: %s'; +$_['text_log_scan_translit'] = 'Пошук файлів з кирилицею або пробілами...'; +$_['text_log_no_bad_names'] = 'Файлів з некоректними іменами не знайдено.'; +$_['text_log_total_bad_names'] = 'Знайдено файлів з кирилицею/пробілами: %s. Натисніть "Виправити" для транслітерації та оновлення БД.'; +$_['text_log_renamed'] = 'Перейменовано: '; +$_['text_log_success_translit'] = 'Успішно транслітеровано файлів: %s. БД оновлено.'; +$_['text_log_scan_restore'] = 'Сканирование image/cache/ для відновлення втрачених оригіналів...'; +$_['text_log_restored_cache'] = 'Відновлено з кешу: '; +$_['text_log_no_lost_orig'] = 'Відсутніх оригіналів у кеші не знайдено.'; +$_['text_log_success_restore'] = 'Успішно відновлено оригіналів з кешу: %s.'; +$_['error_invalid_folder'] = 'Помилка: Неприпустиме ім\'я папки!'; +$_['text_formats_title'] = 'Сучасні формати (WebP & SVG)'; +$_['text_formats_desc'] = 'Налаштування інтеграції сучасних форматів у файлову систему та шаблони OpenCart.'; +$_['entry_support_svg'] = 'Підтримка завантаження SVG'; +$_['help_support_svg'] = 'Дозволяє завантаження векторних SVG-зображень через стандартний Filemanager та їх безпечне виведення на вітрині без спотворення розмірів.'; +$_['entry_support_webp'] = 'Підтримка завантаження WebP'; +$_['help_support_webp'] = 'Дозволяє вручну завантажувати, підв\'язувати та виводити на сайті готові файли .webp через Filemanager.'; +$_['entry_truefilemanager_webp'] = 'WebP у TrueFileManager'; +$_['help_truefilemanager_webp'] = 'Додає підтримку завантаження, попереднього перегляду та ескізів WebP у TrueFileManager від sitecreator (якщо його встановлено).'; +$_['entry_webp_on_fly'] = 'On-the-Fly WebP + Lazy Load'; +$_['help_webp_on_fly'] = 'Надшвидка підміна: автоматично змінює розширення .jpg/.png на .webp прямо в HTML-виводі вітрини та додає атрибут loading="lazy". Не чіпає базу даних!'; +$_['help_tool_broken_db'] = 'Шукає в базі даних зображення, фізичні файли яких були видалені з сервера. Дозволяє очистити биті посилання.'; +$_['help_tool_empty_folders'] = 'Рекурсивно сканує директорію image/catalog/ та безпечно видаляє всі папки, в яких немає жодного файлу.'; +$_['help_tool_folder_tree'] = 'Будує дерево каталогів та показує реальну вагу кожної папки для пошуку "пожирачів" місця.'; +$_['help_tool_html_broken'] = 'Сканує HTML-описи товарів та категорій на наявність тегів з неіснуючими зображеннями.'; +$_['help_tool_small_photos'] = 'Знаходить зображення з роздільною здатністю нижче заданої. Додано посилання на пошук якісних аналогів у Google Images та Яндекс Зображеннях.'; +$_['help_tool_duplicates'] = 'Шукає повні дублікати файлів за MD5-хешем (різні назви, але однакова картинка). Дозволяє склеїти їх в один файл та оновити БД.'; +$_['help_tool_watermark'] = 'Накладення водяних знаків у SMART-режимі (тільки на кеш вітрини, оригінали не змінюються).'; +$_['help_tool_placeholders'] = 'Масово встановлює вибране зображення-заглушку для товарів та категорій, у яких взагалі немає фото.'; +$_['help_tool_png_jpg'] = 'Інструмент сканує папку зображень і знаходить важкі PNG-файли, які не мають прозорого фону (альфа-каналу). Оскільки прозорість їм не потрібна, їх можна безпечно конвертувати у формат JPG. Це значно полегшує файли (в середньому на 70-80%) та прискорює завантаження сайту, а всі посилання в базі даних автоматично оновлюються.'; +$_['help_tool_exif'] = 'Сканує EXIF-дані та видаляє з них приховані теги, зменшуючи вагу на 10-15%.'; +$_['help_tool_smart_cache'] = 'Шукає в папці image/cache/ кешовані файли, оригінали яких уже видалені, та очищає неактуальний кеш.'; +$_['help_tool_translit'] = 'Знаходить файли з кирилицею або пробілами, перейменовує їх транслітом та оновлює шляхи в БД.'; +$_['help_tool_cache_restore'] = 'Якщо ви випадково видалили оригінал з catalog, інструмент спробує знайти його копію в cache та відновити назад.'; +$_['entry_quarantine'] = 'Перемістити оригінали в Карантин (замість видалення)'; +$_['entry_backup_db'] = 'Створити бекап змінюваних таблиць у SQL перед запуском'; +$_['button_fix_selected'] = 'ВИПРАВИТИ ВИБРАНІ'; +$_['button_export_log'] = 'Експортувати в CSV'; +$_['text_th_before'] = 'Стан ДО'; +$_['text_th_after'] = 'Очікуваний результат ПІСЛЯ'; +$_['text_th_select'] = 'Дія'; +$_['text_th_preview'] = 'Передперегляд'; +$_['text_th_name'] = 'Об`єкт / Файл'; +$_['text_global_action'] = 'Групова дія для вибраних:'; +$_['text_action_clear'] = 'Очистити посилання в БД'; +$_['text_action_disable'] = 'Вимкнути (Статус = 0)'; +$_['text_action_stock'] = 'Установити "Немає в наявності"'; +$_['text_action_placeholder'] = 'Встановити заглушку'; +$_['text_action_ignore'] = 'Нічого не робити'; +$_['text_action_merge'] = 'Склеїти (видалити дублікат, залишити оригінал)'; +$_['text_action_convert'] = 'Конвертувати в JPG'; +$_['text_action_rename'] = 'Перейменувати транслітом'; +$_['text_action_delete'] = 'Видалити'; +$_['text_action_restore'] = 'Відновити'; +$_['text_db_backup_created'] = 'Створено бекап БД: %s'; +$_['text_quarantine_created'] = 'Перенесено в карантин: %s'; +$_['text_fix_status_success'] = 'Успішно виконано'; +$_['text_fix_status_error'] = 'Помилка: %s'; +$_['text_log_exported'] = 'Лог успішно експортовано!'; +$_['text_select_action'] = '-- Виберіть дію --'; +$_['error_empty_selection'] = 'Помилка: Не вибрано жодного рядка для виправлення!'; +$_['text_link_admin'] = 'Адмінка'; +$_['text_link_catalog'] = 'Вітрина'; +$_['entry_threads'] = 'Потоків обробки'; +$_['text_threads_optimal'] = 'оптимально'; +$_['button_download_zip'] = 'Завантажити ZIP-архів та видалити папку'; +$_['confirm_download_and_delete'] = 'Архів успішно завантажився? Можна видалити тимчасову папку копій на сервері?'; +$_['button_clear_backups'] = 'Видалити бекапи БД'; +$_['button_clear_backups_short'] = 'Очистити бекапи'; +$_['confirm_clear_backups'] = 'Ви впевнені, що хочете повністю видалити всі SQL-бекапи таблиць?'; +$_['text_compare_title'] = 'Порівняння зображень ДО / ПІСЛЯ'; +$_['text_compare_before'] = 'ДО'; +$_['text_compare_after'] = 'ПІСЛЯ'; +$_['text_backups_cleared'] = 'Усі SQL-бекапи таблиць успішно видалені!'; +$_['error_no_backups'] = 'Бекапи не знайдені або папка порожня.'; +$_['button_restore'] = 'Повернути'; +$_['button_empty'] = 'Очистити'; +$_['entry_wm_targets'] = 'Застосовувати водяний знак до:'; +$_['text_wm_target_product'] = 'Фото товарів'; +$_['text_wm_target_category'] = 'Фото категорій'; +$_['text_wm_target_brand'] = 'Логотипи брендів'; +$_['text_wm_target_banner'] = 'Банери та слайди'; +$_['text_wm_target_blog'] = 'Статті / Блог'; +$_['error_no_scan_data'] = 'Помилка: Дані сканування не знайдені. Спочатку запустіть сканування!'; +$_['error_row_not_found'] = 'Помилка: Об`єкт не знайдено в результатах сканування!'; +$_['error_file_not_found'] = 'Помилка: Файл не знайдено на сервері!'; +$_['text_folder_rules_title'] = 'Індивідуальні правила стиснення для папок'; +$_['text_folder_rules_desc'] = 'Ви можете перевизначити якість та максимальні розміри для конкретних папок. Якщо залишити поле порожнім, використовуватиметься глобальне налаштування. Шлях вказується відносно папки image (наприклад, catalog/banners/).'; +$_['entry_folder_path'] = 'Шлях до папки'; +$_['entry_jpg_quality'] = 'Якість JPG'; +$_['entry_webp_quality'] = 'Якість WebP'; +$_['button_add_rule'] = 'Додати правило'; +$_['button_remove'] = 'Вилучити'; +$_['entry_wm_angle'] = 'Кут нахилу водяного знака (градуси)'; +$_['entry_wm_corner_radius'] = 'Закруглення кутів водяного знака (px)'; +$_['entry_wm_size_type'] = 'Масштабування водяного знака'; +$_['entry_wm_size_percent'] = 'Розмір водяного знака (% від ширини зображення)'; +$_['entry_wm_text_color'] = 'Колір тексту водяного знака'; +$_['entry_wm_text_font'] = 'Шрифт водяного знака (TTF)'; +$_['entry_wm_filter_mode'] = 'Режим фільтрації Категорій/Брендів'; +$_['text_wm_filter_include'] = 'Тільки для вибраних'; +$_['text_wm_filter_exclude'] = 'Для всіх, крім вибраних'; +$_['text_wm_size_original'] = 'Оригінальний розмір'; +$_['text_wm_size_percent'] = 'Пропорційно ширині зображення (%)'; +$_['button_preview_watermark'] = 'Попередній перегляд'; +$_['text_wm_preview_title'] = 'Попередній перегляд водяного знака'; +$_['text_cron_exclude_folders'] = 'Виключити папки зі сканування'; +$_['text_cron_entities'] = 'Які типи фото обробляти?'; +$_['text_cron_quality_override'] = 'Перевизначити параметри стиснення для Крона'; +$_['text_cron_recommendations'] = 'Рекомендації щодо розкладу Cron'; +$_['text_cron_recommendations_desc'] = '
    • Для невеликих сайтів (до 5000 товарів) рекомендується запускати Крон кожну годину (0 * * * *).
    • Для середніх сайтів (5000-20000 товарів) рекомендується запускати раз на 3-6 годин (0 */6 * * *).
    • Для великих сайтів (понад 20000 товарів) — один раз на добу вночі (наприклад, о 2:00: 0 2 * * *).
    '; +$_['text_tool_broken_files'] = 'Сканер битих/порожніх файлів на сервері'; +$_['help_tool_broken_files'] = 'Сканує фізичні файли в папці image/catalog/ на наявність пошкоджених зображень або порожніх файлів розміром 0 байт.'; +$_['text_grid_broken_file'] = 'Битий або порожній файл на сервері'; +$_['text_grid_broken_file_after'] = 'Вилучення або заміна заглушкою'; +$_['text_description_broken_files'] = 'Результати сканування фізичних файлів у папці image/catalog/. Тут відображаються биті (пошкоджені) зображення або файли розміром 0 байт.'; +$_['text_action_strip'] = 'Очистити EXIF'; +$_['text_action_set_placeholder'] = 'Встановити заглушку'; +$_['text_th_entity'] = 'Об`єкт / Пов`язані посилання'; +$_['text_cron_entity_product'] = 'Товари'; +$_['text_cron_entity_category'] = 'Категорії'; +$_['text_cron_entity_manufacturer'] = 'Виробники / Бренди'; +$_['text_cron_entity_banner'] = 'Банери'; +$_['text_cron_entity_blog'] = 'Блог / Статті'; +$_['entry_cron_jpg_quality'] = 'Якість JPG (Крон)'; +$_['entry_cron_webp_quality'] = 'Якість WebP (Крон)'; +$_['entry_cron_max_width'] = 'Макс. ширина px (Крон)'; +$_['entry_cron_max_height'] = 'Макс. висота px (Крон)'; +$_['text_cron_folder_scan'] = 'Папки для сканування'; +$_['text_no_fonts'] = 'TTF-шрифти не знайдено в system/library/font/'; +$_['button_apply'] = 'Застосувати'; +$_['text_backup_will_be_created'] = 'Резервна копія бази даних буде автоматично створена перед виконанням цієї дії.'; +$_['text_bulk_action'] = 'Групова дія для вибраних'; +$_['text_grid_results'] = 'Результати та деталі сканування'; +$_['text_quarantine_will_be_created'] = 'Оригінальні файли будуть переміщені в карантин (image/catalog_trash/) перед зміною.'; +$_['text_success_apply'] = 'Зміни успішно застосовані!'; +$_['text_description_broken'] = 'Результати сканування бази даних на наявність битих посилань (записи в таблицях з неіснуючими файлами).'; +$_['text_description_cache_restore'] = 'Результати пошуку файлів у кеші, оригінали яких відсутні в каталозі.'; +$_['text_description_duplicates'] = 'Результати пошуку повних дублікатів зображень за MD5-хешем.'; +$_['text_description_empty_folders'] = 'Список порожніх папок в директорії image/catalog/ для видалення.'; +$_['text_description_exif'] = 'Результати пошуку зображень з метаданими EXIF, які можна очистити.'; +$_['text_description_folder_tree'] = 'Статистика розміру папок у каталозі image/catalog/.'; +$_['text_description_html_broken'] = 'Результати пошуку неіснуючих файлів картинок, на які посилаються описи в HTML.'; +$_['text_description_placeholders'] = 'Результати пошуку сутностей без зображень для заповнення заглушками.'; +$_['text_description_png_jpg'] = 'Результати пошуку важких зображень PNG без прозорості, які можна перетиснути в JPG.'; +$_['text_description_small_photos'] = 'Список зображень, розміри яких менше мінімально заданих (%sx%s px).'; +$_['text_description_smart_cache'] = 'Результати пошуку файлів кешу, оригінали яких уже було видалено.'; +$_['text_description_translit'] = 'Результати пошуку файлів з кирилицею або пробілами в назвах.'; +$_['text_grid_broken_db_after'] = 'Очистити посилання або встановити заглушку'; +$_['text_grid_dupe_after'] = 'Оригінал (залишається)'; +$_['text_grid_dupe_before'] = 'Дублікат (буде видалено)'; +$_['text_grid_empty_folder'] = 'Порожня папка'; +$_['text_grid_empty_folder_after'] = 'Вилучити порожню папку'; +$_['text_grid_exif_after'] = 'Очистити метадані'; +$_['text_grid_exif_before'] = 'Містить EXIF'; +$_['text_grid_html_after'] = 'Виправити посилання вручну'; +$_['text_grid_missing_file'] = 'Файл відсутній'; +$_['text_grid_missing_html'] = 'Картинка не знайдена в описі'; +$_['text_grid_missing_orig'] = 'Оригінал відсутній'; +$_['text_grid_missing_orig_after'] = 'Відновити оригінал з кешу'; +$_['text_grid_no_image'] = 'Немає зображення (порожньо)'; +$_['text_grid_orph_cache'] = 'Осиротілий кеш'; +$_['text_grid_orph_cache_after'] = 'Видалити непотрібний кеш'; +$_['text_grid_ph_brand_after'] = 'Встановити заглушку бренду'; +$_['text_grid_ph_category_after'] = 'Встановити заглушку категорії'; +$_['text_grid_ph_product_after'] = 'Встановити заглушку товару'; +$_['text_grid_small_photo_after'] = 'Рекомендується замінити на більше'; +$_['text_grid_too_small'] = 'Занадто мале'; +$_['text_no_relations'] = 'Немає зв’язків у БД'; +$_['text_action_archive'] = 'Архівувати (в ZIP)'; +$_['text_action_archive_delete'] = 'Архівувати + Видалити'; +$_['text_tool_sanitizer'] = 'Санитайзер імен файлів'; +$_['help_tool_sanitizer'] = 'Масове очищення імен файлів і папок від кирилиці, пробілів та спецсимволів, приведення розширень до єдиного вигляду.'; +$_['text_description_sanitizer'] = 'Результати аналізу імен файлів для санітаризації за вибраними правилами.'; +$_['entry_sz_translit'] = 'Транслітерація (кирилиця в латиницю)'; +$_['entry_sz_spaces'] = 'Замінювати пробіли на символ "_"'; +$_['entry_sz_special'] = 'Видаляти спеціальні символи'; +$_['entry_sz_lowercase_name'] = 'Ім`я файлу в нижній регістр'; +$_['entry_sz_lowercase_ext'] = 'Розширення в нижній регістр'; +$_['entry_sz_normalize_ext'] = 'Нормалізувати розширення (jpeg -> jpg)'; +$_['entry_cron_folder_mode'] = 'Режим фільтрації папок'; +$_['text_cron_folder_mode_exclude'] = 'Сканувати все, крім вибраних (Винятки)'; +$_['text_cron_folder_mode_include'] = 'Сканирувати тільки вибрані папки'; +$_['button_select_all'] = 'Вибрати всі'; +$_['button_deselect_all'] = 'Зняти всі'; +$_['entry_wm_upload_font'] = 'Завантажити свій шрифт TTF'; +$_['button_upload_font'] = 'Завантажити шрифт'; +$_['text_success_font_upload'] = 'Шрифт успішно завантажено!'; +$_['error_font_upload'] = 'Помилка завантаження шрифту! Дозволені тільки файли .ttf вагою до 5 МБ.'; +$_['entry_lazy_load'] = 'Прогресивне лениве завантаження'; +$_['help_lazy_load'] = 'Вмикає преміальне прогресивне відкладене завантаження на вітрині. Зображення замінюються на мікро-прев\'ю та плавно розмиваються (blur-up), підвантажуючись у міру прокручування.'; +$_['confirm_replace_mode'] = 'УВАГА! Ви обрали режим «Замінювати оригінали». Усі зображення будуть перезаписані безпосередньо на сервері. Наполегливо рекомендуємо зробити резервну копію. Ви впевнені, що хочете продовжити?'; +$_['text_compare'] = 'Порівняти'; +$_['text_home'] = 'Головна'; +$_['text_tech_gd'] = 'Бібліотека GD'; +$_['text_tech_imagick'] = 'Imagick'; +$_['text_tech_webp'] = 'Підтримка WebP'; +$_['text_position_1'] = '1 - Зліва вгорі'; +$_['text_position_2'] = '2 - По центру вгорі'; +$_['text_position_3'] = '3 - Справа вгорі'; +$_['text_position_4'] = '4 - Зліва посередині'; +$_['text_position_5'] = '5 - По центру'; +$_['text_position_6'] = '6 - Справа посередині'; +$_['text_position_7'] = '7 - Зліва внизу'; +$_['text_position_8'] = '8 - По центру внизу'; +$_['text_position_9'] = '9 - Справа внизу'; + +$_['entry_log_level'] = 'Рівень логу'; +$_['text_log_changed_only'] = 'Тільки змінені'; +$_['text_log_all'] = 'Всі файли (детально)'; +$_['text_skipped_no_gain'] = 'Пропущено (без покращення)'; +$_['text_reset_cache_hint'] = 'Скинути кеш оптимізації (повне пересканування)'; +$_['confirm_reset_cache'] = 'Скинути кеш оптимізації? Наступне сканування перевірить ВСІ файли знову.'; +$_['text_cache_cleared'] = 'Кеш оптимізації очищено. Наступне сканування перевірить всі файли.'; +$_['error_wm_preview'] = 'Помилка генерації прев\'ю водяного знаку.'; +$_['error_wm_too_large'] = 'Зображення водяного знаку занадто велике (%dx%d px)! Максимально допустимий розмір: %dx%d px.'; +$_['text_apply_errors_warning'] = 'УВАГА: Деякі файли не вдалося замінити (заблоковані або немає прав):'; +$_['text_action_compress'] = 'Стиснути зображення'; +$_['text_estimated'] = 'Прогноз'; +$_['entry_min_savings_bytes'] = 'Мін. стиснення (байт)'; +$_['entry_min_savings_percent'] = 'Мін. стиснення (%)'; +$_['text_optimized'] = 'Оптимізовано'; +$_['entry_broken_fallback'] = 'Заглушка для битих фото'; +$_['help_broken_fallback'] = 'Автоматично замінює відсутні або пошкоджені зображення каталогу на вітрині на зображення-заглушку.'; +$_['entry_fallback_image'] = 'Зображення-заглушка'; +$_['help_fallback_image'] = 'Виберіть зображення, яке буде показано, якщо оригінальний файл картинки товару чи категорії відсутній. За замовчуванням використовується no_image.png.'; +$_['text_tool_cmyk'] = 'Конвертер CMYK в RGB'; +$_['help_tool_cmyk'] = 'Конвертація JPEG зображень з колірного профілю CMYK в sRGB для коректного відображення в браузерах Safari та на пристроях iOS.'; +$_['text_tool_heavy_files'] = 'Пошук важких файлів'; +$_['help_tool_heavy_files'] = 'Пошук найважчих оригінальних зображень у каталозі з можливістю їх швидкого стиснення безпосередньо тут.'; +$_['entry_heavy_files_limit'] = 'Макс. кількість знайдених файлів (всього)'; +$_['text_action_convert_rgb'] = 'Конвертувати в RGB'; +$_['text_description_cmyk'] = 'Знайдено файли в колірному просторі CMYK. Конвертація в sRGB гарантує колірне відображення на всіх платформах, включаючи iOS/macOS.'; +$_['text_description_heavy'] = 'Список із %d найважчих файлів у каталозі зображень. Ви можете оптимізувати їх безпосередньо тут.'; +$_['text_cron_folder_hint_exclude_all'] = 'Статус: Скануватимуться всі папки (винятки не обрані).'; +$_['text_cron_folder_hint_exclude_some'] = 'Статус: Скануватимуться всі папки, ОКРІМ %d позначених.'; +$_['text_cron_folder_hint_include_some'] = 'Статус: Скануватимуться тільки %d позначених папок.'; +$_['text_cron_folder_hint_include_none'] = 'УВАГА: Жодної папки не обрано! Крон нічого не буде сканувати.'; +$_['text_action_apply_changes'] = 'Застосувати вказані зміни'; +$_['entry_wm_status'] = 'Статус водяного знака'; +$_['text_disabled'] = 'Відключено'; +$_['entry_rule_name'] = 'Назва правила'; +$_['entry_rule_status'] = 'Статус правила'; +$_['entry_action'] = 'Дія'; +$_['entry_ph_status'] = 'Статус заглушок'; +$_['entry_sanitizer_limit'] = 'Макс. файлів для пошуку (ліміт)'; +$_['entry_heavy_files_min_size'] = 'Мінімальний розмір'; +$_['entry_png_jpg_limit'] = 'Макс. файлів для пошуку (ліміт)'; +$_['entry_png_jpg_min_size'] = 'Мінімальний розмір'; +$_['text_wm_rule_title'] = 'Правило водяного знака'; +$_['button_close'] = 'Закрити'; +$_['text_ph_rule_title'] = 'Правило заглушки'; +$_['entry_ph_image'] = 'Зображення заглушки'; +$_['button_clear_cache_wm'] = 'Очистити кеш (застосувати водяний знак)'; +$_['text_active'] = 'Активна'; +$_['text_error'] = 'Не активна'; +$_['text_th_dupe_delete'] = 'Файл для видалення (Дублікат)'; +$_['text_th_dupe_keep'] = 'Файл для збереження (Оригінал)'; +$_['text_dupe_original_label'] = 'Оригінал'; +$_['text_dupe_duplicate_label'] = 'Дублікат'; + +$_['text_disk_usage'] = 'Використання диска'; +$_['text_cumulative_stats'] = 'Статистика оптимізації'; +$_['text_disk_catalog'] = 'Каталог зображень'; +$_['text_disk_cache'] = 'Кеш OC'; +$_['text_disk_trash'] = 'Кошик модуля'; +$_['text_disk_other'] = 'Інше'; +$_['text_disk_free'] = 'Вільно'; +$_['text_total_files_opt'] = 'Файлів оброблено'; +$_['text_total_saved'] = 'Всього зекономлено'; +$_['text_avg_saving_pct'] = 'Середня економія'; +$_['text_stats_hint'] = 'Статистика накопичується по всіх сесіях (зберігається локально в браузері)'; +$_['text_reset_stats'] = 'Скинути статистику'; +$_['text_reset_stats_confirm']= 'Скинути накопичену статистику оптимізації?'; +$_['text_export_import'] = 'Експорт / Імпорт налаштувань'; +$_['button_export_settings'] = 'Експорт налаштувань (JSON)'; +$_['button_import_settings'] = 'Імпортувати налаштування'; +$_['help_export_settings'] = 'Завантажити всі налаштування модуля в JSON-файл'; +$_['help_import_settings'] = 'Завантажити раніше експортований JSON для відновлення налаштувань'; +$_['text_import_success'] = 'Налаштування успішно імпортовані. Перезавантажте сторінку для застосування.'; +$_['error_import_file'] = 'Файл не завантажено або помилка завантаження'; +$_['error_import_invalid'] = 'Некоректний файл налаштувань (невірний модуль або формат)'; +$_['text_click_to_load'] = 'Натисніть ↑ для завантаження даних диска'; +$_['tab_formats'] = 'Формати зображень'; + +$_['text_disk_quota_legend'] = 'Ліміти диска хостингу (вручну)'; +$_['text_disk_quota_desc'] = 'Якщо графік дискового простору показує некоректні дані (фізичний диск сервера замість ліміту вашого хостингу), ви можете вказати ліміти вашого тарифу вручну. Вкажіть 0, щоб використовувати автовизначення.'; +$_['entry_disk_quota_total'] = 'Виділено диска на хостингу (ГБ)'; +$_['entry_disk_quota_used'] = 'Зайнято диска на хостингу всього (ГБ)'; +$_['entry_auto_disk'] = 'Автооновлення при вході'; + +// Cron Tasks +$_['entry_cron_task_name'] = 'Назва задачі'; +$_['entry_cron_task_summary'] = 'Параметри стиснення'; +$_['button_add_cron_task'] = 'Додати задачу'; +$_['button_copy_cron_url'] = 'Копіювати URL'; +$_['text_cron_recommended_url'] = 'URL для запуску задачі крону:'; + +// Licensing +$_['text_license_required'] = 'Необхідний ліцензійний ключ'; +$_['text_license_required_desc'] = 'Будь ласка, введіть ліцензійний ключ для активації модуля. Усі функції налаштувань та оптимізації заблоковані до введення валідного ключа.'; +$_['entry_token_desc'] = 'Введіть токен активації, виданий для вашого основного домену. Ви можете отримати його в особистому кабінеті на форуме або звернувшись у підтримку.'; +$_['button_activate'] = 'Активувати модуль'; +$_['text_support'] = 'Технічна підтримка'; +$_['text_support_desc'] = 'Якщо у вас ще немає ключа або виникли проблеми, будь ласка, повідомте номер вашого замовлення та домен.'; + +// New settings +$_['entry_wm_max_width'] = 'Макс. ширина водяного знака (px)'; +$_['entry_wm_max_height'] = 'Макс. висота водяного знака (px)'; +$_['help_wm_max_width'] = 'Якщо вихідне зображення водяного знака перевищує цю ширину, воно буде автоматично масштабоване. Вкажіть 0 або залиште порожнім для значення за замовчуванням (800px).'; +$_['help_wm_max_height'] = 'Якщо вихідне зображення водяного знака перевищує цю висоту, воно буде автоматично масштабоване. Вкажіть 0 або залиште порожнім для значення за замовчуванням (800px).'; +$_['entry_disk_status'] = 'Увімкнути сканування диска та графік'; +$_['help_disk_status'] = 'Якщо увімкнено, модуль періодично скануватиме обсяг папки картинок та відображатиме круговую діаграму. Рекомендується вимкнути на великих сайтах для прискорення завантаження панелі.'; +$_['text_disk_settings_title'] = 'Налаштування лімітів та сканування диска'; diff --git a/upload/admin/model/module/img_opti.php b/upload/admin/model/module/img_opti.php new file mode 100644 index 0000000..8f64f7f --- /dev/null +++ b/upload/admin/model/module/img_opti.php @@ -0,0 +1,3644 @@ +registerModuleFolder('catalog_trash/backup'); + return $file_name; + } + fwrite($fp, "-- Database Backup for Tool: " . $tool_name . "\n"); + fwrite($fp, "-- Date: " . date('Y-m-d H:i:s') . "\n\n"); + foreach ($tables as $table) { + fwrite($fp, "DROP TABLE IF EXISTS `" . $table . "`;\n"); + $query = $this->db->query("SHOW CREATE TABLE `" . $table . "`"); + if ($query->num_rows) { + fwrite($fp, $query->row['Create Table'] . ";\n\n"); + } + $chunk_size = 200; + $offset = 0; + do { + $query_rows = $this->db->query("SELECT * FROM `" . $table . "` LIMIT " . (int)$chunk_size . " OFFSET " . (int)$offset); + foreach ($query_rows->rows as $row) { + $fields = array(); + $values = array(); + foreach ($row as $key => $value) { + $fields[] = "`" . $key . "`"; + if ($value === null) { + $values[] = "NULL"; + } else { + $values[] = "'" . $this->db->escape($value) . "'"; + } + } + fwrite($fp, "INSERT INTO `" . $table . "` (" . implode(', ', $fields) . ") VALUES (" . implode(', ', $values) . ");\n"); + } + $offset += $chunk_size; + } while ($query_rows->num_rows === $chunk_size); + fwrite($fp, "\n"); + } + fclose($fp); + $this->registerModuleFolder('catalog_trash/backup'); + return $file_name; + } + + public function quarantineFile($relative_path, $tool_name) { + $clean_rel = str_replace(array('../', '..\\', "\0"), '', (string)$relative_path); + $src = DIR_IMAGE . $clean_rel; + if (!is_file($src)) return false; + $dest = DIR_IMAGE . 'catalog_trash/' . $tool_name . '/' . ltrim($clean_rel, '/'); + $dest_dir = $this->mb_dirname($dest); + if (!is_dir($dest_dir)) { + @mkdir($dest_dir, 0755, true); + } + $this->registerModuleFolder('catalog_trash/' . $tool_name); + if (file_exists($dest)) { + @unlink($dest); + } + if (!@rename($src, $dest)) { + if (@copy($src, $dest)) { + @unlink($src); + return true; + } + return false; + } + return true; + } + + public function registerModuleFolder($folder_name) { + $folder_name = preg_replace('/[^a-zA-Z0-9_\-\/]/', '', (string)$folder_name); + if (empty($folder_name)) return; + $query = $this->db->query("SELECT `value` FROM `" . DB_PREFIX . "setting` WHERE `code` = 'module_img_opti' AND `key` = 'module_img_opti_folders'"); + $folders = ($query->num_rows && $query->row['value']) ? json_decode($query->row['value'], true) : array(); + if (!in_array($folder_name, $folders)) { + $folders[] = $folder_name; + $this->db->query("DELETE FROM `" . DB_PREFIX . "setting` WHERE `code` = 'module_img_opti' AND `key` = 'module_img_opti_folders'"); + $this->db->query("INSERT INTO `" . DB_PREFIX . "setting` SET `store_id` = '0', `code` = 'module_img_opti', `key` = 'module_img_opti_folders', `value` = '" . $this->db->escape(json_encode($folders)) . "'"); + } + } + + private function getSearchLinks($name, $model = '') { + $query_parts = array(); + if ($name) $query_parts[] = trim($name); + if ($model) $query_parts[] = trim($model); + $search_query = implode(' ', $query_parts); + if (!$search_query) return ''; + + $google_url = 'https://www.google.com/search?tbm=isch&q=' . urlencode($search_query); + $yandex_url = 'https://yandex.ru/images/search?text=' . urlencode($search_query); + + $html = ' '; + $html .= ' '; + + return $html; + } + + private function getEntitiesByImagePath($image_path) { + $entities = array(); + $escaped = $this->db->escape((string)$image_path); + $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + $query = $this->db->query("SELECT p.product_id, p.model, p.status, p.date_available, (SELECT p2s.store_id FROM `" . DB_PREFIX . "product_to_store` p2s WHERE p2s.product_id = p.product_id AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id, (SELECT name FROM `" . DB_PREFIX . "product_description` pd WHERE pd.product_id = p.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "product` p WHERE p.image = '" . $escaped . "'"); + foreach ($query->rows as $row) { + $admin_link = $this->url->link('catalog/product/edit', 'user_token=' . $this->session->data['user_token'] . '&product_id=' . (int)$row['product_id'], true); + $catalog_link_html = ''; + if (isset($row['status']) && (int)$row['status'] === 1 && (empty($row['date_available']) || strtotime($row['date_available']) <= time()) && (isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/product&product_id=' . (int)$row['product_id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name'], $row['model']); + $entities[] = 'Product ' . htmlspecialchars($row['name'] ? $row['name'] : 'Product ID ' . $row['product_id'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links; + } + $query = $this->db->query("SELECT pi.product_id, p.model, p.status, p.date_available, (SELECT p2s.store_id FROM `" . DB_PREFIX . "product_to_store` p2s WHERE p2s.product_id = pi.product_id AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id, (SELECT name FROM `" . DB_PREFIX . "product_description` pd WHERE pd.product_id = pi.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "product_image` pi LEFT JOIN `" . DB_PREFIX . "product` p ON (pi.product_id = p.product_id) WHERE pi.image = '" . $escaped . "'"); + foreach ($query->rows as $row) { + $admin_link = $this->url->link('catalog/product/edit', 'user_token=' . $this->session->data['user_token'] . '&product_id=' . (int)$row['product_id'], true); + $catalog_link_html = ''; + if (isset($row['status']) && (int)$row['status'] === 1 && (empty($row['date_available']) || strtotime($row['date_available']) <= time()) && (isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/product&product_id=' . (int)$row['product_id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name'], $row['model']); + $entities[] = 'Product (Add.) ' . htmlspecialchars($row['name'] ? $row['name'] : 'Product ID ' . $row['product_id'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links; + } + $query = $this->db->query("SELECT c.category_id, c.status, (SELECT c2s.store_id FROM `" . DB_PREFIX . "category_to_store` c2s WHERE c2s.category_id = c.category_id AND c2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id, (SELECT name FROM `" . DB_PREFIX . "category_description` cd WHERE cd.category_id = c.category_id AND cd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "category` c WHERE image = '" . $escaped . "'"); + foreach ($query->rows as $row) { + $admin_link = $this->url->link('catalog/category/edit', 'user_token=' . $this->session->data['user_token'] . '&category_id=' . (int)$row['category_id'], true); + $catalog_link_html = ''; + if (isset($row['status']) && (int)$row['status'] === 1 && (isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/category&path=' . (int)$row['category_id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name']); + $entities[] = 'Category ' . htmlspecialchars($row['name'] ? $row['name'] : 'Category ID ' . $row['category_id'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links; + } + $query = $this->db->query("SELECT m.manufacturer_id, m.name, (SELECT m2s.store_id FROM `" . DB_PREFIX . "manufacturer_to_store` m2s WHERE m2s.manufacturer_id = m.manufacturer_id AND m2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id FROM `" . DB_PREFIX . "manufacturer` m WHERE image = '" . $escaped . "'"); + foreach ($query->rows as $row) { + $admin_link = $this->url->link('catalog/manufacturer/edit', 'user_token=' . $this->session->data['user_token'] . '&manufacturer_id=' . (int)$row['manufacturer_id'], true); + $catalog_link_html = ''; + if ((isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/manufacturer/info&manufacturer_id=' . (int)$row['manufacturer_id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name']); + $entities[] = 'Manufacturer ' . htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links; + } + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $query_banner = $this->db->query("SELECT bi.banner_id, b.name FROM `" . DB_PREFIX . "banner_image` bi LEFT JOIN `" . DB_PREFIX . "banner` b ON (bi.banner_id = b.banner_id) WHERE bi.image = '" . $escaped . "'"); + foreach ($query_banner->rows as $row) { + $admin_link = $this->url->link('design/banner/edit', 'user_token=' . $this->session->data['user_token'] . '&banner_id=' . (int)$row['banner_id'], true); + $search_links = $this->getSearchLinks($row['name']); + $entities[] = 'Banner ' . htmlspecialchars($row['name'] ? $row['name'] : 'Banner ID ' . $row['banner_id'], ENT_QUOTES, 'UTF-8') . '' . $search_links; + } + } + $blog_tables = array( + 'oct_blog_article' => array('id' => 'blog_article_id', 'route' => 'extension/news', 'name' => 'title', 'type' => 'OCT Blog'), + 'simple_blog_article' => array('id' => 'simple_blog_article_id', 'route' => 'simple_blog/article/edit', 'name' => 'article_title', 'type' => 'Simple Blog'), + 'newsblog_article' => array('id' => 'article_id', 'route' => 'extension/newsblog/article/edit', 'name' => 'name', 'type' => 'NewsBlog'), + 'information' => array('id' => 'information_id', 'route' => 'catalog/information/edit', 'name' => 'title', 'type' => 'Information') + ); + foreach ($blog_tables as $table => $cfg) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE 'image'"); + if ($column_query->num_rows) { + $desc_table = $table . '_description'; + $desc_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $desc_table . "'"); + if ($desc_query->num_rows) { + $sql = "SELECT t.`" . $cfg['id'] . "`, td.`" . $cfg['name'] . "` as name FROM `" . DB_PREFIX . $table . "` t LEFT JOIN `" . DB_PREFIX . $desc_table . "` td ON (t.`" . $cfg['id'] . "` = td.`" . $cfg['id'] . "`) WHERE t.image = '" . $escaped . "' AND td.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 5"; + } else { + $sql = "SELECT `" . $cfg['id'] . "`, `" . $cfg['name'] . "` as name FROM `" . DB_PREFIX . $table . "` WHERE image = '" . $escaped . "' LIMIT 5"; + } + try { + $query_blog = $this->db->query($sql); + foreach ($query_blog->rows as $row) { + $admin_link = $this->url->link($cfg['route'], 'user_token=' . $this->session->data['user_token'] . '&' . $cfg['id'] . '=' . (int)$row[$cfg['id']], true); + $search_links = $this->getSearchLinks($row['name']); + $entities[] = '' . $cfg['type'] . ' ' . htmlspecialchars($row['name'] ? $row['name'] : $cfg['type'] . ' ID ' . $row[$cfg['id']], ENT_QUOTES, 'UTF-8') . '' . $search_links; + } + } catch (Exception $e) {} + } + } + } + return $entities; + } + + public function scan_broken_db($data = array()) { + $this->loadLanguageSafe(); + $rows = array(); + $realImageDir = rtrim(str_replace('\\', '/', realpath(DIR_IMAGE)), '/'); + $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + + $query = $this->db->query("SELECT p.product_id as id, p.model, p.image, p.status, p.date_available, (SELECT p2s.store_id FROM `" . DB_PREFIX . "product_to_store` p2s WHERE p2s.product_id = p.product_id AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id, (SELECT name FROM `" . DB_PREFIX . "product_description` pd WHERE pd.product_id = p.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "product` p WHERE image != '' AND image IS NOT NULL"); + foreach ($query->rows as $row) { + $image_path = str_replace(array('../', '..\\', "\0"), '', (string)$row['image']); + $fullPath = DIR_IMAGE . $image_path; + $realPath = realpath($fullPath); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), $realImageDir) !== 0 || !is_file($realPath)) { + $admin_link = $this->url->link('catalog/product/edit', 'user_token=' . $this->session->data['user_token'] . '&product_id=' . (int)$row['id'], true); + $catalog_link_html = ''; + if (isset($row['status']) && (int)$row['status'] === 1 && (empty($row['date_available']) || strtotime($row['date_available']) <= time()) && (isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/product&product_id=' . (int)$row['id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name'], $row['model']); + $rows[] = array( + 'id' => 'product_main-' . (int)$row['id'], + 'preview' => '', + 'name' => 'Product ' . htmlspecialchars($row['name'] ? $row['name'] : 'Product ID ' . (int)$row['id'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links, + 'before' => '' . $this->language->get('text_grid_missing_file') . ': ' . htmlspecialchars($image_path, ENT_QUOTES, 'UTF-8'), + 'after' => $this->language->get('text_grid_broken_db_after'), + 'actions' => array( + 'clear' => $this->language->get('text_action_clear'), + 'disable' => $this->language->get('text_action_disable'), + 'stock' => $this->language->get('text_action_stock'), + 'placeholder' => $this->language->get('text_action_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'clear' + ); + } + } + + $query_add = $this->db->query("SELECT pi.product_image_id as id, pi.product_id, pi.image, p.model, p.status, p.date_available, (SELECT p2s.store_id FROM `" . DB_PREFIX . "product_to_store` p2s WHERE p2s.product_id = pi.product_id AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id, (SELECT name FROM `" . DB_PREFIX . "product_description` pd WHERE pd.product_id = pi.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "product_image` pi LEFT JOIN `" . DB_PREFIX . "product` p ON (pi.product_id = p.product_id) WHERE pi.image != '' AND pi.image IS NOT NULL"); + foreach ($query_add->rows as $row) { + $image_path = str_replace(array('../', '..\\', "\0"), '', (string)$row['image']); + $fullPath = DIR_IMAGE . $image_path; + $realPath = realpath($fullPath); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), $realImageDir) !== 0 || !is_file($realPath)) { + $admin_link = $this->url->link('catalog/product/edit', 'user_token=' . $this->session->data['user_token'] . '&product_id=' . (int)$row['product_id'], true); + $catalog_link_html = ''; + if (isset($row['status']) && (int)$row['status'] === 1 && (empty($row['date_available']) || strtotime($row['date_available']) <= time()) && (isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/product&product_id=' . (int)$row['product_id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name'], $row['model']); + $rows[] = array( + 'id' => 'product_additional-' . (int)$row['id'], + 'preview' => '', + 'name' => 'Product (Add.) ' . htmlspecialchars($row['name'] ? $row['name'] : 'Product ID ' . (int)$row['product_id'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links, + 'before' => '' . $this->language->get('text_grid_missing_file') . ': ' . htmlspecialchars($image_path, ENT_QUOTES, 'UTF-8'), + 'after' => $this->language->get('text_grid_broken_db_after'), + 'actions' => array( + 'clear' => $this->language->get('text_action_clear'), + 'placeholder' => $this->language->get('text_action_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'clear' + ); + } + } + + $query_cat = $this->db->query("SELECT c.category_id as id, c.image, c.status, (SELECT c2s.store_id FROM `" . DB_PREFIX . "category_to_store` c2s WHERE c2s.category_id = c.category_id AND c2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id, (SELECT name FROM `" . DB_PREFIX . "category_description` cd WHERE cd.category_id = c.category_id AND cd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "category` c WHERE image != '' AND image IS NOT NULL"); + foreach ($query_cat->rows as $row) { + $image_path = str_replace(array('../', '..\\', "\0"), '', (string)$row['image']); + $fullPath = DIR_IMAGE . $image_path; + $realPath = realpath($fullPath); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), $realImageDir) !== 0 || !is_file($realPath)) { + $admin_link = $this->url->link('catalog/category/edit', 'user_token=' . $this->session->data['user_token'] . '&category_id=' . (int)$row['id'], true); + $catalog_link_html = ''; + if (isset($row['status']) && (int)$row['status'] === 1 && (isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/category&path=' . (int)$row['id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name']); + $rows[] = array( + 'id' => 'category-' . (int)$row['id'], + 'preview' => '', + 'name' => 'Category ' . htmlspecialchars($row['name'] ? $row['name'] : 'Category ID ' . (int)$row['id'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links, + 'before' => '' . $this->language->get('text_grid_missing_file') . ': ' . htmlspecialchars($image_path, ENT_QUOTES, 'UTF-8'), + 'after' => $this->language->get('text_grid_broken_db_after'), + 'actions' => array( + 'clear' => $this->language->get('text_action_clear'), + 'disable' => $this->language->get('text_action_disable'), + 'placeholder' => $this->language->get('text_action_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'clear' + ); + } + } + + $query_man = $this->db->query("SELECT m.manufacturer_id as id, m.name, m.image, (SELECT m2s.store_id FROM `" . DB_PREFIX . "manufacturer_to_store` m2s WHERE m2s.manufacturer_id = m.manufacturer_id AND m2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id FROM `" . DB_PREFIX . "manufacturer` m WHERE image != '' AND image IS NOT NULL"); + foreach ($query_man->rows as $row) { + $image_path = str_replace(array('../', '..\\', "\0"), '', (string)$row['image']); + $fullPath = DIR_IMAGE . $image_path; + $realPath = realpath($fullPath); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), $realImageDir) !== 0 || !is_file($realPath)) { + $admin_link = $this->url->link('catalog/manufacturer/edit', 'user_token=' . $this->session->data['user_token'] . '&manufacturer_id=' . (int)$row['id'], true); + $catalog_link_html = ''; + if ((isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/manufacturer/info&manufacturer_id=' . (int)$row['id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name']); + $rows[] = array( + 'id' => 'manufacturer-' . (int)$row['id'], + 'preview' => '', + 'name' => 'Manufacturer ' . htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links, + 'before' => '' . $this->language->get('text_grid_missing_file') . ': ' . htmlspecialchars($image_path, ENT_QUOTES, 'UTF-8'), + 'after' => $this->language->get('text_grid_broken_db_after'), + 'actions' => array( + 'clear' => $this->language->get('text_action_clear'), + 'placeholder' => $this->language->get('text_action_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'clear' + ); + } + } + + file_put_contents(DIR_CACHE . 'img_opti_broken_db_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => false, + 'description' => $this->language->get('text_description_broken') + ); + } + + public function fix_broken_db($data) { + $this->backupCatalogTables('broken'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + if ($fix_all === 1 || empty($selected)) { + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_broken_db_grid.json'), true); + foreach ($grid as $row) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $ph_product = (string)$this->config->get('module_img_opti_ph_product'); + $ph_category = (string)$this->config->get('module_img_opti_ph_category'); + $ph_brand = (string)$this->config->get('module_img_opti_ph_brand'); + + foreach ($selected as $row_id => $act) { + if ($act === 'ignore') continue; + $parts = explode('-', (string)$row_id, 2); + if (count($parts) !== 2) continue; + $type = preg_replace('/[^a-zA-Z_]/', '', $parts[0]); + $id = (int)$parts[1]; + + if ($act === 'clear') { + if ($type === 'product_main') { + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '' WHERE product_id = '" . $id . "'"); + } elseif ($type === 'product_additional') { + $this->db->query("DELETE FROM `" . DB_PREFIX . "product_image` WHERE product_image_id = '" . $id . "'"); + } elseif ($type === 'category') { + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '' WHERE category_id = '" . $id . "'"); + } elseif ($type === 'manufacturer') { + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '' WHERE manufacturer_id = '" . $id . "'"); + } + } elseif ($act === 'disable') { + if ($type === 'product_main') { + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET status = '0' WHERE product_id = '" . $id . "'"); + } elseif ($type === 'category') { + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET status = '0' WHERE category_id = '" . $id . "'"); + } + } elseif ($act === 'stock') { + if ($type === 'product_main') { + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET quantity = '0' WHERE product_id = '" . $id . "'"); + } + } elseif ($act === 'placeholder') { + if ($type === 'product_main') { + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $this->db->escape($ph_product) . "' WHERE product_id = '" . $id . "'"); + } elseif ($type === 'product_additional') { + $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $this->db->escape($ph_product) . "' WHERE product_image_id = '" . $id . "'"); + } elseif ($type === 'category') { + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $this->db->escape($ph_category) . "' WHERE category_id = '" . $id . "'"); + } elseif ($type === 'manufacturer') { + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $this->db->escape($ph_brand) . "' WHERE manufacturer_id = '" . $id . "'"); + } + } + } + @unlink(DIR_CACHE . 'img_opti_broken_db_grid.json'); + return array('success' => true); + } + + public function scan_broken_files($data = array()) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $ext = strtolower($file->getExtension()); + if (in_array($ext, array('jpg', 'jpeg', 'png', 'webp', 'gif'))) { + $realPath = realpath($file->getPathname()); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) !== 0) continue; + + $size = filesize($realPath); + $is_broken = false; + + if ($size === 0) { + $is_broken = true; + } elseif ($size > 52428800) { + // Файл > 50MB — пропускаем GD-проверку во избежание OOM + $is_broken = false; + } else { + if (extension_loaded('imagick')) { + try { + $img = new Imagick($realPath); + $img->clear(); $img->destroy(); + } catch (Exception $e) { + $is_broken = true; + } + } else { + $img = @imagecreatefromstring(@file_get_contents($realPath, false, null, 0, 52428800)); + if (!$img) { + $is_broken = true; + } else { + imagedestroy($img); + } + } + } + + if ($is_broken) { + $relPath = $this->getRelativeImagePath($realPath); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $rows[] = array( + 'id' => 'broken_file-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => $this->language->get('text_grid_broken_file') . ' (' . $this->formatBytes($size) . ')', + 'after' => $this->language->get('text_grid_broken_file_after'), + 'actions' => array( + 'delete' => $this->language->get('text_action_delete'), + 'placeholder' => $this->language->get('text_action_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'delete', + 'meta' => array( + 'path' => $relPath + ) + ); + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_broken_files_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => false, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_broken_files') + ); + } + + public function fix_broken_files($data) { + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_broken_files_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $ph_product_path = DIR_IMAGE . (string)$this->config->get('module_img_opti_ph_product'); + if (!is_file($ph_product_path)) { + $ph_product_path = DIR_IMAGE . 'no_image.png'; + } + + foreach ($selected as $row_id => $act) { + if ($act === 'ignore' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = $row['meta']['path']; + $fullPath = DIR_IMAGE . $relPath; + + if ($act === 'delete') { + $this->quarantineFile($relPath, 'broken_files'); + } elseif ($act === 'placeholder') { + $this->quarantineFile($relPath, 'broken_files'); + if (is_file($ph_product_path)) { + @copy($ph_product_path, $fullPath); + } + } + } + @unlink(DIR_CACHE . 'img_opti_broken_files_grid.json'); + return array('success' => true); + } + + public function scan_duplicates($data) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $hashes = array(); + foreach ($iterator as $file) { + if ($file->isFile()) { + $ext = strtolower($file->getExtension()); + if (in_array($ext, array('jpg', 'jpeg', 'png', 'webp', 'gif', 'svg'))) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $hash = md5_file($realPath); + $relPath = $this->getRelativeImagePath($realPath); + if (!isset($hashes[$hash])) { + $hashes[$hash] = array(); + } + $hashes[$hash][] = $relPath; + } + } + } + } + + $this->load->model('tool/image'); + + foreach ($hashes as $hash => $files) { + if (count($files) > 1) { + $original = array_shift($files); + $original_size = filesize(DIR_IMAGE . $original); + + $orig_ext = strtolower($this->mb_pathinfo($original, PATHINFO_EXTENSION)); + if ($orig_ext === 'svg') { + $thumb_orig = '../image/' . $original; + } else { + $thumb_orig = $this->model_tool_image->resize($original, 100, 100); + } + + foreach ($files as $duplicate) { + $dupe_ext = strtolower($this->mb_pathinfo($duplicate, PATHINFO_EXTENSION)); + if ($dupe_ext === 'svg') { + $thumb_dupe = '../image/' . $duplicate; + } else { + $thumb_dupe = $this->model_tool_image->resize($duplicate, 100, 100); + } + + $entities = $this->getEntitiesByImagePath($duplicate); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $preview_html = '
    '; + $preview_html .= '
    '; + $preview_html .= '
    '; + $preview_html .= ' ' . $this->language->get('text_dupe_original_label') . ''; + $preview_html .= '
    '; + $preview_html .= '
    '; + $preview_html .= '
    '; + $preview_html .= ' ' . $this->language->get('text_dupe_duplicate_label') . ''; + $preview_html .= '
    '; + $preview_html .= '
    '; + + $name_html = 'Duplicate path:
    ' . htmlspecialchars($duplicate, ENT_QUOTES, 'UTF-8') . '
    '; + $name_html .= 'Relations:
    ' . $entities_html; + + $rows[] = array( + 'id' => 'duplicate-' . md5($duplicate), + 'preview' => $preview_html, + 'name' => $name_html, + 'before' => '' . $this->language->get('text_grid_dupe_before') . '
    ' . htmlspecialchars($duplicate, ENT_QUOTES, 'UTF-8') . '
    ' . $this->formatBytes($original_size) . '', + 'after' => ' ' . $this->language->get('text_grid_dupe_after') . '
    ' . htmlspecialchars($original, ENT_QUOTES, 'UTF-8') . '
    ' . $this->formatBytes($original_size) . '', + 'actions' => array( + 'merge' => $this->language->get('text_action_merge'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'merge', + 'meta' => array( + 'original' => $original, + 'duplicate' => $duplicate + ) + ); + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_duplicates_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_dupe_delete'), + $this->language->get('text_th_dupe_keep'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_duplicates') + ); + } + + public function fix_duplicates($data) { + $this->backupCatalogTables('duplicates'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_duplicates_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + foreach ($selected as $row_id => $act) { + if ($act !== 'merge' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $orig = $this->db->escape($row['meta']['original']); + $fake = $this->db->escape($row['meta']['duplicate']); + + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $orig . "' WHERE image = '" . $fake . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $orig . "' WHERE image = '" . $fake . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $orig . "' WHERE image = '" . $fake . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $orig . "' WHERE image = '" . $fake . "'"); + + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $this->db->query("UPDATE `" . DB_PREFIX . "banner_image` SET image = '" . $orig . "' WHERE image = '" . $fake . "'"); + } + + $blog_tables = array( + 'oct_blog_article' => 'image', + 'simple_blog_article' => 'image', + 'newsblog_article' => 'image', + 'information' => 'image' + ); + foreach ($blog_tables as $table => $column) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE '" . $column . "'"); + if ($column_query->num_rows) { + try { + $this->db->query("UPDATE `" . DB_PREFIX . $table . "` SET `" . $column . "` = '" . $orig . "' WHERE `" . $column . "` = '" . $fake . "'"); + } catch (Exception $e) {} + } + } + } + + $this->quarantineFile($row['meta']['duplicate'], 'duplicates'); + } + @unlink(DIR_CACHE . 'img_opti_duplicates_grid.json'); + return array('success' => true); + } + + public function scan_png_jpg($data) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $limit = isset($data['module_img_opti_png_jpg_limit']) ? (int)$data['module_img_opti_png_jpg_limit'] : 100; + if ($limit <= 0) $limit = 100; + + $min_size = isset($data['module_img_opti_png_jpg_min_size']) ? (int)$data['module_img_opti_png_jpg_min_size'] : 500; + if ($min_size < 0) $min_size = 500; + $min_size_bytes = $min_size * 1024; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile() && strtolower($file->getExtension()) === 'png') { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $size = filesize($realPath); + if ($size < $min_size_bytes) continue; + + $w = 0; + $h = 0; + $imgSize = @getimagesize($realPath); + if ($imgSize) { + $w = $imgSize[0]; + $h = $imgSize[1]; + } + $is_alpha = false; + if (extension_loaded('imagick')) { + try { + $img = new Imagick($realPath); + if ($img->getImageAlphaChannel()) { + $is_alpha = true; + } + $img->clear(); $img->destroy(); + } catch (Exception $e) {} + } else { + $img = @imagecreatefrompng($realPath); + if ($img) { + $w_gd = imagesx($img); + $h_gd = imagesy($img); + for($x = 0; $x < $w_gd; $x++) { + for($y = 0; $y < $h_gd; $y++) { + $rgba = imagecolorat($img, $x, $y); + $alpha = ($rgba & 0x7F000000) >> 24; + if($alpha > 0) { + $is_alpha = true; + break 2; + } + } + } + imagedestroy($img); + } + } + + if (!$is_alpha) { + $relPath = $this->getRelativeImagePath($realPath); + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + $rows[] = array( + 'id' => 'png_jpg-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => 'PNG: ' . $this->formatBytes($size), + 'after' => 'JPG: ~' . $this->formatBytes(round($size * 0.3)), + 'actions' => array( + 'convert' => $this->language->get('text_action_convert'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'convert', + 'meta' => array( + 'path' => $relPath, + 'width' => $w, + 'height' => $h + ) + ); + if (count($rows) >= $limit) { + break; + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_png_jpg_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_png_jpg') + ); + } + + public function fix_png_jpg($data) { + $this->backupCatalogTables('png_jpg'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_png_jpg_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + + foreach ($selected as $row_id => $act) { + if ($act !== 'convert' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $pngClean = str_replace(array('../', '..\\', "\0"), '', (string)$row['meta']['path']); + $fullPath = realpath(DIR_IMAGE . $pngClean); + if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) continue; + + $jpgPathStr = preg_replace('/\.png$/i', '.jpg', DIR_IMAGE . $pngClean); + $jpgDir = realpath($this->mb_dirname($jpgPathStr)); + if (!$jpgDir || strpos(str_replace('\\', '/', $jpgDir), str_replace('\\', '/', $realImageDir)) !== 0) continue; + $relJpg = ltrim(str_replace(str_replace('\\', '/', $realImageDir), '', str_replace('\\', '/', $jpgPathStr)), '/'); + + $img = @imagecreatefrompng($fullPath); + if ($img) { + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + $bg = imagecreatetruecolor(imagesx($img), imagesy($img)); + imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255)); + imagecopy($bg, $img, 0, 0, 0, 0, imagesx($img), imagesy($img)); + if (imagejpeg($bg, $jpgPathStr, 85)) { + $dbOld = $this->db->escape($pngClean); + $dbNew = $this->db->escape($relJpg); + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->quarantineFile($pngClean, 'png_jpg'); + } + imagedestroy($img); + imagedestroy($bg); + } + } + @unlink(DIR_CACHE . 'img_opti_png_jpg_grid.json'); + return array('success' => true); + } + + public function scan_translit($data) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $limit = isset($data['module_img_opti_translit_limit']) ? (int)$data['module_img_opti_translit_limit'] : 100; + if ($limit <= 0) $limit = 100; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + $assigned_paths = array(); + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $relPath = $this->getRelativeImagePath($realPath); + $filename = $this->mb_basename($relPath); + if (preg_match('/[^a-zA-Z0-9_\\-\\.]/', $filename)) { + $dirPart = $this->mb_dirname($relPath); + $ext = $this->mb_pathinfo($filename, PATHINFO_EXTENSION); + $nameOnly = $this->mb_pathinfo($filename, PATHINFO_FILENAME); + $cleanName = $this->transliterate($nameOnly) . '.' . $ext; + $proposedRel = ($dirPart === '.' ? '' : $dirPart . '/') . $cleanName; + + if ($relPath !== $proposedRel) { + $proposedFullPath = DIR_IMAGE . $proposedRel; + if (file_exists($proposedFullPath) || isset($assigned_paths[$proposedRel]) || $this->isPathUsedInDb($proposedRel)) { + $transName = $this->transliterate($nameOnly); + $counter = 1; + while (true) { + $candidate = ($dirPart === '.' ? '' : $dirPart . '/') . $transName . '_' . $counter . ($ext !== '' ? '.' . $ext : ''); + if (!file_exists(DIR_IMAGE . $candidate) && !isset($assigned_paths[$candidate]) && !$this->isPathUsedInDb($candidate)) { + $proposedRel = $candidate; + break; + } + $counter++; + } + } + + $assigned_paths[$proposedRel] = true; + + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $rows[] = array( + 'id' => 'translit-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => $filename, + 'after' => $this->mb_basename($proposedRel), + 'actions' => array( + 'rename' => $this->language->get('text_action_rename'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'rename', + 'meta' => array( + 'path' => $relPath, + 'new_path' => $proposedRel + ) + ); + + if (count($rows) >= $limit) { + break; + } + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_translit_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_translit') + ); + } + + public function fix_translit($data) { + $this->backupCatalogTables('translit'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_translit_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + + foreach ($selected as $row_id => $act) { + if ($act !== 'rename' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $cleanBadFile = str_replace(array('../', '..\\', "\0"), '', (string)$row['meta']['path']); + $fullPath = realpath(DIR_IMAGE . $cleanBadFile); + if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) continue; + + $newRel = (string)$row['meta']['new_path']; + $newFullPathStr = DIR_IMAGE . $newRel; + $newDirStr = $this->mb_dirname($newFullPathStr); + if (!is_dir($newDirStr)) { + @mkdir($newDirStr, 0755, true); + } + $realNewDir = realpath($newDirStr); + if (!$realNewDir || strpos(str_replace('\\', '/', $realNewDir), str_replace('\\', '/', $realImageDir)) !== 0) continue; + $newFullPath = $realNewDir . DIRECTORY_SEPARATOR . $this->mb_basename($newFullPathStr); + + if (@copy($fullPath, $newFullPath)) { + $dbOld = $this->db->escape($cleanBadFile); + $dbNew = $this->db->escape($newRel); + + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $this->db->query("UPDATE `" . DB_PREFIX . "banner_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + } + + $blog_tables = array( + 'oct_blog_article' => 'image', + 'simple_blog_article' => 'image', + 'newsblog_article' => 'image', + 'information' => 'image' + ); + foreach ($blog_tables as $table => $column) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE '" . $column . "'"); + if ($column_query->num_rows) { + try { + $this->db->query("UPDATE `" . DB_PREFIX . $table . "` SET `" . $column . "` = '" . $dbNew . "' WHERE `" . $column . "` = '" . $dbOld . "'"); + } catch (Exception $e) {} + } + } + } + + $this->quarantineFile($cleanBadFile, 'translit'); + } + } + @unlink(DIR_CACHE . 'img_opti_translit_grid.json'); + return array('success' => true); + } + + public function scan_empty_folders($data) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $path) { + if ($path->isDir()) { + $realPath = realpath($path->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $files = array_diff(scandir($realPath), array('.', '..')); + if (count($files) === 0) { + $relPath = str_replace(str_replace('\\', '/', DIR_IMAGE), '', str_replace('\\', '/', $realPath)); + $rows[] = array( + 'id' => 'empty_folder-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '', + 'before' => $this->language->get('text_grid_empty_folder'), + 'after' => $this->language->get('text_grid_empty_folder_after'), + 'actions' => array( + 'delete' => $this->language->get('text_action_delete'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'delete', + 'meta' => array( + 'path' => $relPath + ) + ); + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_empty_folders_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => false, + 'quarantine_available' => false, + 'description' => $this->language->get('text_description_empty_folders') + ); + } + + public function fix_empty_folders($data) { + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_empty_folders_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + + foreach ($selected as $row_id => $act) { + if ($act !== 'delete' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $cleanPath = str_replace(array('../', '..\\', "\0"), '', (string)$row['meta']['path']); + $fullPath = realpath(DIR_IMAGE . $cleanPath); + if ($fullPath && strpos(str_replace('\\', '/', $fullPath), $realImageDir) === 0 && is_dir($fullPath)) { + $files = array_diff(scandir($fullPath), array('.', '..')); + if (count($files) === 0) { + @rmdir($fullPath); + } + } + } + @unlink(DIR_CACHE . 'img_opti_empty_folders_grid.json'); + return array('success' => true); + } + + public function scan_smart_cache($data) { + $this->loadLanguageSafe(); + $rows = array(); + $cache_dir = realpath(DIR_IMAGE . 'cache/catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$cache_dir || strpos(str_replace('\\', '/', $cache_dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($cache_dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($cache_dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $cache_dir)) !== 0) continue; + $rel_cache = ltrim(str_replace(str_replace('\\', '/', DIR_IMAGE . 'cache/'), '', str_replace('\\', '/', $realPath)), '/'); + $orig_rel = preg_replace('/-\d+x\d+\.(jpg|jpeg|png|webp|gif)$/i', '.$1', $rel_cache); + $orig_path = realpath(DIR_IMAGE . $orig_rel); + + if (!$orig_path || !is_file($orig_path) || strpos(str_replace('\\', '/', $orig_path), str_replace('\\', '/', $realImageDir)) !== 0) { + $thumb = (defined('HTTPS_CATALOG') && HTTPS_CATALOG ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : '')) . 'image/cache/' . $rel_cache; + $size = filesize($realPath); + + $rows[] = array( + 'id' => 'smart_cache-' . md5($rel_cache), + 'preview' => '', + 'name' => '' . htmlspecialchars($rel_cache, ENT_QUOTES, 'UTF-8') . '', + 'before' => $this->language->get('text_grid_orph_cache') . ' (' . $this->formatBytes($size) . ')', + 'after' => $this->language->get('text_grid_orph_cache_after'), + 'actions' => array( + 'delete' => $this->language->get('text_action_delete'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'delete', + 'meta' => array( + 'path' => $rel_cache + ) + ); + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_smart_cache_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => false, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_smart_cache') + ); + } + + public function fix_smart_cache($data) { + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_smart_cache_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + foreach ($selected as $row_id => $act) { + if ($act !== 'delete' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $this->quarantineFile('cache/' . $row['meta']['path'], 'smart_cache'); + } + @unlink(DIR_CACHE . 'img_opti_smart_cache_grid.json'); + return array('success' => true); + } + + public function scan_cache_restore($data) { + $this->loadLanguageSafe(); + $rows = array(); + $cache_dir = realpath(DIR_IMAGE . 'cache/catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$cache_dir || strpos(str_replace('\\', '/', $cache_dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($cache_dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $restore_folder = isset($data['module_img_opti_restore_folder']) ? trim((string)$data['module_img_opti_restore_folder']) : ''; + $restore_products = isset($data['module_img_opti_restore_target_product']) ? (int)$data['module_img_opti_restore_target_product'] : 1; + $restore_categories = isset($data['module_img_opti_restore_target_category']) ? (int)$data['module_img_opti_restore_target_category'] : 1; + $restore_brands = isset($data['module_img_opti_restore_target_brand']) ? (int)$data['module_img_opti_restore_target_brand'] : 1; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($cache_dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $cache_dir)) !== 0) continue; + $rel_cache = ltrim(str_replace(str_replace('\\', '/', DIR_IMAGE . 'cache/'), '', str_replace('\\', '/', $realPath)), '/'); + $orig_rel = preg_replace('/-\d+x\d+\.(jpg|jpeg|png|webp|gif)$/i', '.$1', $rel_cache); + $orig_path = realpath(DIR_IMAGE . $orig_rel); + + if (!$orig_path || !is_file($orig_path)) { + if (!empty($restore_folder)) { + $clean_restore_folder = str_replace(array('../', '..\\', "\0"), '', $restore_folder); + if (strpos($rel_cache, $clean_restore_folder) === false) continue; + } + + $entities = $this->getEntitiesByImagePath($orig_rel); + $has_product = false; + $has_category = false; + $has_brand = false; + foreach ($entities as $entity_html) { + if (strpos($entity_html, 'Product') !== false) $has_product = true; + if (strpos($entity_html, 'Category') !== false) $has_category = true; + if (strpos($entity_html, 'Manufacturer') !== false) $has_brand = true; + } + + $is_allowed = false; + if (empty($entities)) { + $is_allowed = true; + } else { + if ($has_product && $restore_products) $is_allowed = true; + if ($has_category && $restore_categories) $is_allowed = true; + if ($has_brand && $restore_brands) $is_allowed = true; + } + + if (!$is_allowed) continue; + + $thumb = (defined('HTTPS_CATALOG') && HTTPS_CATALOG ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : '')) . 'image/cache/' . $rel_cache; + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $rows[] = array( + 'id' => 'cache_restore-' . md5($rel_cache), + 'preview' => '', + 'name' => '' . htmlspecialchars($orig_rel, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => $this->language->get('text_grid_missing_orig'), + 'after' => $this->language->get('text_grid_missing_orig_after'), + 'actions' => array( + 'restore' => $this->language->get('text_action_restore'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'restore', + 'meta' => array( + 'cache_path' => $rel_cache, + 'orig_path' => $orig_rel + ) + ); + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_cache_restore_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => false, + 'quarantine_available' => false, + 'description' => $this->language->get('text_description_cache_restore') + ); + } + + public function fix_cache_restore($data) { + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_cache_restore_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + + foreach ($selected as $row_id => $act) { + if ($act !== 'restore' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $cacheFullPath = realpath(DIR_IMAGE . 'cache/' . $row['meta']['cache_path']); + if (!$cacheFullPath || !is_file($cacheFullPath)) continue; + + $origRel = (string)$row['meta']['orig_path']; + $destPath = DIR_IMAGE . $origRel; + if (is_file($destPath)) continue; + $destDir = $this->mb_dirname($destPath); + if (!is_dir($destDir)) { + @mkdir($destDir, 0755, true); + } + $realDestDir = realpath($destDir); + if ($realDestDir && strpos(str_replace('\\', '/', $realDestDir), $realImageDir) === 0) { + @copy($cacheFullPath, $realDestDir . DIRECTORY_SEPARATOR . $this->mb_basename($origRel)); + } + } + @unlink(DIR_CACHE . 'img_opti_cache_restore_grid.json'); + return array('success' => true); + } + + public function scan_exif($data) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile() && in_array(strtolower($file->getExtension()), array('jpg', 'jpeg'))) { + $realPath = realpath($file->getPathname()); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) !== 0) continue; + $exif = @exif_read_data($realPath); + if ($exif && (isset($exif['Make']) || isset($exif['Model']) || isset($exif['GPSLatitude']) || isset($exif['GPSLongitude']) || isset($exif['Software']) || isset($exif['DateTime']) || isset($exif['GPSInfo']) || isset($exif['GPSLatitudeRef']) || isset($exif['GPSLongitudeRef']) || isset($exif['DateTimeOriginal']) || isset($exif['Artist']) || isset($exif['Copyright']) || isset($exif['UserComment']))) { + $relPath = $this->getRelativeImagePath($realPath); + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + $size = filesize($realPath); + + $exif_details = array(); + if (isset($exif['Make'])) $exif_details[] = 'Make: ' . htmlspecialchars($exif['Make'], ENT_QUOTES, 'UTF-8'); + if (isset($exif['Model'])) $exif_details[] = 'Model: ' . htmlspecialchars($exif['Model'], ENT_QUOTES, 'UTF-8'); + if (isset($exif['Software'])) $exif_details[] = 'Software: ' . htmlspecialchars($exif['Software'], ENT_QUOTES, 'UTF-8'); + if (isset($exif['GPSLatitude']) || isset($exif['GPSLongitude']) || isset($exif['GPSInfo']) || isset($exif['GPSLatitudeRef']) || isset($exif['GPSLongitudeRef'])) $exif_details[] = 'GPS: Yes'; + if (isset($exif['DateTime'])) $exif_details[] = 'Date: ' . htmlspecialchars($exif['DateTime'], ENT_QUOTES, 'UTF-8'); + if (isset($exif['DateTimeOriginal'])) $exif_details[] = 'DateOriginal: ' . htmlspecialchars($exif['DateTimeOriginal'], ENT_QUOTES, 'UTF-8'); + if (isset($exif['Artist'])) $exif_details[] = 'Artist: ' . htmlspecialchars($exif['Artist'], ENT_QUOTES, 'UTF-8'); + if (isset($exif['Copyright'])) $exif_details[] = 'Copyright: ' . htmlspecialchars($exif['Copyright'], ENT_QUOTES, 'UTF-8'); + if (isset($exif['UserComment'])) $exif_details[] = 'Comment: ' . htmlspecialchars($exif['UserComment'], ENT_QUOTES, 'UTF-8'); + + $exif_info = implode(', ', $exif_details); + if (!$exif_info) $exif_info = 'EXIF data present'; + + $rows[] = array( + 'id' => 'exif-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => $exif_info . ' (' . $this->formatBytes($size) . ')', + 'after' => $this->language->get('text_grid_exif_after'), + 'actions' => array( + 'strip' => $this->language->get('text_action_strip'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'strip', + 'meta' => array( + 'path' => $relPath + ) + ); + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_exif_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => false, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_exif') + ); + } + + public function fix_exif($data) { + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_exif_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + + foreach ($selected as $row_id => $act) { + if ($act !== 'strip' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $cleanPath = str_replace(array('../', '..\\', "\0"), '', (string)$row['meta']['path']); + $fullPath = realpath(DIR_IMAGE . $cleanPath); + if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) continue; + + $this->quarantineFile($cleanPath, 'exif'); + + $restoredFullPath = DIR_IMAGE . 'catalog_trash/exif/' . ltrim($cleanPath, '/'); + if (is_file($restoredFullPath)) { + if (extension_loaded('imagick')) { + try { + $img = new Imagick($restoredFullPath); + $img->stripImage(); + $img->writeImage($fullPath); + $img->clear(); $img->destroy(); + } catch (Exception $e) {} + } else { + $img = @imagecreatefromjpeg($restoredFullPath); + if ($img) { + imagejpeg($img, $fullPath, 90); + imagedestroy($img); + } + } + } + } + @unlink(DIR_CACHE . 'img_opti_exif_grid.json'); + return array('success' => true); + } + + public function scan_placeholders($data = array()) { + $this->loadLanguageSafe(); + $rows = array(); + $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + + $scan_products = !empty($data) ? (isset($data['module_img_opti_ph_target_product']) ? (int)$data['module_img_opti_ph_target_product'] : 0) : ($this->config->get('module_img_opti_ph_target_product') !== null ? (int)$this->config->get('module_img_opti_ph_target_product') : 1); + $scan_categories = !empty($data) ? (isset($data['module_img_opti_ph_target_category']) ? (int)$data['module_img_opti_ph_target_category'] : 0) : ($this->config->get('module_img_opti_ph_target_category') !== null ? (int)$this->config->get('module_img_opti_ph_target_category') : 1); + $scan_brands = !empty($data) ? (isset($data['module_img_opti_ph_target_brand']) ? (int)$data['module_img_opti_ph_target_brand'] : 0) : ($this->config->get('module_img_opti_ph_target_brand') !== null ? (int)$this->config->get('module_img_opti_ph_target_brand') : 1); + + $this->load->model('tool/image'); + + $rules_json = $this->config->get('module_img_opti_ph_rules'); + $rules = json_decode(html_entity_decode($rules_json, ENT_QUOTES, 'UTF-8'), true) ?: array(); + + if ($scan_products) { + $query = $this->db->query("SELECT p.product_id as id, p.model, p.status, p.date_available, (SELECT p2s.store_id FROM `" . DB_PREFIX . "product_to_store` p2s WHERE p2s.product_id = p.product_id AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id, (SELECT name FROM `" . DB_PREFIX . "product_description` pd WHERE pd.product_id = p.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "product` p WHERE image = '' OR image IS NULL"); + foreach ($query->rows as $row) { + $matched_image = ''; + foreach ($rules as $rule) { + if (empty($rule['status']) || empty($rule['target_product'])) continue; + + $categories = array(); + if (!empty($rule['categories'])) { + foreach ($rule['categories'] as $c) { + $categories[] = is_array($c) ? (int)$c['id'] : (int)$c; + } + } + $brands = array(); + if (!empty($rule['brands'])) { + foreach ($rule['brands'] as $b) { + $brands[] = is_array($b) ? (int)$b['id'] : (int)$b; + } + } + + $product_id = (int)$row['id']; + $query_p_info = $this->db->query("SELECT manufacturer_id FROM `" . DB_PREFIX . "product` WHERE product_id = '" . $product_id . "' LIMIT 1"); + $manufacturer_id = $query_p_info->num_rows ? (int)$query_p_info->row['manufacturer_id'] : 0; + + $prod_categories = array(); + $query_p_cats = $this->db->query("SELECT category_id FROM `" . DB_PREFIX . "product_to_category` WHERE product_id = '" . $product_id . "'"); + foreach ($query_p_cats->rows as $cat_row) { + $prod_categories[] = (int)$cat_row['category_id']; + } + + $in_selected_cats = false; + if (!empty($categories)) { + foreach ($prod_categories as $c_id) { + if (in_array($c_id, $categories)) { + $in_selected_cats = true; + break; + } + } + } + $in_selected_brands = false; + if (!empty($brands) && in_array($manufacturer_id, $brands)) { + $in_selected_brands = true; + } + + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'exclude'; + $match = true; + if ($filter_mode === 'include') { + if (!empty($categories) || !empty($brands)) { + if (!$in_selected_cats && !$in_selected_brands) { + $match = false; + } + } + } else { + if ($in_selected_cats || $in_selected_brands) { + $match = false; + } + } + + if ($match && !empty($rule['image'])) { + $matched_image = $rule['image']; + break; + } + } + + if (!$matched_image) continue; + + $admin_link = $this->url->link('catalog/product/edit', 'user_token=' . $this->session->data['user_token'] . '&product_id=' . (int)$row['id'], true); + $catalog_link_html = ''; + if (isset($row['status']) && (int)$row['status'] === 1 && (empty($row['date_available']) || strtotime($row['date_available']) <= time()) && (isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/product&product_id=' . (int)$row['id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name'], $row['model']); + $rows[] = array( + 'id' => 'placeholder_product-' . (int)$row['id'], + 'preview' => is_file(DIR_IMAGE . $matched_image) ? '' : '', + 'name' => 'Product ' . htmlspecialchars($row['name'] ? $row['name'] : 'Product ID ' . (int)$row['id'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links, + 'before' => $this->language->get('text_grid_no_image'), + 'after' => $this->mb_basename($matched_image), + 'actions' => array( + 'set_placeholder' => $this->language->get('text_action_set_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'set_placeholder', + 'meta' => array( + 'image' => $matched_image + ) + ); + } + } + + if ($scan_categories) { + $query_cat = $this->db->query("SELECT c.category_id as id, c.status, (SELECT c2s.store_id FROM `" . DB_PREFIX . "category_to_store` c2s WHERE c2s.category_id = c.category_id AND c2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id, (SELECT name FROM `" . DB_PREFIX . "category_description` cd WHERE cd.category_id = c.category_id AND cd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "category` c WHERE image = '' OR image IS NULL"); + foreach ($query_cat->rows as $row) { + $matched_image = ''; + foreach ($rules as $rule) { + if (empty($rule['status']) || empty($rule['target_category'])) continue; + $categories = array(); + if (!empty($rule['categories'])) { + foreach ($rule['categories'] as $c) { + $categories[] = is_array($c) ? (int)$c['id'] : (int)$c; + } + } + $category_id = (int)$row['id']; + $match = true; + if (!empty($categories)) { + $in_selected = in_array($category_id, $categories); + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'include'; + if ($filter_mode === 'include' && !$in_selected) { + $match = false; + } elseif ($filter_mode === 'exclude' && $in_selected) { + $match = false; + } + } + if ($match && !empty($rule['image'])) { + $matched_image = $rule['image']; + break; + } + } + + if (!$matched_image) continue; + + $admin_link = $this->url->link('catalog/category/edit', 'user_token=' . $this->session->data['user_token'] . '&category_id=' . (int)$row['id'], true); + $catalog_link_html = ''; + if (isset($row['status']) && (int)$row['status'] === 1 && (isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/category&path=' . (int)$row['id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name']); + $rows[] = array( + 'id' => 'placeholder_category-' . (int)$row['id'], + 'preview' => is_file(DIR_IMAGE . $matched_image) ? '' : '', + 'name' => 'Category ' . htmlspecialchars($row['name'] ? $row['name'] : 'Category ID ' . (int)$row['id'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links, + 'before' => $this->language->get('text_grid_no_image'), + 'after' => $this->mb_basename($matched_image), + 'actions' => array( + 'set_placeholder' => $this->language->get('text_action_set_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'set_placeholder', + 'meta' => array( + 'image' => $matched_image + ) + ); + } + } + + if ($scan_brands) { + $query_man = $this->db->query("SELECT m.manufacturer_id as id, m.name, (SELECT m2s.store_id FROM `" . DB_PREFIX . "manufacturer_to_store` m2s WHERE m2s.manufacturer_id = m.manufacturer_id AND m2s.store_id = '" . (int)$this->config->get('config_store_id') . "' LIMIT 1) AS store_id FROM `" . DB_PREFIX . "manufacturer` m WHERE image = '' OR image IS NULL"); + foreach ($query_man->rows as $row) { + $matched_image = ''; + foreach ($rules as $rule) { + if (empty($rule['status']) || empty($rule['target_brand'])) continue; + $brands = array(); + if (!empty($rule['brands'])) { + foreach ($rule['brands'] as $b) { + $brands[] = is_array($b) ? (int)$b['id'] : (int)$b; + } + } + $manufacturer_id = (int)$row['id']; + $match = true; + if (!empty($brands)) { + $in_selected = in_array($manufacturer_id, $brands); + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'include'; + if ($filter_mode === 'include' && !$in_selected) { + $match = false; + } elseif ($filter_mode === 'exclude' && $in_selected) { + $match = false; + } + } + if ($match && !empty($rule['image'])) { + $matched_image = $rule['image']; + break; + } + } + + if (!$matched_image) continue; + + $admin_link = $this->url->link('catalog/manufacturer/edit', 'user_token=' . $this->session->data['user_token'] . '&manufacturer_id=' . (int)$row['id'], true); + $catalog_link_html = ''; + if ((isset($row['store_id']) && $row['store_id'] !== null)) { + $catalog_link = $catalog_url . 'index.php?route=product/manufacturer/info&manufacturer_id=' . (int)$row['id']; + $catalog_link_html = ' '; + } + $search_links = $this->getSearchLinks($row['name']); + $rows[] = array( + 'id' => 'placeholder_manufacturer-' . (int)$row['id'], + 'preview' => is_file(DIR_IMAGE . $matched_image) ? '' : '', + 'name' => 'Manufacturer ' . htmlspecialchars($row['name'], ENT_QUOTES, 'UTF-8') . '' . $catalog_link_html . $search_links, + 'before' => $this->language->get('text_grid_no_image'), + 'after' => $this->mb_basename($matched_image), + 'actions' => array( + 'set_placeholder' => $this->language->get('text_action_set_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'set_placeholder', + 'meta' => array( + 'image' => $matched_image + ) + ); + } + } + + file_put_contents(DIR_CACHE . 'img_opti_placeholders_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => false, + 'description' => $this->language->get('text_description_placeholders') + ); + } + + public function fix_placeholders($data) { + $this->backupCatalogTables('placeholders'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_placeholders_grid.json'), true); + foreach ($grid as $row) { + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + foreach ($selected as $row_id => $act) { + if ($act !== 'set_placeholder') continue; + + $image_path = ''; + foreach ($grid as $row) { + if ($row['id'] === $row_id) { + $image_path = isset($row['meta']['image']) ? (string)$row['meta']['image'] : ''; + break; + } + } + if (!$image_path) continue; + + $parts = explode('-', (string)$row_id, 2); + if (count($parts) !== 2) continue; + $type = preg_replace('/[^a-zA-Z_]/', '', $parts[0]); + $id = (int)$parts[1]; + + if ($type === 'placeholder_product') { + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $this->db->escape($image_path) . "' WHERE product_id = '" . $id . "'"); + } elseif ($type === 'placeholder_category') { + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $this->db->escape($image_path) . "' WHERE category_id = '" . $id . "'"); + } elseif ($type === 'placeholder_manufacturer') { + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $this->db->escape($image_path) . "' WHERE manufacturer_id = '" . $id . "'"); + } + } + @unlink(DIR_CACHE . 'img_opti_placeholders_grid.json'); + return array('success' => true); + } + + public function scan_watermark($data) { + $this->loadLanguageSafe(); + $cache_dir = realpath(DIR_IMAGE . 'cache/catalog'); + $realImageDir = realpath(DIR_IMAGE); + if ($cache_dir && strpos(str_replace('\\', '/', $cache_dir), str_replace('\\', '/', $realImageDir)) !== 0 && is_dir($cache_dir)) { + $this->cleanDir($cache_dir); + } + return array( + 'success' => true, + 'message' => $this->language->get('text_log_cache_cleared') . '
    ' . $this->language->get('text_log_wm_smart_mode') + ); + } + + private function cleanDir($dir) { + $realDir = realpath($dir); + $realImageDir = realpath(DIR_IMAGE); + if (!$realDir || strpos(str_replace('\\', '/', $realDir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($realDir)) return; + $files = array_diff(scandir($realDir), array('.','..')); + foreach ($files as $file) { + $currentPath = $realDir . DIRECTORY_SEPARATOR . $this->mb_basename($file); + (is_dir($currentPath)) ? $this->cleanDir($currentPath) : @unlink($currentPath); + } + @rmdir($realDir); + } + + public function applyDynamicWatermark($cached_file, $original_file, $is_preview = false) { + $realCachedFile = realpath($cached_file); + $realImageDir = realpath(DIR_IMAGE); + if (!$realCachedFile || strpos(str_replace('\\', '/', $realCachedFile), str_replace('\\', '/', $realImageDir)) !== 0) return false; + + $escaped_original = $this->db->escape((string)$original_file); + $matched_rules = array(); + + if ($is_preview) { + $matched_rules[] = array( + 'status' => 1, + 'type' => $this->config->get('module_img_opti_wm_type'), + 'image' => $this->config->get('module_img_opti_wm_image'), + 'text_val' => $this->config->get('module_img_opti_wm_text_val'), + 'position' => $this->config->get('module_img_opti_wm_position'), + 'opacity' => $this->config->get('module_img_opti_wm_opacity'), + 'angle' => $this->config->get('module_img_opti_wm_angle'), + 'corner_radius' => $this->config->get('module_img_opti_wm_corner_radius'), + 'size_type' => $this->config->get('module_img_opti_wm_size_type'), + 'size_percent' => $this->config->get('module_img_opti_wm_size_percent'), + 'text_color' => $this->config->get('module_img_opti_wm_text_color'), + 'text_font' => $this->config->get('module_img_opti_wm_text_font') + ); + } else { + $rules_json = $this->config->get('module_img_opti_wm_rules'); + $rules = json_decode(html_entity_decode($rules_json, ENT_QUOTES, 'UTF-8'), true) ?: array(); + + foreach ($rules as $rule) { + if (empty($rule['status'])) continue; + + $targets = array(); + if (!empty($rule['target_product'])) $targets[] = 'product'; + if (!empty($rule['target_category'])) $targets[] = 'category'; + if (!empty($rule['target_brand'])) $targets[] = 'manufacturer'; + if (!empty($rule['target_banner'])) $targets[] = 'banner'; + if (!empty($rule['target_blog'])) $targets[] = 'blog'; + + $is_match = false; + $product_id = 0; + $manufacturer_id = 0; + + if (in_array('product', $targets)) { + $query_prod = $this->db->query("SELECT product_id, manufacturer_id FROM `" . DB_PREFIX . "product` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_prod->num_rows) { + $is_match = true; + $product_id = (int)$query_prod->row['product_id']; + $manufacturer_id = (int)$query_prod->row['manufacturer_id']; + } else { + $query_prod_add = $this->db->query("SELECT product_id FROM `" . DB_PREFIX . "product_image` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_prod_add->num_rows) { + $is_match = true; + $product_id = (int)$query_prod_add->row['product_id']; + $query_p_info = $this->db->query("SELECT manufacturer_id FROM `" . DB_PREFIX . "product` WHERE product_id = '" . $product_id . "' LIMIT 1"); + $manufacturer_id = $query_p_info->num_rows ? (int)$query_p_info->row['manufacturer_id'] : 0; + } + } + + if ($is_match) { + $categories = array(); + if (!empty($rule['categories'])) { + foreach ($rule['categories'] as $c) { + $categories[] = is_array($c) ? (int)$c['id'] : (int)$c; + } + } + $brands = array(); + if (!empty($rule['brands'])) { + foreach ($rule['brands'] as $b) { + $brands[] = is_array($b) ? (int)$b['id'] : (int)$b; + } + } + + $prod_categories = array(); + $query_p_cats = $this->db->query("SELECT category_id FROM `" . DB_PREFIX . "product_to_category` WHERE product_id = '" . $product_id . "'"); + foreach ($query_p_cats->rows as $row) { + $prod_categories[] = (int)$row['category_id']; + } + + $in_selected_cats = false; + if (!empty($categories)) { + foreach ($prod_categories as $c_id) { + if (in_array($c_id, $categories)) { + $in_selected_cats = true; + break; + } + } + } + + $in_selected_brands = false; + if (!empty($brands) && in_array($manufacturer_id, $brands)) { + $in_selected_brands = true; + } + + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'include'; + if ($filter_mode === 'include') { + if (!empty($categories) || !empty($brands)) { + if (!$in_selected_cats && !$in_selected_brands) { + $is_match = false; + } + } + } else { + if ($in_selected_cats || $in_selected_brands) { + $is_match = false; + } + } + } + } + + if (!$is_match && in_array('category', $targets)) { + $query_cat = $this->db->query("SELECT category_id FROM `" . DB_PREFIX . "category` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_cat->num_rows) { + $is_match = true; + $category_id = (int)$query_cat->row['category_id']; + $categories = array(); + if (!empty($rule['categories'])) { + foreach ($rule['categories'] as $c) { + $categories[] = is_array($c) ? (int)$c['id'] : (int)$c; + } + } + if (!empty($categories)) { + $in_selected = in_array($category_id, $categories); + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'include'; + if ($filter_mode === 'include' && !$in_selected) { + $is_match = false; + } elseif ($filter_mode === 'exclude' && $in_selected) { + $is_match = false; + } + } + } + } + + if (!$is_match && in_array('manufacturer', $targets)) { + $query_brand = $this->db->query("SELECT manufacturer_id FROM `" . DB_PREFIX . "manufacturer` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_brand->num_rows) { + $is_match = true; + $manufacturer_id = (int)$query_brand->row['manufacturer_id']; + $brands = array(); + if (!empty($rule['brands'])) { + foreach ($rule['brands'] as $b) { + $brands[] = is_array($b) ? (int)$b['id'] : (int)$b; + } + } + if (!empty($brands)) { + $in_selected = in_array($manufacturer_id, $brands); + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'include'; + if ($filter_mode === 'include' && !$in_selected) { + $is_match = false; + } elseif ($filter_mode === 'exclude' && $in_selected) { + $is_match = false; + } + } + } + } + + if (!$is_match && in_array('banner', $targets)) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $query_banner = $this->db->query("SELECT banner_image_id FROM `" . DB_PREFIX . "banner_image` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_banner->num_rows) { + $is_match = true; + } + } + } + + if (!$is_match && in_array('blog', $targets)) { + $blog_tables = array( + 'oct_blog_article', + 'simple_blog_article', + 'newsblog_article', + 'information' + ); + foreach ($blog_tables as $b_table) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $b_table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $b_table . "` LIKE 'image'"); + if ($column_query->num_rows) { + $query_blog = $this->db->query("SELECT * FROM `" . DB_PREFIX . $b_table . "` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_blog->num_rows) { + $is_match = true; + break; + } + } + } + } + } + + if ($is_match) { + $matched_rules[] = $rule; + } + } + } + + if (empty($matched_rules)) { + return false; + } + + $engine = preg_replace('/[^a-z]/', '', strtolower($this->config->get('module_img_opti_engine'))); + $hasImagick = extension_loaded('imagick'); + + if ($engine === 'imagick' && $hasImagick) { + try { + $img = new Imagick($realCachedFile); + foreach ($matched_rules as $rule) { + $type = preg_replace('/[^a-z]/', '', $rule['type']); + $imagePathStr = str_replace(array('../', '..\\', ' '), '', (string)$rule['image']); + $imagePath = realpath(DIR_IMAGE . $imagePathStr); + if ($imagePath && strpos(str_replace('\\', '/', $imagePath), str_replace('\\', '/', $realImageDir)) !== 0) { + $imagePath = false; + } + $textVal = (string)$rule['text_val']; + $position = (int)$rule['position']; + $opacity = (int)$rule['opacity']; + $size_type = isset($rule['size_type']) ? $rule['size_type'] : 'original'; + $size_percent = isset($rule['size_percent']) ? (int)$rule['size_percent'] : 20; + $angle = isset($rule['angle']) ? (int)$rule['angle'] : 0; + $corner_radius = isset($rule['corner_radius']) ? (int)$rule['corner_radius'] : 0; + $text_font = isset($rule['text_font']) ? (string)$rule['text_font'] : ''; + $text_color = isset($rule['text_color']) ? (string)$rule['text_color'] : '#ffffff'; + + if ($type === 'image') { + if (!$imagePath || !is_file($imagePath)) continue; + } + if ($type === 'text' && empty($textVal)) continue; + + $imgW = $img->getImageWidth(); + $imgH = $img->getImageHeight(); + + if ($type === 'image') { + $wm = new Imagick($imagePath); + + $max_wm_w = (int)$this->config->get('module_img_opti_wm_max_width'); + if ($max_wm_w <= 0) $max_wm_w = 800; + $max_wm_h = (int)$this->config->get('module_img_opti_wm_max_height'); + if ($max_wm_h <= 0) $max_wm_h = 800; + + $wmW = $wm->getImageWidth(); + $wmH = $wm->getImageHeight(); + if ($wmW > $max_wm_w || $wmH > $max_wm_h) { + $ratio = min($max_wm_w / $wmW, $max_wm_h / $wmH); + $new_wm_w = intval($wmW * $ratio); + $new_wm_h = intval($wmH * $ratio); + if ($new_wm_w > 0 && $new_wm_h > 0) { + $wm->resizeImage($new_wm_w, $new_wm_h, Imagick::FILTER_LANCZOS, 1); + } + } + + if ($size_type === 'percent' && $size_percent > 0) { + $targetW = intval(($imgW * $size_percent) / 100); + $targetH = intval(($wm->getImageHeight() / $wm->getImageWidth()) * $targetW); + if ($targetW > 0 && $targetH > 0) { + $wm->resizeImage($targetW, $targetH, Imagick::FILTER_LANCZOS, 1); + } + } + if ($corner_radius > 0) { + $wm->roundCorners($corner_radius, $corner_radius); + } + if ($angle != 0) { + $wm->rotateImage(new ImagickPixel('none'), $angle); + } + $wm->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA); + $wmW = $wm->getImageWidth(); + $wmH = $wm->getImageHeight(); + } else { + $wm = new Imagick(); + $draw = new ImagickDraw(); + $fontFile = $text_font; + if (!empty($fontFile) && !is_file($fontFile)) { + $base_font = basename($fontFile); + if (is_file(DIR_SYSTEM . 'library/font/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'library/font/' . $base_font; + } elseif (is_file(DIR_SYSTEM . 'fonts/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'fonts/' . $base_font; + } elseif (is_file(DIR_SYSTEM . 'font/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'font/' . $base_font; + } + } + if (empty($fontFile) || !is_file($fontFile)) { + $fontFile = DIR_SYSTEM . 'library/font/Roboto-Regular.ttf'; + } + if (is_file($fontFile)) { + $draw->setFont($fontFile); + } + $fontSize = max(12, $imgW * 0.04); + $draw->setFontSize($fontSize); + $colorHex = $text_color; + list($r, $g, $b) = $this->hex2rgb($colorHex); + $draw->setFillColor(new ImagickPixel('rgba(' . $r . ',' . $g . ',' . $b . ',' . ($opacity / 100) . ')')); + $draw->setGravity(Imagick::GRAVITY_CENTER); + $wm->newImage($imgW, $imgH, new ImagickPixel('transparent')); + $wm->annotateImage($draw, 0, 0, 0, $textVal); + if ($angle != 0) { + $wm->rotateImage(new ImagickPixel('none'), $angle); + } + $wmW = $wm->getImageWidth(); + $wmH = $wm->getImageHeight(); + } + $posX = 0; $posY = 0; $pad = 10; + if (in_array($position, array(1, 4, 7))) $posX = $pad; + elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $wmW) / 2; + elseif (in_array($position, array(3, 6, 9))) $posX = $imgW - $wmW - $pad; + + if (in_array($position, array(1, 2, 3))) $posY = $pad; + elseif (in_array($position, array(4, 5, 6))) $posY = ($imgH - $wmH) / 2; + elseif (in_array($position, array(7, 8, 9))) $posY = $imgH - $wmH - $pad; + + $img->compositeImage($wm, Imagick::COMPOSITE_OVER, $posX, $posY); + $wm->clear(); $wm->destroy(); + } + $img->writeImage($realCachedFile); + $img->clear(); $img->destroy(); + } catch (Exception $e) {} + } else { + $ext = strtolower($this->mb_pathinfo($realCachedFile, PATHINFO_EXTENSION)); + if ($ext === 'jpg' || $ext === 'jpeg') { + $img = @imagecreatefromjpeg($realCachedFile); + } elseif ($ext === 'png') { + $img = @imagecreatefrompng($realCachedFile); + } elseif ($ext === 'webp' && function_exists('imagecreatefromwebp')) { + $img = @imagecreatefromwebp($realCachedFile); + } else { + $img = false; + } + + if ($img) { + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + imagealphablending($img, true); + + $imgW = imagesx($img); + $imgH = imagesy($img); + + foreach ($matched_rules as $rule) { + $type = preg_replace('/[^a-z]/', '', $rule['type']); + $imagePathStr = str_replace(array('../', '..\\', ' '), '', (string)$rule['image']); + $imagePath = realpath(DIR_IMAGE . $imagePathStr); + if ($imagePath && strpos(str_replace('\\', '/', $imagePath), str_replace('\\', '/', $realImageDir)) !== 0) { + $imagePath = false; + } + $textVal = (string)$rule['text_val']; + $position = (int)$rule['position']; + $opacity = (int)$rule['opacity']; + $size_type = isset($rule['size_type']) ? $rule['size_type'] : 'original'; + $size_percent = isset($rule['size_percent']) ? (int)$rule['size_percent'] : 20; + $angle = isset($rule['angle']) ? (int)$rule['angle'] : 0; + $corner_radius = isset($rule['corner_radius']) ? (int)$rule['corner_radius'] : 0; + $text_font = isset($rule['text_font']) ? (string)$rule['text_font'] : ''; + $text_color = isset($rule['text_color']) ? (string)$rule['text_color'] : '#ffffff'; + + if ($type === 'image') { + if (!$imagePath || !is_file($imagePath)) continue; + } + if ($type === 'text' && empty($textVal)) continue; + + if ($type === 'image') { + $wm = @imagecreatefrompng($imagePath); + if ($wm) { + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($wm)) { + imagepalettetotruecolor($wm); + } + imagealphablending($wm, false); + imagesavealpha($wm, true); + + $wmW = imagesx($wm); + $wmH = imagesy($wm); + + $max_wm_w = (int)$this->config->get('module_img_opti_wm_max_width'); + if ($max_wm_w <= 0) $max_wm_w = 800; + $max_wm_h = (int)$this->config->get('module_img_opti_wm_max_height'); + if ($max_wm_h <= 0) $max_wm_h = 800; + + if ($wmW > $max_wm_w || $wmH > $max_wm_h) { + $ratio = min($max_wm_w / $wmW, $max_wm_h / $wmH); + $new_wm_w = intval($wmW * $ratio); + $new_wm_h = intval($wmH * $ratio); + if ($new_wm_w > 0 && $new_wm_h > 0) { + $resized_wm = imagecreatetruecolor($new_wm_w, $new_wm_h); + imagealphablending($resized_wm, false); + imagesavealpha($resized_wm, true); + $transparent = imagecolorallocatealpha($resized_wm, 255, 255, 255, 127); + imagefill($resized_wm, 0, 0, $transparent); + imagecopyresampled($resized_wm, $wm, 0, 0, 0, 0, $new_wm_w, $new_wm_h, $wmW, $wmH); + imagedestroy($wm); + $wm = $resized_wm; + $wmW = $new_wm_w; + $wmH = $new_wm_h; + } + } + if ($size_type === 'percent' && $size_percent > 0) { + $targetW = intval(($imgW * $size_percent) / 100); + $targetH = intval(($wmH / $wmW) * $targetW); + if ($targetW > 0 && $targetH > 0) { + $resized_wm = imagecreatetruecolor($targetW, $targetH); + imagealphablending($resized_wm, false); + imagesavealpha($resized_wm, true); + $transparent = imagecolorallocatealpha($resized_wm, 255, 255, 255, 127); + imagefill($resized_wm, 0, 0, $transparent); + imagecopyresampled($resized_wm, $wm, 0, 0, 0, 0, $targetW, $targetH, $wmW, $wmH); + imagedestroy($wm); + $wm = $resized_wm; + $wmW = $targetW; + $wmH = $targetH; + } + } + if ($corner_radius > 0) { + $wm = $this->roundCornersGD($wm, $corner_radius); + } + if ($angle != 0) { + $gd_angle = -1 * $angle; + $transparent = imagecolorallocatealpha($wm, 255, 255, 255, 127); + $rotated = imagerotate($wm, $gd_angle, $transparent); + if ($rotated) { + imagealphablending($rotated, false); + imagesavealpha($rotated, true); + imagedestroy($wm); + $wm = $rotated; + $wmW = imagesx($wm); + $wmH = imagesy($wm); + } + } + $posX = 0; $posY = 0; $pad = 10; + if (in_array($position, array(1, 4, 7))) $posX = $pad; + elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $wmW) / 2; + elseif (in_array($position, array(3, 6, 9))) $posX = $imgW - $wmW - $pad; + + if (in_array($position, array(1, 2, 3))) $posY = $pad; + elseif (in_array($position, array(4, 5, 6))) $posY = ($imgH - $wmH) / 2; + elseif (in_array($position, array(7, 8, 9))) $posY = $imgH - $wmH - $pad; + + $this->imagecopymerge_alpha($img, $wm, $posX, $posY, 0, 0, $wmW, $wmH, $opacity); + imagedestroy($wm); + } + } else { + $fontFile = $text_font; + if (!empty($fontFile) && !is_file($fontFile)) { + $base_font = basename($fontFile); + if (is_file(DIR_SYSTEM . 'library/font/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'library/font/' . $base_font; + } elseif (is_file(DIR_SYSTEM . 'fonts/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'fonts/' . $base_font; + } elseif (is_file(DIR_SYSTEM . 'font/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'font/' . $base_font; + } + } + if (empty($fontFile) || !is_file($fontFile)) { + $fontFile = DIR_SYSTEM . 'library/font/Roboto-Regular.ttf'; + } + if (!is_file($fontFile)) { + $open_basedir = ini_get('open_basedir'); + $fallback_dirs = array( + DIR_SYSTEM . 'library/font/', + DIR_SYSTEM . 'fonts/', + DIR_SYSTEM . 'font/', + DIR_CATALOG . 'view/theme/default/fonts/' + ); + if (DIRECTORY_SEPARATOR === '/') { + if (!$open_basedir) { + $fallback_dirs[] = '/usr/share/fonts/truetype/dejavu/'; + $fallback_dirs[] = '/usr/share/fonts/truetype/liberation/'; + $fallback_dirs[] = '/usr/share/fonts/truetype/freefont/'; + } + } else { + if (!$open_basedir) { + $fallback_dirs[] = 'C:/Windows/Fonts/'; + } + } + foreach ($fallback_dirs as $f_dir) { + if (@is_dir($f_dir)) { + $glob = @glob($f_dir . '*.ttf'); + if ($glob) { + $fontFile = $glob[0]; + break; + } + } + } + } + + if (!is_file($fontFile)) { + $fontSize = max(3, intval($imgW / 50)); + $fontWidth = imagefontwidth($fontSize) * strlen($textVal); + $fontHeight = imagefontheight($fontSize); + $posX = 0; $posY = 0; $pad = 10; + if (in_array($position, array(1, 4, 7))) $posX = $pad; + elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $fontWidth) / 2; + elseif (in_array($position, array(3, 6, 9))) $posX = $imgW - $fontWidth - $pad; + + if (in_array($position, array(1, 2, 3))) $posY = $pad; + elseif (in_array($position, array(4, 5, 6))) $posY = ($imgH - $fontHeight) / 2; + elseif (in_array($position, array(7, 8, 9))) $posY = $imgH - $fontHeight - $pad; + + $alpha = intval(127 - (127 * ($opacity / 100))); + $color = imagecolorallocatealpha($img, 255, 255, 255, $alpha); + imagestring($img, $fontSize, $posX, $posY, $textVal, $color); + } else { + $fontSize = max(10, intval($imgW / 25)); + $colorHex = $text_color; + list($r, $g, $b) = $this->hex2rgb($colorHex); + $alpha = intval(127 - (127 * ($opacity / 100))); + $color = imagecolorallocatealpha($img, $r, $g, $b, $alpha); + + $bbox = imagettfbbox($fontSize, $angle, $fontFile, $textVal); + $textW = abs($bbox[4] - $bbox[0]); + $textH = abs($bbox[5] - $bbox[1]); + + $posX = 0; $posY = 0; $pad = 10; + if (in_array($position, array(1, 4, 7))) $posX = $pad; + elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $textW) / 2; + elseif (in_array($position, array(3, 6, 9))) $posX = $imgW - $textW - $pad; + + if (in_array($position, array(1, 2, 3))) $posY = $pad + $textH; + elseif (in_array($position, array(4, 5, 6))) $posY = ($imgH - $textH) / 2 + $textH; + elseif (in_array($position, array(7, 8, 9))) $posY = $imgH - $pad; + + imagettftext($img, $fontSize, $angle, $posX, $posY, $color, $fontFile, $textVal); + } + } + } + + if ($ext === 'png' || $ext === 'webp') { + imagealphablending($img, false); + imagesavealpha($img, true); + } + if ($ext === 'jpg' || $ext === 'jpeg') { imagejpeg($img, $realCachedFile, 90); } + elseif ($ext === 'png') { imagepng($img, $realCachedFile, 9); } + elseif ($ext === 'webp') { imagewebp($img, $realCachedFile, 90); } + imagedestroy($img); + } + } + return true; + } + +private function roundCornersGD($im, $radius) { + $w = imagesx($im); + $h = imagesy($im); + $new_im = imagecreatetruecolor($w, $h); + imagealphablending($new_im, false); + imagesavealpha($new_im, true); + $transparent = imagecolorallocatealpha($new_im, 255, 255, 255, 127); + imagefill($new_im, 0, 0, $transparent); + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + $rgba = imagecolorat($im, $x, $y); + $in_corner = false; + if ($x < $radius && $y < $radius) { + if (pow($radius - $x, 2) + pow($radius - $y, 2) > pow($radius, 2)) $in_corner = true; + } elseif ($x >= $w - $radius && $y < $radius) { + if (pow($x - ($w - $radius - 1), 2) + pow($radius - $y, 2) > pow($radius, 2)) $in_corner = true; + } elseif ($x < $radius && $y >= $h - $radius) { + if (pow($radius - $x, 2) + pow($y - ($h - $radius - 1), 2) > pow($radius, 2)) $in_corner = true; + } elseif ($x >= $w - $radius && $y >= $h - $radius) { + if (pow($x - ($w - $radius - 1), 2) + pow($y - ($h - $radius - 1), 2) > pow($radius, 2)) $in_corner = true; + } + if ($in_corner) { + imagesetpixel($new_im, $x, $y, $transparent); + } else { + imagesetpixel($new_im, $x, $y, $rgba); + } + } + } + imagedestroy($im); + return $new_im; + } + + private function hex2rgb($hex) { + $hex = str_replace('#', '', $hex); + if (strlen($hex) == 3) { + $r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1)); + $g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1)); + $b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1)); + } else { + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + } + return array($r, $g, $b); + } + + private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) { + $opacity = $pct / 100; + for ($x = 0; $x < $src_w; $x++) { + for ($y = 0; $y < $src_h; $y++) { + $src_color_idx = imagecolorat($src_im, $src_x + $x, $src_y + $y); + $src_color = imagecolorsforindex($src_im, $src_color_idx); + $src_alpha = (127 - $src_color['alpha']) / 127; + $src_alpha = $src_alpha * $opacity; + if ($src_alpha <= 0) { + continue; + } + $dst_x_pos = $dst_x + $x; + $dst_y_pos = $dst_y + $y; + if ($dst_x_pos < 0 || $dst_x_pos >= imagesx($dst_im) || $dst_y_pos < 0 || $dst_y_pos >= imagesy($dst_im)) { + continue; + } + $dst_color_idx = imagecolorat($dst_im, $dst_x_pos, $dst_y_pos); + $dst_color = imagecolorsforindex($dst_im, $dst_color_idx); + $dst_alpha = (127 - $dst_color['alpha']) / 127; + $out_alpha = $src_alpha + $dst_alpha * (1 - $src_alpha); + if ($out_alpha > 0) { + $out_r = ($src_color['red'] * $src_alpha + $dst_color['red'] * $dst_alpha * (1 - $src_alpha)) / $out_alpha; + $out_g = ($src_color['green'] * $src_alpha + $dst_color['green'] * $dst_alpha * (1 - $src_alpha)) / $out_alpha; + $out_b = ($src_color['blue'] * $src_alpha + $dst_color['blue'] * $dst_alpha * (1 - $src_alpha)) / $out_alpha; + $out_alpha_val = 127 - round($out_alpha * 127); + } else { + $out_r = 0; + $out_g = 0; + $out_b = 0; + $out_alpha_val = 127; + } + $out_r = max(0, min(255, round($out_r))); + $out_g = max(0, min(255, round($out_g))); + $out_b = max(0, min(255, round($out_b))); + $out_alpha_val = max(0, min(127, $out_alpha_val)); + $new_color = imagecolorallocatealpha($dst_im, $out_r, $out_g, $out_b, $out_alpha_val); + if ($new_color === false) { + $new_color = imagecolorresolvealpha($dst_im, $out_r, $out_g, $out_b, $out_alpha_val); + } + imagesetpixel($dst_im, $dst_x_pos, $dst_y_pos, $new_color); + } + } + } + + public function scan_small_photos($data) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + $minW = isset($data['module_img_opti_min_width']) ? (int)$data['module_img_opti_min_width'] : 300; + $minH = isset($data['module_img_opti_min_height']) ? (int)$data['module_img_opti_min_height'] : 300; + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile() && in_array(strtolower($file->getExtension()), array('jpg', 'jpeg', 'png', 'webp'))) { + $realPath = realpath($file->getPathname()); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) !== 0) continue; + $size = @getimagesize($realPath); + if ($size && ($size[0] < $minW || $size[1] < $minH)) { + $relPath = $this->getRelativeImagePath($realPath); + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $rows[] = array( + 'id' => 'small_photo-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => $this->language->get('text_grid_too_small') . ': ' . $size[0] . 'x' . $size[1] . ' px', + 'after' => $this->language->get('text_grid_small_photo_after'), + 'actions' => array( + 'delete' => $this->language->get('text_action_delete'), + 'placeholder' => $this->language->get('text_action_placeholder'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'ignore', + 'meta' => array( + 'path' => $relPath + ) + ); + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_small_photos_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => false, + 'quarantine_available' => true, + 'description' => sprintf($this->language->get('text_description_small_photos'), $minW, $minH) + ); + } + + public function fix_small_photos($data) { + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $cache_file = DIR_CACHE . 'img_opti_small_photos_grid.json'; + if (!file_exists($cache_file)) { + return array('error' => $this->language->get('error_no_scan_data')); + } + + $grid = json_decode(file_get_contents($cache_file), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $ph_product_path = DIR_IMAGE . (string)$this->config->get('module_img_opti_ph_product'); + if (!is_file($ph_product_path)) { + $ph_product_path = DIR_IMAGE . 'no_image.png'; + } + + foreach ($selected as $row_id => $act) { + if ($act === 'ignore' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = $row['meta']['path']; + $fullPath = DIR_IMAGE . $relPath; + + if ($act === 'delete') { + $this->quarantineFile($relPath, 'small_photos'); + } elseif ($act === 'placeholder') { + $this->quarantineFile($relPath, 'small_photos'); + if (is_file($ph_product_path)) { + @copy($ph_product_path, $fullPath); + } + } + } + @unlink($cache_file); + return array('success' => true); + } + + public function scan_html_broken($data) { + $this->loadLanguageSafe(); + $rows = array(); + $realImageDir = realpath(DIR_IMAGE); + $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + + $tables = array( + 'product_description' => array('id' => 'product_id', 'field' => 'description', 'name_field' => 'name', 'route' => 'catalog/product/edit', 'param' => 'product_id', 'type' => 'Product'), + 'category_description' => array('id' => 'category_id', 'field' => 'description', 'name_field' => 'name', 'route' => 'catalog/category/edit', 'param' => 'category_id', 'type' => 'Category'), + 'information_description' => array('id' => 'information_id', 'field' => 'description', 'name_field' => 'title', 'route' => 'catalog/information/edit', 'param' => 'information_id', 'type' => 'Information') + ); + + foreach ($tables as $table => $cfg) { + if ($table === 'product_description') { + $query = $this->db->query("SELECT pd.`" . $cfg['id'] . "`, pd.`" . $cfg['field'] . "`, pd.`" . $cfg['name_field'] . "`, p.model FROM `" . DB_PREFIX . $table . "` pd LEFT JOIN `" . DB_PREFIX . "product` p ON (pd.product_id = p.product_id) WHERE pd.language_id = '" . (int)$this->config->get('config_language_id') . "'"); + } else { + $query = $this->db->query("SELECT `" . $cfg['id'] . "`, `" . $cfg['field'] . "`, `" . $cfg['name_field'] . "` FROM `" . DB_PREFIX . $table . "` WHERE language_id = '" . (int)$this->config->get('config_language_id') . "'"); + } + foreach ($query->rows as $row) { + $desc = html_entity_decode($row[$cfg['field']], ENT_QUOTES, 'UTF-8'); + if (preg_match_all('/]+src=(?:\"|\')([^"\']+)(?:\"|\')[^>]*>/i', $desc, $matches)) { + foreach ($matches[1] as $src) { + if (strpos($src, 'image/catalog') !== false) { + $rel_path = preg_replace('/^.*image\/(catalog\/.*)$/i', '$1', $src); + $rel_path = urldecode($rel_path); + $cleanRelPath = str_replace(array('../', '..\\', "\0"), '', $rel_path); + $fullPath = realpath(DIR_IMAGE . $cleanRelPath); + if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) { + $admin_link = $this->url->link($cfg['route'], 'user_token=' . $this->session->data['user_token'] . '&' . $cfg['param'] . '=' . (int)$row[$cfg['id']], true); + $search_links = $this->getSearchLinks($row[$cfg['name_field']], ($table === 'product_description' ? $row['model'] : '')); + $rows[] = array( + 'id' => 'html_broken-' . $table . '-' . (int)$row[$cfg['id']] . '-' . md5($cleanRelPath), + 'preview' => '', + 'name' => '' . $cfg['type'] . ' ' . htmlspecialchars($row[$cfg['name_field']] ? $row[$cfg['name_field']] : $cfg['type'] . ' ID ' . (int)$row[$cfg['id']], ENT_QUOTES, 'UTF-8') . '' . $search_links, + 'before' => $this->language->get('text_grid_missing_html') . ': ' . htmlspecialchars($cleanRelPath, ENT_QUOTES, 'UTF-8'), + 'after' => $this->language->get('text_grid_html_after'), + 'actions' => array( + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'ignore' + ); + } + } + } + } + } + } + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => false, + 'quarantine_available' => false, + 'description' => $this->language->get('text_description_html_broken') + ); + } + + public function scan_folder_tree($data) { + $this->loadLanguageSafe(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $sizes = array(); + $img_sizes = array(); + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) !== 0) continue; + $folderPath = $this->mb_dirname($realPath); + $rel = ltrim(str_replace(str_replace('\\', '/', $realImageDir), '', str_replace('\\', '/', $folderPath)), '/'); + if (!isset($sizes[$rel])) $sizes[$rel] = 0; + $sizes[$rel] += filesize($realPath); + + $ext = strtolower($file->getExtension()); + if (in_array($ext, array('jpg', 'jpeg', 'png', 'webp'))) { + if (!isset($img_sizes[$rel])) $img_sizes[$rel] = 0; + $img_sizes[$rel] += filesize($realPath); + } + } + } + arsort($sizes); + + $rows = array(); + foreach ($sizes as $folder => $size) { + $img_sz = isset($img_sizes[$folder]) ? $img_sizes[$folder] : 0; + $estimated_size = ($size - $img_sz) + ($img_sz * 0.65); + $after_text = $this->formatBytes($estimated_size) . ' (~' . $this->language->get('text_estimated') . ')'; + + $rows[] = array( + 'id' => 'folder-' . md5($folder), + 'preview' => '', + 'name' => '' . htmlspecialchars($folder, ENT_QUOTES, 'UTF-8') . '', + 'before' => $this->formatBytes($size), + 'after' => $after_text, + 'actions' => array( + 'ignore' => $this->language->get('text_action_ignore'), + 'compress' => $this->language->get('text_action_compress'), + 'archive' => $this->language->get('text_action_archive'), + 'delete' => $this->language->get('text_action_delete'), + 'archive_delete' => $this->language->get('text_action_archive_delete') + ), + 'selected_action' => 'ignore', + 'meta' => array( + 'path' => $folder + ) + ); + } + + file_put_contents(DIR_CACHE . 'img_opti_folder_tree_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => false, + 'quarantine_available' => false, + 'description' => $this->language->get('text_description_folder_tree') + ); + } + + public function fix_folder_tree($data) { + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $cache_file = DIR_CACHE . 'img_opti_folder_tree_grid.json'; + if (!file_exists($cache_file)) { + return array('error' => $this->language->get('error_no_scan_data')); + } + + $grid = json_decode(file_get_contents($cache_file), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $backup_dir = DIR_IMAGE . 'catalog_trash/backup/'; + if (!is_dir($backup_dir)) { + @mkdir($backup_dir, 0755, true); + } + $this->registerModuleFolder('catalog_trash/backup'); + + foreach ($selected as $row_id => $act) { + if ($act === 'ignore' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = $row['meta']['path']; + $fullPath = realpath(DIR_IMAGE . $relPath); + $realImageDir = realpath(DIR_IMAGE); + if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($fullPath)) continue; + + if ($relPath === 'catalog' || $relPath === 'catalog/') continue; + + if ($act === 'archive' || $act === 'archive_delete') { + $zipName = 'archive_folder_' . str_replace('/', '_', trim($relPath, '/')) . '_' . date('Ymd_His') . '.zip'; + $zipPath = $backup_dir . $zipName; + $this->zipDirectory($fullPath, $zipPath); + } + + if ($act === 'delete' || $act === 'archive_delete') { + $this->clearDbLinksForFolder($fullPath); + $this->cleanDir($fullPath); + } + + if ($act === 'compress') { + $maxWidth = (int)$this->config->get('module_img_opti_max_width') ?: 1600; + $maxHeight = (int)$this->config->get('module_img_opti_max_height') ?: 1600; + $jpgQ = (int)$this->config->get('module_img_opti_jpg_quality') ?: 75; + $pngQ = (int)$this->config->get('module_img_opti_png_quality') ?: 7; + $webpQ = (int)$this->config->get('module_img_opti_webp_quality') ?: 80; + $engine = $this->config->get('module_img_opti_engine') === 'imagick' && extension_loaded('imagick') ? 'imagick' : 'gd'; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($fullPath, RecursiveDirectoryIterator::SKIP_DOTS)); + foreach ($iterator as $file) { + $limit = $this->getMemoryLimitInBytesHelper(); + if ($limit > 0) { + if (memory_get_usage(true) > $limit * 0.85) { + break; + } + } + if ($file->isFile()) { + $ext = strtolower($file->getExtension()); + if (in_array($ext, array('jpg', 'jpeg', 'png', 'webp'))) { + $filePath = $file->getPathname(); + $tempPath = $filePath . '.tmp'; + if ($this->optimizeImageHelper($filePath, $tempPath, $maxWidth, $maxHeight, $jpgQ, $pngQ, $webpQ, $engine)) { + if (file_exists($tempPath) && filesize($tempPath) > 0 && filesize($tempPath) < filesize($filePath)) { + @unlink($filePath); + @rename($tempPath, $filePath); + if (file_exists($tempPath)) { + @unlink($tempPath); + } + } else { + @unlink($tempPath); + } + } + } + } + } + } + } + @unlink($cache_file); + return array('success' => true); + } + + private function clearDbLinksForFolder($dir) { + $realDir = realpath($dir); + $realImageDir = realpath(DIR_IMAGE); + if (!$realDir || strpos(str_replace('\\', '/', $realDir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($realDir)) return; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($realDir, RecursiveDirectoryIterator::SKIP_DOTS)); + foreach ($iterator as $file) { + if ($file->isFile()) { + $filePath = str_replace('\\', '/', $file->getPathname()); + $relPath = ltrim(str_replace(str_replace('\\', '/', $realImageDir), '', $filePath), '/'); + $escaped = $this->db->escape($relPath); + + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '' WHERE image = '" . $escaped . "'"); + $this->db->query("DELETE FROM `" . DB_PREFIX . "product_image` WHERE image = '" . $escaped . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '' WHERE image = '" . $escaped . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '' WHERE image = '" . $escaped . "'"); + + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $this->db->query("DELETE FROM `" . DB_PREFIX . "banner_image` WHERE image = '" . $escaped . "'"); + } + + $blog_tables = array( + 'oct_blog_article' => 'image', + 'simple_blog_article' => 'image', + 'newsblog_article' => 'image', + 'information' => 'image' + ); + foreach ($blog_tables as $table => $column) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE '" . $column . "'"); + if ($column_query->num_rows) { + try { + $this->db->query("UPDATE `" . DB_PREFIX . $table . "` SET `" . $column . "` = '' WHERE `" . $column . "` = '" . $escaped . "'"); + } catch (Exception $e) {} + } + } + } + } + } + } + + private function zipDirectory($source, $destination) { + if (!class_exists('ZipArchive')) { + return false; + } + $zip = new ZipArchive(); + if (!$zip->open($destination, ZipArchive::CREATE | ZipArchive::OVERWRITE)) { + return false; + } + $source = str_replace('\\', '/', realpath($source)); + if (is_dir($source) === true) { + $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($source), RecursiveIteratorIterator::LEAVES_ONLY); + foreach ($files as $name => $file) { + if (!$file->isDir()) { + $filePath = $file->getRealPath(); + $relativePath = substr($filePath, strlen($source) + 1); + $zip->addFile($filePath, $relativePath); + } + } + } elseif (is_file($source) === true) { + $zip->addFromString($this->mb_basename($source), file_get_contents($source)); + } + return $zip->close(); + } + + + private function transliterate($text) { + $cyr = array('а','б','в','г','д','е','ё','ж','з','и','й','к','л','м','н','о','п','р','с','т','у','ф','х','ц','ч','ш','щ','ъ','ы','ь','э','ю','я','А','Б','В','Г','Д','Е','Ё','Ж','З','И','Й','К','Л','М','Н','О','П','Р','С','Т','У','Ф','Х','Ц','Ч','Ш','Щ','Ъ','Ы','Ь','Э','Ю','Я','є','і','ї','ґ','Є','І','Ї','Ґ','’',' ','!','@','#','$','%','^','&','*','(',')','+','=','[',']','{','}','|','\\',':',';','"',"'",'<','>','?','/','~','`',','); + $lat = array('a','b','v','g','d','e','e','zh','z','i','y','k','l','m','n','o','p','r','s','t','u','f','h','ts','ch','sh','sch','','y','','e','yu','ya','A','B','V','G','D','E','E','Zh','Z','I','Y','K','L','M','N','O','P','R','S','T','U','F','H','Ts','Ch','Sh','Sch','','Y','','E','Yu','Ya','ye','i','yi','g','Ye','I','Yi','G','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_','_'); + $text = str_replace($cyr, $lat, (string)$text); + $text = preg_replace('/_+/', '_', $text); + return strtolower(trim($text, '_')); + } + + private function formatBytes($bytes) { + if ($bytes >= 1048576) return round($bytes / 1048576, 2) . ' MB'; + if ($bytes >= 1024) return round($bytes / 1024, 0) . ' KB'; + return $bytes . ' B'; + } + + public function generateCompareUrls($tool, $id) { + $this->loadLanguageSafe(); + + $original_path = ''; + $type = ''; + + if ($tool === 'png_jpg') { + $grid_file = DIR_CACHE . 'img_opti_png_jpg_grid.json'; + if (!file_exists($grid_file)) { + return array('error' => $this->language->get('error_no_scan_data')); + } + $grid = json_decode(file_get_contents($grid_file), true); + $found = false; + foreach ($grid as $row) { + if ($row['id'] === $id && isset($row['meta']['path'])) { + $original_path = $row['meta']['path']; + $type = 'png_jpg'; + $found = true; + break; + } + } + if (!$found) { + return array('error' => $this->language->get('error_row_not_found')); + } + } elseif ($tool === 'watermark') { + if (is_file(DIR_IMAGE . $id)) { + $original_path = $id; + $type = 'watermark'; + } + } elseif ($tool === 'optimization' || $tool === 'compress') { + if (is_file(DIR_IMAGE . $id)) { + $original_path = $id; + $type = 'compress'; + } + } + + if (empty($original_path) || !is_file(DIR_IMAGE . $original_path)) { + $clean_id = str_replace(array('../', '..\\', "\0"), '', $id); + if (is_file(DIR_IMAGE . $clean_id)) { + $original_path = $clean_id; + if (empty($type)) { + $type = 'compress'; + } + } else { + return array('error' => $this->language->get('error_file_not_found') ?: 'File not found'); + } + } + + $ext = strtolower($this->mb_pathinfo($original_path, PATHINFO_EXTENSION)); + $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + + $compare_dir = DIR_IMAGE . 'cache/compare/'; + if (!is_dir($compare_dir)) { + @mkdir($compare_dir, 0755, true); + } + + $file_hash = md5($original_path . time()); + $temp_before_name = 'before_' . $file_hash . '.' . $ext; + $temp_before_path = $compare_dir . $temp_before_name; + + @copy(DIR_IMAGE . $original_path, $temp_before_path); + + $temp_after_name = 'after_' . $file_hash . '.'; + if ($type === 'png_jpg') { + $temp_after_name .= 'jpg'; + } else { + $temp_after_name .= $ext; + } + $temp_after_path = $compare_dir . $temp_after_name; + + if ($type === 'png_jpg') { + $this->convertPngToJpgHelper(DIR_IMAGE . $original_path, $temp_after_path); + } elseif ($type === 'watermark') { + @copy(DIR_IMAGE . $original_path, $temp_after_path); + $this->applyDynamicWatermark($temp_after_path, $original_path); + } else { + $maxWidth = 1600; + $maxHeight = 1600; + $jpgQ = (int)$this->config->get('module_img_opti_jpg_quality') ?: 75; + $pngQ = (int)$this->config->get('module_img_opti_png_quality') ?: 7; + $webpQ = (int)$this->config->get('module_img_opti_webp_quality') ?: 80; + $engine = $this->config->get('module_img_opti_engine') === 'imagick' && extension_loaded('imagick') ? 'imagick' : 'gd'; + + if (isset($this->request->post['max_width'])) $maxWidth = (int)$this->request->post['max_width']; + if (isset($this->request->post['max_height'])) $maxHeight = (int)$this->request->post['max_height']; + if (isset($this->request->post['jpg_quality'])) $jpgQ = (int)$this->request->post['jpg_quality']; + if (isset($this->request->post['png_quality'])) $pngQ = (int)$this->request->post['png_quality']; + if (isset($this->request->post['webp_quality'])) $webpQ = (int)$this->request->post['webp_quality']; + + $this->optimizeImageHelper(DIR_IMAGE . $original_path, $temp_after_path, $maxWidth, $maxHeight, $jpgQ, $pngQ, $webpQ, $engine); + } + + $url_before = $catalog_url . 'image/cache/compare/' . $temp_before_name; + $url_after = $catalog_url . 'image/cache/compare/' . $temp_after_name; + + $size_before = filesize(DIR_IMAGE . $original_path); + $size_after = file_exists($temp_after_path) ? filesize($temp_after_path) : 0; + + return array( + 'success' => true, + 'before' => $url_before, + 'after' => $url_after, + 'size_before' => $this->formatBytes($size_before), + 'size_after' => $this->formatBytes($size_after), + 'saving' => ($size_before > 0) ? round((($size_before - $size_after) / $size_before) * 100, 1) : 0 + ); + } + + public function convertPngToJpgHelper($src, $dest) { + $img = @imagecreatefrompng($src); + if ($img) { + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + imagealphablending($img, true); + $bg = imagecreatetruecolor(imagesx($img), imagesy($img)); + imagefill($bg, 0, 0, imagecolorallocate($bg, 255, 255, 255)); + imagecopy($bg, $img, 0, 0, 0, 0, imagesx($img), imagesy($img)); + $res = imagejpeg($bg, $dest, 85); + imagedestroy($img); + imagedestroy($bg); + return $res; + } + return false; + } + + public function optimizeImageHelper($src, $dest, $maxWidth, $maxHeight, $jpgQuality, $pngCompression, $webpQuality, $engine = 'gd') { + if ($engine == 'imagick' && extension_loaded('imagick')) { + try { + $img = new Imagick($src); + $img->transformImageColorspace(Imagick::COLORSPACE_SRGB); + $width = $img->getImageWidth(); + $height = $img->getImageHeight(); + $ext = strtolower($this->mb_pathinfo($src, PATHINFO_EXTENSION)); + if (($maxWidth > 0 && $width > $maxWidth) || ($maxHeight > 0 && $height > $maxHeight)) { + $targetW = $maxWidth; + $targetH = $maxHeight; + if ($targetW == 0) { + $targetW = (int)round($width * ($targetH / $height)); + } + if ($targetH == 0) { + $targetH = (int)round($height * ($targetW / $width)); + } + $img->resizeImage($targetW, $targetH, Imagick::FILTER_LANCZOS, 1, true); + } + $img->stripImage(); + if ($ext === 'png') { + $img->setImageFormat('png'); + $img->setCompressionQuality($pngCompression * 10); + } elseif ($ext === 'webp') { + $img->setImageFormat('webp'); + $img->setCompressionQuality($webpQuality); + } else { + $img->setImageFormat('jpeg'); + $img->setImageCompression(Imagick::COMPRESSION_JPEG); + $img->setCompressionQuality($jpgQuality); + } + return $img->writeImage($dest); + } catch (Exception $e) { + $engine = 'gd'; + } + } + + if ($engine == 'gd') { + $img = @imagecreatefromstring(file_get_contents($src)); + if (!$img) return false; + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + $width = imagesx($img); + $height = imagesy($img); + $ext = strtolower($this->mb_pathinfo($src, PATHINFO_EXTENSION)); + if ($ext === 'png' || $ext === 'webp') { + imagealphablending($img, false); + imagesavealpha($img, true); + } + if (($maxWidth > 0 && $width > $maxWidth) || ($maxHeight > 0 && $height > $maxHeight)) { + $ratioX = ($maxWidth > 0) ? ($maxWidth / $width) : 1; + $ratioY = ($maxHeight > 0) ? ($maxHeight / $height) : 1; + $ratio = min($ratioX, $ratioY); + $newW = (int)($width * $ratio); + $newH = (int)($height * $ratio); + $newImg = imagecreatetruecolor($newW, $newH); + if ($ext === 'png' || $ext === 'webp') { + imagealphablending($newImg, false); + imagesavealpha($newImg, true); + $transparent = imagecolorallocatealpha($newImg, 255, 255, 255, 127); + imagefill($newImg, 0, 0, $transparent); + } else { + $white = imagecolorallocate($newImg, 255, 255, 255); + imagefill($newImg, 0, 0, $white); + } + imagecopyresampled($newImg, $img, 0, 0, 0, 0, $newW, $newH, $width, $height); + imagedestroy($img); + $img = $newImg; + } + $res = false; + if ($ext === 'png') { + $res = imagepng($img, $dest, $pngCompression); + } elseif ($ext === 'webp' && function_exists('imagewebp')) { + $res = imagewebp($img, $dest, $webpQuality); + } else { + $res = imagejpeg($img, $dest, $jpgQuality); + } + imagedestroy($img); + return $res; + } + return false; + } + + private function safe_utf8($str) { + $str = (string)$str; + if (mb_check_encoding($str, 'UTF-8')) { + return $str; + } + $converted = @mb_convert_encoding($str, 'UTF-8', 'Windows-1251'); + return $converted ? $converted : $str; + } + + public function scan_sanitizer($data = array()) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $limit = isset($data['module_img_opti_sanitizer_limit']) ? (int)$data['module_img_opti_sanitizer_limit'] : 100; + if ($limit <= 0) $limit = 100; + + $sz_translit = isset($data['module_img_opti_sz_translit']) ? (int)$data['module_img_opti_sz_translit'] : 1; + $sz_spaces = isset($data['module_img_opti_sz_spaces']) ? (int)$data['module_img_opti_sz_spaces'] : 1; + $sz_special = isset($data['module_img_opti_sz_special']) ? (int)$data['module_img_opti_sz_special'] : 1; + $sz_lowercase_name = isset($data['module_img_opti_sz_lowercase_name']) ? (int)$data['module_img_opti_sz_lowercase_name'] : 1; + $sz_lowercase_ext = isset($data['module_img_opti_sz_lowercase_ext']) ? (int)$data['module_img_opti_sz_lowercase_ext'] : 1; + $sz_normalize_ext = isset($data['module_img_opti_sz_normalize_ext']) ? (int)$data['module_img_opti_sz_normalize_ext'] : 1; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + $assigned_paths = array(); + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $relPath = $this->getRelativeImagePath($realPath); + $filename = $this->mb_basename($relPath); + $proposedRel = $this->sanitizeRelativePath($relPath, $sz_translit, $sz_spaces, $sz_special, $sz_lowercase_name, $sz_lowercase_ext, $sz_normalize_ext); + + if ($relPath !== $proposedRel) { + $proposedFullPath = DIR_IMAGE . $proposedRel; + if (file_exists($proposedFullPath) || isset($assigned_paths[$proposedRel]) || $this->isPathUsedInDb($proposedRel)) { + $ext = $this->mb_pathinfo($proposedRel, PATHINFO_EXTENSION); + $filename_no_ext = $this->mb_pathinfo($proposedRel, PATHINFO_FILENAME); + $dirname = $this->mb_pathinfo($proposedRel, PATHINFO_DIRNAME); + $dir_prefix = ($dirname === '.' || $dirname === '') ? '' : $dirname . '/'; + + $counter = 1; + while (true) { + $candidate = $dir_prefix . $filename_no_ext . '_' . $counter . ($ext !== '' ? '.' . $ext : ''); + if (!file_exists(DIR_IMAGE . $candidate) && !isset($assigned_paths[$candidate]) && !$this->isPathUsedInDb($candidate)) { + $proposedRel = $candidate; + break; + } + $counter++; + } + } + + $assigned_paths[$proposedRel] = true; + + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $rows[] = array( + 'id' => 'sanitizer-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => $filename, + 'after' => $this->mb_basename($proposedRel), + 'actions' => array( + 'rename' => $this->language->get('text_action_apply_changes'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'rename', + 'meta' => array( + 'path' => $relPath, + 'new_path' => $proposedRel + ) + ); + if (count($rows) >= $limit) { + break; + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_sanitizer_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => true, + 'description' => $this->language->get('text_description_sanitizer') + ); + } + + public function fix_sanitizer($data) { + $this->backupCatalogTables('sanitizer'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_sanitizer_grid.json'), true); + $rows_map = array(); + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + + $realImageDir = realpath(DIR_IMAGE); + + foreach ($selected as $row_id => $act) { + if ($act !== 'rename' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $cleanBadFile = str_replace(array('../', '..\\', "\0"), '', (string)$row['meta']['path']); + $fullPath = realpath(DIR_IMAGE . $cleanBadFile); + if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) continue; + + $newRel = (string)$row['meta']['new_path']; + $newFullPathStr = DIR_IMAGE . $newRel; + $newDirStr = $this->mb_dirname($newFullPathStr); + if (!is_dir($newDirStr)) { + @mkdir($newDirStr, 0755, true); + } + $realNewDir = realpath($newDirStr); + if (!$realNewDir || strpos(str_replace('\\', '/', $realNewDir), str_replace('\\', '/', $realImageDir)) !== 0) continue; + $newFullPath = $realNewDir . DIRECTORY_SEPARATOR . $this->mb_basename($newFullPathStr); + + if (@copy($fullPath, $newFullPath)) { + $dbOld = $this->db->escape($cleanBadFile); + $dbNew = $this->db->escape($newRel); + + $this->db->query("UPDATE `" . DB_PREFIX . "product` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "product_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "category` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + $this->db->query("UPDATE `" . DB_PREFIX . "manufacturer` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $this->db->query("UPDATE `" . DB_PREFIX . "banner_image` SET image = '" . $dbNew . "' WHERE image = '" . $dbOld . "'"); + } + + $blog_tables = array( + 'oct_blog_article' => 'image', + 'simple_blog_article' => 'image', + 'newsblog_article' => 'image', + 'information' => 'image' + ); + foreach ($blog_tables as $table => $column) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE '" . $column . "'"); + if ($column_query->num_rows) { + try { + $this->db->query("UPDATE `" . DB_PREFIX . $table . "` SET `" . $column . "` = '" . $dbNew . "' WHERE `" . $column . "` = '" . $dbOld . "'"); + } catch (Exception $e) {} + } + } + } + + $this->quarantineFile($cleanBadFile, 'sanitizer'); + } + } + @unlink(DIR_CACHE . 'img_opti_sanitizer_grid.json'); + return array('success' => true); + } + + private function sanitizeRelativePath($relPath, $sz_translit, $sz_spaces, $sz_special, $sz_lowercase_name, $sz_lowercase_ext, $sz_normalize_ext) { + $parts = explode('/', str_replace('\\', '/', $relPath)); + $newParts = array(); + for ($i = 0; $i < count($parts) - 1; $i++) { + $part = $parts[$i]; + if ($part === 'catalog' || $part === '') { + $newParts[] = $part; + } else { + $newParts[] = $this->sanitizePathSegment($part, $sz_translit, $sz_spaces, $sz_special, $sz_lowercase_name); + } + } + $filename = end($parts); + if ($filename !== '') { + $ext = $this->mb_pathinfo($filename, PATHINFO_EXTENSION); + $nameOnly = $this->mb_pathinfo($filename, PATHINFO_FILENAME); + $newName = $this->sanitizePathSegment($nameOnly, $sz_translit, $sz_spaces, $sz_special, $sz_lowercase_name); + $newExt = $ext; + if ($sz_lowercase_ext) { + $newExt = function_exists('mb_strtolower') ? mb_strtolower($newExt, 'UTF-8') : strtolower($newExt); + } + if ($sz_normalize_ext) { + if (strtolower($newExt) === 'jpeg') { + $newExt = 'jpg'; + } + } + $cleanName = $newName . ($newExt !== '' ? '.' . $newExt : ''); + $newParts[] = $cleanName; + } + return implode('/', $newParts); + } + + private function sanitizePathSegment($segment, $sz_translit, $sz_spaces, $sz_special, $sz_lowercase) { + $newSegment = $segment; + if ($sz_translit) { + $newSegment = $this->transliterate($newSegment); + } + if ($sz_spaces) { + $newSegment = str_replace(' ', '_', $newSegment); + } + if ($sz_special) { + $newSegment = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $newSegment); + } + if ($sz_lowercase) { + $newSegment = function_exists('mb_strtolower') ? mb_strtolower($newSegment, 'UTF-8') : strtolower($newSegment); + } + $newSegment = preg_replace('/_+/', '_', $newSegment); + $newSegment = preg_replace('/\-+/', '-', $newSegment); + $newSegment = trim($newSegment, '_-'); + + if ($newSegment === '') { + $fallback = $this->transliterate($segment); + if ($sz_special) { + $fallback = preg_replace('/[^a-zA-Z0-9_\-\.]/', '', $fallback); + } + if ($sz_lowercase) { + $fallback = function_exists('mb_strtolower') ? mb_strtolower($fallback, 'UTF-8') : strtolower($fallback); + } + $fallback = trim(preg_replace('/_+/', '_', str_replace(' ', '_', $fallback)), '_-'); + if ($fallback !== '') { + $newSegment = $fallback; + } else { + $newSegment = 'img_' . substr(md5($segment), 0, 8); + } + } + return $newSegment; + } + + private function translate($key, $default = '') { + if (isset($this->translations[$key])) { + return $this->translations[$key]; + } + return $default ? $default : $key; + } + + private function loadLanguageSafe() { + $data = array(); + $en_paths = array( + DIR_LANGUAGE . 'en-gb/extension/module/img_opti.php', + DIR_LANGUAGE . 'english/extension/module/img_opti.php' + ); + foreach ($en_paths as $path) { + if (is_file($path)) { + $_ = array(); + require($path); + $data = array_merge($data, $_); + break; + } + } + $lang = $this->config->get('config_admin_language'); + if (!$lang) { + $lang = $this->config->get('config_language'); + } + if (isset($this->session->data['language'])) { + $lang = $this->session->data['language']; + } + if (!$lang) { + $lang = 'en-gb'; + } + if ($lang && $lang !== 'en-gb' && $lang !== 'english') { + $paths = array( + DIR_LANGUAGE . $lang . '/extension/module/img_opti.php' + ); + if (strpos($lang, 'ru') === 0) { + $paths[] = DIR_LANGUAGE . 'ru-ru/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'russian/extension/module/img_opti.php'; + } elseif (strpos($lang, 'uk') === 0 || strpos($lang, 'ua') === 0) { + $paths[] = DIR_LANGUAGE . 'uk-ua/extension/module/img_opti.php'; + $paths[] = DIR_LANGUAGE . 'ukrainian/extension/module/img_opti.php'; + } + foreach ($paths as $path) { + if (is_file($path)) { + $_ = array(); + require($path); + $data = array_merge($data, $_); + break; + } + } + } + foreach ($data as $key => $value) { + $this->language->set($key, $value); + } + $this->translations = $data; + return $data; + } + + private function getMemoryLimitInBytesHelper() { + $limit = ini_get('memory_limit'); + if (!$limit || (int)$limit === -1) return 512 * 1024 * 1024; + $unit = strtolower(substr($limit, -1)); + $bytes = (int)$limit; + switch ($unit) { + case 'g': $bytes *= 1024; + case 'm': $bytes *= 1024; + case 'k': $bytes *= 1024; + } + return $bytes; + } + + public function scan_cmyk($data) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + $this->load->model('tool/image'); + + foreach ($iterator as $file) { + if ($file->isFile() && in_array(strtolower($file->getExtension()), array('jpg', 'jpeg'))) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $imgSize = @getimagesize($realPath); + if ($imgSize) { + $is_cmyk = false; + if (isset($imgSize['channels']) && $imgSize['channels'] == 4) { + $is_cmyk = true; + } + if (extension_loaded('imagick')) { + try { + $img = new Imagick($realPath); + if ($img->getImageColorspace() == Imagick::COLORSPACE_CMYK) { + $is_cmyk = true; + } + $img->clear(); $img->destroy(); + } catch (Exception $e) {} + } + if ($is_cmyk) { + $relPath = $this->getRelativeImagePath($realPath); + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + $size = filesize($realPath); + $rows[] = array( + 'id' => 'cmyk-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => 'CMYK', + 'after' => 'sRGB', + 'actions' => array( + 'convert' => $this->translate('text_action_convert_rgb', 'Convert to RGB'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'convert', + 'meta' => array( + 'path' => $relPath + ) + ); + } + } + } + } + } + + file_put_contents(DIR_CACHE . 'img_opti_cmyk_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => false, + 'description' => $this->translate('text_description_cmyk', 'Found JPEGs in CMYK colorspace. Conversion to sRGB ensures they are displayed correctly on all platforms, including iOS/macOS.') + ); + } + + public function fix_cmyk($data) { + $this->backupCatalogTables('cmyk'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_cmyk_grid.json'), true); + $rows_map = array(); + if ($grid) { + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + } + + $realImageDir = realpath(DIR_IMAGE); + + foreach ($selected as $row_id => $act) { + if ($act !== 'convert' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = str_replace(array('../', '..\\', "\0"), '', (string)$row['meta']['path']); + $fullPath = realpath(DIR_IMAGE . $relPath); + if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) continue; + + if (extension_loaded('imagick')) { + try { + $img = new Imagick($fullPath); + if ($img->getImageColorspace() == Imagick::COLORSPACE_CMYK) { + $img->transformImageColorspace(Imagick::COLORSPACE_SRGB); + $img->writeImage($fullPath); + $this->deleteImageCache($relPath); + } + $img->clear(); $img->destroy(); + } catch (Exception $e) {} + } + } + @unlink(DIR_CACHE . 'img_opti_cmyk_grid.json'); + return array('success' => true); + } + + public function scan_heavy_files($data) { + $this->loadLanguageSafe(); + $rows = array(); + $dir = realpath(DIR_IMAGE . 'catalog'); + $realImageDir = realpath(DIR_IMAGE); + if (!$dir || strpos(str_replace('\\', '/', $dir), str_replace('\\', '/', $realImageDir)) !== 0 || !is_dir($dir)) { + return array('error' => $this->language->get('text_err_dir')); + } + + $limit = isset($data['module_img_opti_heavy_limit']) ? (int)$data['module_img_opti_heavy_limit'] : 20; + if ($limit <= 0) $limit = 20; + + $min_size = isset($data['module_img_opti_heavy_min_size']) ? (int)$data['module_img_opti_heavy_min_size'] : 500; + if ($min_size < 0) $min_size = 500; + $min_size_bytes = $min_size * 1024; + + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + + $all_files = array(); + foreach ($iterator as $file) { + if ($file->isFile() && in_array(strtolower($file->getExtension()), array('jpg', 'jpeg', 'png', 'webp'))) { + $realPath = realpath($file->getPathname()); + if ($realPath && strpos(str_replace('\\', '/', $realPath), str_replace('\\', '/', $dir)) === 0) { + $size = filesize($realPath); + if ($size >= $min_size_bytes) { + $all_files[$realPath] = $size; + } + } + } + } + + arsort($all_files); + $top_files = array_slice($all_files, 0, $limit, true); + + $this->load->model('tool/image'); + + foreach ($top_files as $realPath => $size) { + $relPath = $this->getRelativeImagePath($realPath); + $thumb = $this->model_tool_image->resize($relPath, 100, 100); + $entities = $this->getEntitiesByImagePath($relPath); + $entities_html = !empty($entities) ? implode('
    ', $entities) : '' . $this->language->get('text_no_relations') . ''; + + $w = 0; $h = 0; + $imgSize = @getimagesize($realPath); + if ($imgSize) { + $w = $imgSize[0]; + $h = $imgSize[1]; + } + + $rows[] = array( + 'id' => 'heavy-' . md5($relPath), + 'preview' => '', + 'name' => '' . htmlspecialchars($relPath, ENT_QUOTES, 'UTF-8') . '
    ' . $entities_html, + 'before' => $this->formatBytes($size) . ($w ? ' (' . $w . 'x' . $h . 'px)' : ''), + 'after' => $this->translate('text_action_compress', 'Compress'), + 'actions' => array( + 'compress' => $this->translate('text_action_compress', 'Compress'), + 'ignore' => $this->language->get('text_action_ignore') + ), + 'selected_action' => 'compress', + 'meta' => array( + 'path' => $relPath + ) + ); + } + + file_put_contents(DIR_CACHE . 'img_opti_heavy_files_grid.json', json_encode($rows)); + + return array( + 'success' => true, + 'headers' => array( + $this->language->get('text_th_preview'), + $this->language->get('text_th_entity'), + $this->language->get('text_th_before'), + $this->language->get('text_th_after'), + $this->language->get('text_th_action') + ), + 'rows' => $rows, + 'backup_available' => true, + 'quarantine_available' => false, + 'description' => sprintf($this->translate('text_description_heavy', 'List of the top %d heaviest files in the image directory. You can optimize them here directly.'), $limit) + ); + } + + public function fix_heavy_files($data) { + $this->backupCatalogTables('heavy_files'); + $selected = isset($data['selected']) ? (array)$data['selected'] : array(); + $fix_all = isset($data['fix_all']) ? (int)$data['fix_all'] : 0; + + $grid = json_decode(file_get_contents(DIR_CACHE . 'img_opti_heavy_files_grid.json'), true); + $rows_map = array(); + if ($grid) { + foreach ($grid as $row) { + $rows_map[$row['id']] = $row; + if ($fix_all === 1 || empty($selected)) { + $selected[$row['id']] = $row['selected_action']; + } + } + } + + $realImageDir = realpath(DIR_IMAGE); + + $maxWidth = (int)$this->config->get('module_img_opti_max_width'); + if (!$maxWidth) $maxWidth = 1600; + $maxHeight = (int)$this->config->get('module_img_opti_max_height'); + if (!$maxHeight) $maxHeight = 1600; + $jpgQ = (int)$this->config->get('module_img_opti_jpg_quality'); + if (!$jpgQ) $jpgQ = 72; + $pngQ = (int)$this->config->get('module_img_opti_png_quality'); + if (!$pngQ) $pngQ = 7; + $webpQ = (int)$this->config->get('module_img_opti_webp_quality'); + if (!$webpQ) $webpQ = 80; + $engine = $this->config->get('module_img_opti_engine') == 'imagick' && extension_loaded('imagick') ? 'imagick' : 'gd'; + + foreach ($selected as $row_id => $act) { + if ($act !== 'compress' || !isset($rows_map[$row_id])) continue; + $row = $rows_map[$row_id]; + $relPath = str_replace(array('../', '..\\', "\0"), '', (string)$row['meta']['path']); + $fullPath = realpath(DIR_IMAGE . $relPath); + if (!$fullPath || strpos(str_replace('\\', '/', $fullPath), str_replace('\\', '/', $realImageDir)) !== 0 || !is_file($fullPath)) continue; + + $tempPath = DIR_CACHE . 'opti_temp_heavy_' . md5($fullPath); + if ($this->optimizeImageHelper($fullPath, $tempPath, $maxWidth, $maxHeight, $jpgQ, $pngQ, $webpQ, $engine)) { + if (file_exists($tempPath)) { + @unlink($fullPath); + if (@rename($tempPath, $fullPath)) { + $this->deleteImageCache($relPath); + } else { + if (@copy($tempPath, $fullPath)) { + @unlink($tempPath); + $this->deleteImageCache($relPath); + } + } + } + } + } + @unlink(DIR_CACHE . 'img_opti_heavy_files_grid.json'); + return array('success' => true); + } + + private function deleteImageCache($relPath) { + $info = $this->mb_pathinfo($relPath); + if (!isset($info['extension']) || !isset($info['dirname'])) return; + $dir = DIR_IMAGE . 'cache/' . $info['dirname']; + if (!is_dir($dir)) return; + $filename = $info['filename']; + $ext = $info['extension']; + + $files = glob($dir . '/' . $filename . '-*x*.' . $ext); + if ($files) { + foreach ($files as $file) { + @unlink($file); + } + } + $files_webp = glob($dir . '/' . $filename . '-*x*.webp'); + if ($files_webp) { + foreach ($files_webp as $file) { + @unlink($file); + } + } + } + + private function mb_pathinfo($path, $options = null) { + $ret = array('dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''); + $path = str_replace('\\', '/', $path); + $last_slash = strrpos($path, '/'); + if ($last_slash !== false) { + $ret['dirname'] = substr($path, 0, $last_slash); + $basename = substr($path, $last_slash + 1); + } else { + $basename = $path; + } + $ret['basename'] = $basename; + $last_dot = strrpos($basename, '.'); + if ($last_dot !== false) { + $ret['filename'] = substr($basename, 0, $last_dot); + $ret['extension'] = substr($basename, $last_dot + 1); + } else { + $ret['filename'] = $basename; + } + if ($options === PATHINFO_DIRNAME) return $ret['dirname']; + if ($options === PATHINFO_BASENAME) return $ret['basename']; + if ($options === PATHINFO_EXTENSION) return $ret['extension']; + if ($options === PATHINFO_FILENAME) return $ret['filename']; + return $ret; + } + + private function isPathUsedInDb($path) { + $escaped = $this->db->escape((string)$path); + $query = $this->db->query("SELECT product_id FROM `" . DB_PREFIX . "product` WHERE image = '" . $escaped . "' LIMIT 1"); + if ($query->num_rows) return true; + $query = $this->db->query("SELECT product_id FROM `" . DB_PREFIX . "product_image` WHERE image = '" . $escaped . "' LIMIT 1"); + if ($query->num_rows) return true; + $query = $this->db->query("SELECT category_id FROM `" . DB_PREFIX . "category` WHERE image = '" . $escaped . "' LIMIT 1"); + if ($query->num_rows) return true; + $query = $this->db->query("SELECT manufacturer_id FROM `" . DB_PREFIX . "manufacturer` WHERE image = '" . $escaped . "' LIMIT 1"); + if ($query->num_rows) return true; + return false; + } + + private function mb_dirname($path) { + $path = str_replace('\\', '/', $path); + $last_slash = strrpos($path, '/'); + if ($last_slash !== false) { + return substr($path, 0, $last_slash); + } + return '.'; + } + + private function mb_basename($path) { + $path = str_replace('\\', '/', $path); + $parts = explode('/', $path); + return end($parts); + } + + private function getRelativeImagePath($absolutePath) { + $absolutePath = str_replace('\\', '/', $absolutePath); + $dirImageNorm = str_replace('\\', '/', DIR_IMAGE); + $rel = $absolutePath; + if (substr($absolutePath, 0, strlen($dirImageNorm)) === $dirImageNorm) { + $rel = substr($absolutePath, strlen($dirImageNorm)); + } elseif (strtolower(substr($absolutePath, 0, 3)) === strtolower(substr($dirImageNorm, 0, 3))) { + $dirImageNormLower = strtolower($dirImageNorm); + $absLower = strtolower($absolutePath); + if (substr($absLower, 0, strlen($dirImageNormLower)) === $dirImageNormLower) { + $rel = substr($absolutePath, strlen($dirImageNorm)); + } + } + $rel = ltrim($rel, '/'); + return $this->safe_utf8($rel); + } +} \ No newline at end of file diff --git a/upload/admin/view/template/module/img_opti.tpl b/upload/admin/view/template/module/img_opti.tpl new file mode 100644 index 0000000..00b4738 --- /dev/null +++ b/upload/admin/view/template/module/img_opti.tpl @@ -0,0 +1,3709 @@ + +
    + + +
    + +
    + +
    + + +
    + +
    + + +
    + +
    +
    +

    +
    +
    + +
    + +
    +
    +
    +

    +
    +
    +
    + +
    +
    + +
    +
    + + + + +
    +
    +
    +

    +

    +
      +
    • Email: info@nat.od.ua
    • +
    • Gitea: https://git.nat.od.ua
    • +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + +
    + + + + + + + + +
    +
    +
    +
    + +
    +
    +
    +

    + + + +

    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + + + + + + +
    + +
    +
    +
    + +
    +

    +
      +
    • : ' . $text_enabled . '' : '' . $text_disabled_error . ''; ?>
    • +
    • : ' . $text_supported . '' : '' . $text_no . ''; ?>
    • +
    • : ' . $text_supported . '' : '' . $text_no . ''; ?>
    • +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + + + +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + +
    +
    + +
    + + +
    +
    + + +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    + +
    +
    + +
    +
    + +
    + + +
    +
    + + +
    + +
    +

    0 / 0

    +
    +
    +
    + + + +
    + + +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + + +
    + + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    + + +
    + + +
    + +
    + +
    +
    +
    +
    + +
    +
    +   +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    + +
    +
    + +
    +
    + + +
    +
    + + + +
    +
    + +
    + + + + + + + + + + + + + + +
    +
    + +
    +
    +
    + wget -q -O - "index.php?route=module/img_opti_cron&key=" >/dev/null 2>&1 +
    + +
    +
    +
    + + + + + + + + + + + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +

    +
    +
    + +
    +
    +
    + + + +
    +
    +
    +
    +

    +

    +

    info@nat.od.ua

    +
    + +
    +
    +
    +

    +

    +
    +
    +
    +
    +

    +

    +
    +
    +
    +
    +
    +
    +

    +

    +
    +
    +
    +
    +

    +

    +
    +
    +
    +
    +
    +
    +

    +

    +
    +
    +
    +
    +

    +

    +
    +
    +
    +
    +
    +
    +

    +

    +
    +
    +
    +
    +

    +

    +
    +
    +
    +
    +
    +
    +

    +

    +
    +
    +
    +
    +

    +

    +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + + + + + + + + + + + + + + diff --git a/upload/catalog/controller/module/img_opti_cron.php b/upload/catalog/controller/module/img_opti_cron.php new file mode 100644 index 0000000..01f57f8 --- /dev/null +++ b/upload/catalog/controller/module/img_opti_cron.php @@ -0,0 +1,637 @@ +load->model('setting/setting'); + $settings = $this->model_setting_setting->getSetting('module_img_opti'); + + if (PHP_SAPI !== 'cli') { + if (empty($this->request->get['key']) || $this->request->get['key'] !== $settings['module_img_opti_token']) { + die('Error: Unauthorized request. Invalid cron key.'); + } + } + + if (empty($settings['module_img_opti_status'])) { + die('Error: Module Image Optimizer [NAT] is disabled.'); + } + + if (empty($settings['module_img_opti_token']) || !$this->checkLicense($settings['module_img_opti_token'])) { + die('Error: Module token is invalid or license not verified.'); + } + + $tasks = array(); + if (!empty($settings['module_img_opti_cron_tasks'])) { + $tasks = json_decode(html_entity_decode($settings['module_img_opti_cron_tasks'], ENT_QUOTES, 'UTF-8'), true); + } + + if (!is_array($tasks) || empty($tasks)) { + $legacy_entities = isset($settings['module_img_opti_cron_entities']) ? $settings['module_img_opti_cron_entities'] : array(); + if (is_string($legacy_entities)) { + $legacy_entities = json_decode(html_entity_decode($legacy_entities, ENT_QUOTES, 'UTF-8'), true) ?: explode(',', $legacy_entities); + } + $legacy_folders = isset($settings['module_img_opti_cron_folders']) ? $settings['module_img_opti_cron_folders'] : array(); + if (is_string($legacy_folders)) { + $legacy_folders = json_decode(html_entity_decode($legacy_folders, ENT_QUOTES, 'UTF-8'), true) ?: explode(',', $legacy_folders); + } + + $tasks = array( + array( + 'status' => 0, + 'name' => 'Default Task', + 'new_only' => isset($settings['module_img_opti_cron_new_only']) ? (int)$settings['module_img_opti_cron_new_only'] : 1, + 'entities' => is_array($legacy_entities) ? $legacy_entities : array('product', 'category', 'manufacturer', 'banner', 'blog'), + 'folder_mode' => isset($settings['module_img_opti_cron_folder_mode']) ? $settings['module_img_opti_cron_folder_mode'] : 'exclude', + 'folders' => is_array($legacy_folders) ? $legacy_folders : array(), + 'quality_override' => isset($settings['module_img_opti_cron_quality_override']) ? (int)$settings['module_img_opti_cron_quality_override'] : 0, + 'jpg_quality' => isset($settings['module_img_opti_cron_jpg_quality']) ? (int)$settings['module_img_opti_cron_jpg_quality'] : 72, + 'webp_quality' => isset($settings['module_img_opti_cron_webp_quality']) ? (int)$settings['module_img_opti_cron_webp_quality'] : 80, + 'max_width' => isset($settings['module_img_opti_cron_max_width']) ? (int)$settings['module_img_opti_cron_max_width'] : 1600, + 'max_height' => isset($settings['module_img_opti_cron_max_height']) ? (int)$settings['module_img_opti_cron_max_height'] : 1600 + ) + ); + } + + $tasks_to_run = array(); + if (isset($this->request->get['task'])) { + $req_task = trim($this->request->get['task']); + foreach ($tasks as $task) { + if (strcasecmp($task['name'], $req_task) === 0) { + if (empty($task['status'])) { + die("Error: Requested cron task '" . htmlspecialchars($req_task) . "' is disabled."); + } + $tasks_to_run[] = $task; + break; + } + } + if (empty($tasks_to_run)) { + die("Error: Requested cron task '" . htmlspecialchars($req_task) . "' was not found."); + } + } else { + foreach ($tasks as $task) { + if (!empty($task['status'])) { + $tasks_to_run[] = $task; + } + } + if (empty($tasks_to_run)) { + die("Success: No enabled cron tasks found.\n"); + } + } + + echo "Image Optimizer [NAT] Cron Engine Started...\n"; + echo "Found " . count($tasks_to_run) . " task(s) to execute.\n\n"; + + $engine = isset($settings['module_img_opti_engine']) && $settings['module_img_opti_engine'] == 'imagick' && extension_loaded('imagick') ? 'imagick' : 'gd'; + $min_savings_bytes = isset($settings['module_img_opti_min_savings_bytes']) ? (int)$settings['module_img_opti_min_savings_bytes'] : 100; + $min_savings_percent = isset($settings['module_img_opti_min_savings_percent']) ? (float)$settings['module_img_opti_min_savings_percent'] : 0.3; + $threshold = isset($settings['module_img_opti_size_threshold']) ? (float)$settings['module_img_opti_size_threshold'] : 0; + + $cacheFileOpt = DIR_CACHE . 'img_opti_optimized.json'; + $cache = array(); + if (file_exists($cacheFileOpt)) { + $content = @file_get_contents($cacheFileOpt); + if ($content) { + $decoded = json_decode($content, true); + if (is_array($decoded)) { + $cache = $decoded; + } + } + } + + $realImageDir = rtrim(str_replace('\\', '/', realpath(DIR_IMAGE)), '/'); + $dirImageNorm = rtrim(str_replace('\\', '/', DIR_IMAGE), '/') . '/'; + + foreach ($tasks_to_run as $task) { + $taskName = $task['name']; + echo "--- Executing Task: $taskName ---\n"; + + $task_name_lower = strtolower($taskName); + $cacheFile = DIR_CACHE . 'img_opti_cron_last_run_' . md5($task_name_lower) . '.txt'; + if ($task_name_lower === 'default task' && !file_exists($cacheFile) && file_exists(DIR_CACHE . 'img_opti_cron_last_run.txt')) { + @copy(DIR_CACHE . 'img_opti_cron_last_run.txt', $cacheFile); + } + + $onlyNew = isset($task['new_only']) ? (int)$task['new_only'] : 1; + $lastRunTime = 0; + if ($onlyNew) { + $lastRunTime = file_exists($cacheFile) ? (int)file_get_contents($cacheFile) : 0; + } + + $task_maxWidth = isset($settings['module_img_opti_max_width']) ? (int)$settings['module_img_opti_max_width'] : 1600; + $task_maxHeight = isset($settings['module_img_opti_max_height']) ? (int)$settings['module_img_opti_max_height'] : 1600; + $task_jpgQ = isset($settings['module_img_opti_jpg_quality']) ? (int)$settings['module_img_opti_jpg_quality'] : 72; + $task_pngQ = isset($settings['module_img_opti_png_quality']) ? (int)$settings['module_img_opti_png_quality'] : 7; + $task_webpQ = isset($settings['module_img_opti_webp_quality']) ? (int)$settings['module_img_opti_webp_quality'] : 80; + + if (!empty($task['quality_override'])) { + $task_jpgQ = isset($task['jpg_quality']) ? (int)$task['jpg_quality'] : $task_jpgQ; + $task_webpQ = isset($task['webp_quality']) ? (int)$task['webp_quality'] : $task_webpQ; + $task_maxWidth = isset($task['max_width']) ? (int)$task['max_width'] : $task_maxWidth; + $task_maxHeight = isset($task['max_height']) ? (int)$task['max_height'] : $task_maxHeight; + } + + $batchLimit = 100; + $dirsToScan = array(); + $excludeDirs = array(); + + $folder_mode = isset($task['folder_mode']) ? $task['folder_mode'] : 'exclude'; + $selected_folders = isset($task['folders']) ? $task['folders'] : array(); + if (is_string($selected_folders)) { + $selected_folders = json_decode(html_entity_decode($selected_folders, ENT_QUOTES, 'UTF-8'), true) ?: explode(',', $selected_folders); + } + + if ($folder_mode === 'exclude') { + $dirsToScan[] = $realImageDir . '/catalog'; + foreach ($selected_folders as $folder) { + $folder = trim(str_replace(array('../', '..\\', "\0"), '', $folder)); + if ($folder === '') continue; + $path = realpath(DIR_IMAGE . $folder); + if ($path && is_dir($path) && strpos(str_replace('\\', '/', $path), $realImageDir) === 0) { + $excludeDirs[] = str_replace('\\', '/', $path); + } + } + } else { + foreach ($selected_folders as $folder) { + $folder = trim(str_replace(array('../', '..\\', "\0"), '', $folder)); + if ($folder === '') continue; + $path = realpath(DIR_IMAGE . $folder); + if ($path && is_dir($path) && strpos(str_replace('\\', '/', $path), $realImageDir) === 0) { + $dirsToScan[] = $path; + } + } + if (empty($dirsToScan)) { + $dirsToScan[] = $realImageDir . '/catalog'; + } + } + + $cron_entities = isset($task['entities']) ? $task['entities'] : array(); + if (is_string($cron_entities)) { + $cron_entities = json_decode(html_entity_decode($cron_entities, ENT_QUOTES, 'UTF-8'), true) ?: explode(',', $cron_entities); + } + + echo "Mode: " . ($onlyNew ? "Scanning for NEW files since " . date('Y-m-d H:i:s', $lastRunTime) : "Scanning ALL files") . "\n"; + echo "Quality Settings - JPG: $task_jpgQ%, WebP: $task_webpQ%, Max size: " . $task_maxWidth . "x" . $task_maxHeight . "px\n"; + + $filesToProcess = array(); + $currentTime = time(); + + foreach ($dirsToScan as $dir) { + if (!is_dir($dir)) continue; + try { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)); + foreach ($iterator as $file) { + if ($file->isFile()) { + $realPath = realpath($file->getPathname()); + if (!$realPath || strpos(str_replace('\\', '/', $realPath), $realImageDir) !== 0) continue; + + $normalizedPath = str_replace('\\', '/', $realPath); + $in_excluded = false; + foreach ($excludeDirs as $ex_dir) { + if (strpos($normalizedPath, $ex_dir) === 0) { + $in_excluded = true; + break; + } + } + if ($in_excluded) continue; + + $ext = strtolower($file->getExtension()); + if (in_array($ext, array('jpg', 'jpeg', 'png', 'webp', 'gif'))) { + $filePathNorm = str_replace('\\', '/', $realPath); + $relPath = $this->getRelativeImagePath($filePathNorm); + $skip = false; + if ($onlyNew && isset($cache[$relPath])) { + clearstatcache(true, $filePathNorm); + $size = @filesize($filePathNorm); + $mtime = @filemtime($filePathNorm); + if ($size === $cache[$relPath]['size'] && $mtime === $cache[$relPath]['mtime']) { + $skip = true; + } + } + if ($skip) continue; + + if ($file->getMTime() > $lastRunTime) { + $relativePath = $this->getRelativeImagePath($normalizedPath); + $entities = $this->getEntitiesByImagePath($relativePath); + $has_match = false; + + if (empty($entities)) { + if (empty($cron_entities)) { + $has_match = true; + } + } else { + if (empty($cron_entities)) { + $has_match = true; + } else { + foreach ($entities as $entity_html) { + if (strpos($entity_html, 'Product') !== false && in_array('product', $cron_entities)) $has_match = true; + if (strpos($entity_html, 'Category') !== false && in_array('category', $cron_entities)) $has_match = true; + if (strpos($entity_html, 'Manufacturer') !== false && in_array('manufacturer', $cron_entities)) $has_match = true; + if (strpos($entity_html, 'Banner') !== false && in_array('banner', $cron_entities)) $has_match = true; + if (strpos($entity_html, 'Blog') !== false && in_array('blog', $cron_entities)) $has_match = true; + if (strpos($entity_html, 'Simple Blog') !== false && in_array('blog', $cron_entities)) $has_match = true; + if (strpos($entity_html, 'OCT Blog') !== false && in_array('blog', $cron_entities)) $has_match = true; + if (strpos($entity_html, 'NewsBlog') !== false && in_array('blog', $cron_entities)) $has_match = true; + if (strpos($entity_html, 'Information') !== false && in_array('blog', $cron_entities)) $has_match = true; + } + } + } + + if ($has_match) { + $filesToProcess[] = $realPath; + if (count($filesToProcess) >= $batchLimit) { + break 2; + } + } + } + } + } + } + } catch (Exception $e) { + continue; + } + } + + if (empty($filesToProcess)) { + file_put_contents($cacheFile, $currentTime); + echo "No new files found for optimization in this task.\n\n"; + continue; + } + + $processedCount = 0; + $savedBytes = 0; + $cache_updates = array(); + + foreach ($filesToProcess as $filePath) { + $realFilePath = realpath($filePath); + if (!$realFilePath || strpos(str_replace('\\', '/', $realFilePath), $dirImageNorm) !== 0 || !is_file($realFilePath)) continue; + + $relativePath = $this->getRelativeImagePath($realFilePath); + $fileParams = $this->getImageParameters($relativePath, array( + 'module_img_opti_folder_rules' => isset($settings['module_img_opti_folder_rules']) ? $settings['module_img_opti_folder_rules'] : array(), + 'module_img_opti_jpg_quality' => $task_jpgQ, + 'module_img_opti_png_quality' => $task_pngQ, + 'module_img_opti_webp_quality' => $task_webpQ, + 'module_img_opti_max_width' => $task_maxWidth, + 'module_img_opti_max_height' => $task_maxHeight + )); + + $oldSize = filesize($realFilePath); + $tempPath = DIR_CACHE . 'opti_cron_temp_' . md5($realFilePath); + + if ($this->optimizeImage($realFilePath, $tempPath, $fileParams['max_width'], $fileParams['max_height'], $fileParams['jpg_quality'], $fileParams['png_quality'], $fileParams['webp_quality'], $engine)) { + clearstatcache(); + $newSize = filesize($tempPath); + $increasePercent = ($oldSize > 0) ? (($newSize - $oldSize) / $oldSize) * 100 : 0; + + $savedBytesFile = $oldSize - $newSize; + $savedPercentFile = ($oldSize > 0) ? ($savedBytesFile / $oldSize) * 100 : 0; + $is_too_small_gain = ($savedBytesFile < $min_savings_bytes || $savedPercentFile < $min_savings_percent); + + if ($increasePercent > $threshold || $is_too_small_gain) { + @unlink($tempPath); + $cacheKey = $this->getRelativeImagePath($realFilePath); + $cache_updates[$cacheKey] = array( + 'size' => $oldSize, + 'mtime' => @filemtime($realFilePath) + ); + } else { + @rename($tempPath, $realFilePath); + $savedBytes += ($oldSize - $newSize); + $processedCount++; + clearstatcache(true, $realFilePath); + $cacheKey = $this->getRelativeImagePath($realFilePath); + $cache_updates[$cacheKey] = array( + 'size' => @filesize($realFilePath), + 'mtime' => @filemtime($realFilePath) + ); + } + } else { + @unlink($tempPath); + } + + gc_collect_cycles(); + + $memLimit = $this->getMemoryLimitInBytesHelper(); + if (memory_get_usage(true) > 0.80 * $memLimit) { + echo "Memory limit reached (80%). Stopping task execution batch...\n"; + break; + } + } + + if (!empty($cache_updates)) { + if (file_exists($cacheFileOpt)) { + $content = @file_get_contents($cacheFileOpt); + if ($content) { + $decoded = json_decode($content, true); + if (is_array($decoded)) { + $cache = array_merge($cache, $decoded); + } + } + } + foreach ($cache_updates as $k => $v) { + $cache[$k] = $v; + } + @file_put_contents($cacheFileOpt, json_encode($cache, JSON_UNESCAPED_UNICODE)); + } + + if ($processedCount > 0 || count($filesToProcess) < $batchLimit) { + file_put_contents($cacheFile, $currentTime); + } + + echo "Task Done: Processed $processedCount files. Saved space: " . $this->formatBytes($savedBytes) . ".\n\n"; + } + + echo "All cron tasks execution finished.\n"; + } + + private function getEntitiesByImagePath($image_path) { + $entities = array(); + $escaped = $this->db->escape((string)$image_path); + $catalog_url = (defined('HTTPS_CATALOG') && HTTPS_CATALOG) ? HTTPS_CATALOG : (defined('HTTP_CATALOG') ? HTTP_CATALOG : ''); + $query = $this->db->query("SELECT product_id, (SELECT name FROM `" . DB_PREFIX . "product_description` pd WHERE pd.product_id = p.product_id AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' LIMIT 1) AS name FROM `" . DB_PREFIX . "product` p WHERE image = '" . $escaped . "'"); + foreach ($query->rows as $row) { + $entities[] = 'Product ID ' . $row['product_id']; + } + $query = $this->db->query("SELECT pi.product_id FROM `" . DB_PREFIX . "product_image` pi WHERE image = '" . $escaped . "'"); + foreach ($query->rows as $row) { + $entities[] = 'Product ID ' . $row['product_id']; + } + $query = $this->db->query("SELECT category_id FROM `" . DB_PREFIX . "category` c WHERE image = '" . $escaped . "'"); + foreach ($query->rows as $row) { + $entities[] = 'Category ID ' . $row['category_id']; + } + $query = $this->db->query("SELECT manufacturer_id, name FROM `" . DB_PREFIX . "manufacturer` WHERE image = '" . $escaped . "'"); + foreach ($query->rows as $row) { + $entities[] = 'Manufacturer ID ' . $row['manufacturer_id']; + } + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $query_banner = $this->db->query("SELECT bi.banner_id FROM `" . DB_PREFIX . "banner_image` bi WHERE bi.image = '" . $escaped . "'"); + foreach ($query_banner->rows as $row) { + $entities[] = 'Banner ID ' . $row['banner_id']; + } + } + $blog_tables = array( + 'oct_blog_article' => array('id' => 'blog_article_id'), + 'simple_blog_article' => array('id' => 'simple_blog_article_id'), + 'newsblog_article' => array('id' => 'article_id'), + 'information' => array('id' => 'information_id') + ); + foreach ($blog_tables as $table => $cfg) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $table . "` LIKE 'image'"); + if ($column_query->num_rows) { + try { + $query_blog = $this->db->query("SELECT `" . $cfg['id'] . "` FROM `" . DB_PREFIX . $table . "` WHERE image = '" . $escaped . "' LIMIT 5"); + foreach ($query_blog->rows as $row) { + $entities[] = 'Blog ID ' . $row[$cfg['id']]; + } + } catch (Exception $e) {} + } + } + } + return $entities; + } + + private function optimizeImage($src, $dest, $maxWidth, $maxHeight, $jpgQuality, $pngCompression, $webpQuality, $engine = 'gd') { + if ($engine == 'imagick' && extension_loaded('imagick')) { + try { + $img = new Imagick($src); + $img->transformImageColorspace(Imagick::COLORSPACE_SRGB); + + $width = $img->getImageWidth(); + $height = $img->getImageHeight(); + $ext = strtolower($this->mb_pathinfo($src, PATHINFO_EXTENSION)); + + if (($maxWidth > 0 && $width > $maxWidth) || ($maxHeight > 0 && $height > $maxHeight)) { + $targetW = $maxWidth; + $targetH = $maxHeight; + if ($targetW == 0) { + $targetW = (int)round($width * ($targetH / $height)); + } + if ($targetH == 0) { + $targetH = (int)round($height * ($targetW / $width)); + } + $img->resizeImage($targetW, $targetH, Imagick::FILTER_LANCZOS, 1, true); + } + + $img->stripImage(); + + if ($ext === 'png') { + $img->setImageFormat('png'); + $img->setCompressionQuality($pngCompression * 10); + } elseif ($ext === 'webp') { + $img->setImageFormat('webp'); + $img->setCompressionQuality($webpQuality); + } else { + $img->setImageFormat('jpeg'); + $img->setImageCompression(Imagick::COMPRESSION_JPEG); + $img->setCompressionQuality($jpgQuality); + } + + return $img->writeImage($dest); + } catch (Exception $e) { + $engine = 'gd'; + } + } + + if ($engine == 'gd') { + $img = @imagecreatefromstring(file_get_contents($src)); + if (!$img) return false; + + $width = imagesx($img); + $height = imagesy($img); + $ext = strtolower($this->mb_pathinfo($src, PATHINFO_EXTENSION)); + + if ($ext === 'png' || $ext === 'webp') { + imagealphablending($img, false); + imagesavealpha($img, true); + } + + if (($maxWidth > 0 && $width > $maxWidth) || ($maxHeight > 0 && $height > $maxHeight)) { + $ratioX = ($maxWidth > 0) ? ($maxWidth / $width) : 1; + $ratioY = ($maxHeight > 0) ? ($maxHeight / $height) : 1; + $ratio = min($ratioX, $ratioY); + + $newW = (int)($width * $ratio); + $newH = (int)($height * $ratio); + + $newImg = imagecreatetruecolor($newW, $newH); + + if ($ext === 'png' || $ext === 'webp') { + imagealphablending($newImg, false); + imagesavealpha($newImg, true); + $transparent = imagecolorallocatealpha($newImg, 255, 255, 255, 127); + imagefill($newImg, 0, 0, $transparent); + } else { + $white = imagecolorallocate($newImg, 255, 255, 255); + imagefill($newImg, 0, 0, $white); + } + + imagecopyresampled($newImg, $img, 0, 0, 0, 0, $newW, $newH, $width, $height); + imagedestroy($img); + $img = $newImg; + } + + $res = false; + if ($ext === 'png') { + $res = imagepng($img, $dest, $pngCompression); + } elseif ($ext === 'webp' && function_exists('imagewebp')) { + $res = imagewebp($img, $dest, $webpQuality); + } else { + $res = imagejpeg($img, $dest, $jpgQuality); + } + + imagedestroy($img); + return $res; + } + return false; + } + + private function checkLicense($key) { + $host = explode(':', str_replace(array('http://', 'https://', 'www.'), '', strtolower($this->request->server['HTTP_HOST'])))[0]; + if ($host === 'localhost' || $host === '127.0.0.1') return true; + if (empty($key)) return false; + $parts = explode('.', $host); + $v_k = 'k'; $v_to = 'to'; $v_bit = '_lom'; $v_end = 'aet_'; $v_img = 'image_'; $v_opt = 'optimizer_'; $v_mid = 'tot'; $v_core = '_redis'; $v_sfx = 'ka'; + $salt = ($v_k . $v_to . $v_bit) . ($v_end . $v_img . $v_opt . $v_mid) . ($v_core . $v_sfx); + while(count($parts) >= 2) { + $domain = implode('.', $parts); + if ($key === strtoupper(implode('-', str_split(substr(md5($domain . $salt), 0, 16), 4)))) return true; + array_shift($parts); + } + return false; + } + + private function formatBytes($bytes) { + if ($bytes >= 1048576) return round($bytes / 1048576, 2) . ' MB'; + if ($bytes >= 1024) return round($bytes / 1024, 0) . ' KB'; + return $bytes . ' B'; + } + + private function getImageParameters($relativePath, $globalSettings) { + $rules = array(); + if (isset($globalSettings['module_img_opti_folder_rules'])) { + if (is_array($globalSettings['module_img_opti_folder_rules'])) { + $rules = $globalSettings['module_img_opti_folder_rules']; + } else { + $rules = json_decode(html_entity_decode((string)$globalSettings['module_img_opti_folder_rules'], ENT_QUOTES, 'UTF-8'), true); + if (!is_array($rules)) { + $rules = array(); + } + } + } + + $jpgQ = isset($globalSettings['module_img_opti_jpg_quality']) ? (int)$globalSettings['module_img_opti_jpg_quality'] : 72; + $pngQ = isset($globalSettings['module_img_opti_png_quality']) ? (int)$globalSettings['module_img_opti_png_quality'] : 7; + $webpQ = isset($globalSettings['module_img_opti_webp_quality']) ? (int)$globalSettings['module_img_opti_webp_quality'] : 80; + $maxW = isset($globalSettings['module_img_opti_max_width']) ? (int)$globalSettings['module_img_opti_max_width'] : 1600; + $maxH = isset($globalSettings['module_img_opti_max_height']) ? (int)$globalSettings['module_img_opti_max_height'] : 1600; + + $relativePath = str_replace('\\', '/', $relativePath); + $relativePath = ltrim($relativePath, '/'); + + foreach ($rules as $rule) { + if (empty($rule['folder'])) continue; + $folder = str_replace('\\', '/', $rule['folder']); + $folder = trim($folder, '/'); + + if ($folder !== '' && strpos($relativePath, $folder) !== false) { + if (isset($rule['jpg_quality']) && $rule['jpg_quality'] !== '') { + $jpgQ = (int)$rule['jpg_quality']; + } + if (isset($rule['webp_quality']) && $rule['webp_quality'] !== '') { + $webpQ = (int)$rule['webp_quality']; + } + if (isset($rule['max_width']) && $rule['max_width'] !== '') { + $maxW = (int)$rule['max_width']; + } + if (isset($rule['max_height']) && $rule['max_height'] !== '') { + $maxH = (int)$rule['max_height']; + } + break; + } + } + + return array( + 'jpg_quality' => $jpgQ, + 'png_quality' => $pngQ, + 'webp_quality' => $webpQ, + 'max_width' => $maxW, + 'max_height' => $maxH + ); + } + + private function getMemoryLimitInBytesHelper() { + $limit = ini_get('memory_limit'); + if (!$limit || (int)$limit === -1) return 512 * 1024 * 1024; + $unit = strtolower(substr($limit, -1)); + $bytes = (int)$limit; + switch ($unit) { + case 'g': $bytes *= 1024; + case 'm': $bytes *= 1024; + case 'k': $bytes *= 1024; + } + return $bytes; + } + + private function getRelativeImagePath($absolutePath) { + $absolutePath = str_replace('\\', '/', $absolutePath); + $dirImageNorm = str_replace('\\', '/', DIR_IMAGE); + $rel = $absolutePath; + if (strpos(strtolower($absolutePath), strtolower($dirImageNorm)) === 0) { + $rel = substr($absolutePath, strlen($dirImageNorm)); + } + $rel = ltrim($rel, '/'); + if (mb_check_encoding($rel, 'UTF-8')) { + return $rel; + } + $converted = @mb_convert_encoding($rel, 'UTF-8', 'Windows-1251'); + return $converted ? $converted : $rel; + } + + private function mb_pathinfo($path, $options = null) { + $ret = array('dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''); + $path = str_replace('\\', '/', $path); + $last_slash = strrpos($path, '/'); + if ($last_slash !== false) { + $ret['dirname'] = substr($path, 0, $last_slash); + $basename = substr($path, $last_slash + 1); + } else { + $basename = $path; + } + $ret['basename'] = $basename; + $last_dot = strrpos($basename, '.'); + if ($last_dot !== false) { + $ret['filename'] = substr($basename, 0, $last_dot); + $ret['extension'] = substr($basename, $last_dot + 1); + } else { + $ret['filename'] = $basename; + } + if ($options === PATHINFO_DIRNAME) return $ret['dirname']; + if ($options === PATHINFO_BASENAME) return $ret['basename']; + if ($options === PATHINFO_EXTENSION) return $ret['extension']; + if ($options === PATHINFO_FILENAME) return $ret['filename']; + return $ret; + } + + private function safe_utf8($str) { + $str = (string)$str; + if (mb_check_encoding($str, 'UTF-8')) { + return $str; + } + $converted = @mb_convert_encoding($str, 'UTF-8', 'Windows-1251'); + return $converted ? $converted : $str; + } + + private function mb_basename($path) { + $path = str_replace('\\', '/', $path); + $parts = explode('/', $path); + return end($parts); + } +} \ No newline at end of file diff --git a/upload/catalog/model/module/img_opti.php b/upload/catalog/model/module/img_opti.php new file mode 100644 index 0000000..7f3a165 --- /dev/null +++ b/upload/catalog/model/module/img_opti.php @@ -0,0 +1,672 @@ +db->escape($original_file_normalized); + $matched_rules = array(); + + if ($is_preview) { + $matched_rules[] = array( + 'status' => 1, + 'type' => $this->config->get('module_img_opti_wm_type'), + 'image' => $this->config->get('module_img_opti_wm_image'), + 'text_val' => $this->config->get('module_img_opti_wm_text_val'), + 'position' => $this->config->get('module_img_opti_wm_position'), + 'opacity' => $this->config->get('module_img_opti_wm_opacity'), + 'angle' => $this->config->get('module_img_opti_wm_angle'), + 'corner_radius' => $this->config->get('module_img_opti_wm_corner_radius'), + 'size_type' => $this->config->get('module_img_opti_wm_size_type'), + 'size_percent' => $this->config->get('module_img_opti_wm_size_percent'), + 'text_color' => $this->config->get('module_img_opti_wm_text_color'), + 'text_font' => $this->config->get('module_img_opti_wm_text_font') + ); + } else { + $rules_json = $this->config->get('module_img_opti_wm_rules'); + $rules = json_decode(html_entity_decode($rules_json, ENT_QUOTES, 'UTF-8'), true) ?: array(); + + foreach ($rules as $rule) { + if (empty($rule['status'])) continue; + + $targets = array(); + if (!empty($rule['target_product'])) $targets[] = 'product'; + if (!empty($rule['target_category'])) $targets[] = 'category'; + if (!empty($rule['target_brand'])) $targets[] = 'manufacturer'; + if (!empty($rule['target_banner'])) $targets[] = 'banner'; + if (!empty($rule['target_blog'])) $targets[] = 'blog'; + + $is_match = false; + $product_id = 0; + $manufacturer_id = 0; + + if (in_array('product', $targets)) { + $query_prod = $this->db->query("SELECT product_id, manufacturer_id FROM `" . DB_PREFIX . "product` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_prod->num_rows) { + $is_match = true; + $product_id = (int)$query_prod->row['product_id']; + $manufacturer_id = (int)$query_prod->row['manufacturer_id']; + } else { + $query_prod_add = $this->db->query("SELECT product_id FROM `" . DB_PREFIX . "product_image` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_prod_add->num_rows) { + $is_match = true; + $product_id = (int)$query_prod_add->row['product_id']; + $query_p_info = $this->db->query("SELECT manufacturer_id FROM `" . DB_PREFIX . "product` WHERE product_id = '" . $product_id . "' LIMIT 1"); + $manufacturer_id = $query_p_info->num_rows ? (int)$query_p_info->row['manufacturer_id'] : 0; + } + } + + if ($is_match) { + $categories = array(); + if (!empty($rule['categories'])) { + foreach ($rule['categories'] as $c) { + $categories[] = is_array($c) ? (int)$c['id'] : (int)$c; + } + } + $brands = array(); + if (!empty($rule['brands'])) { + foreach ($rule['brands'] as $b) { + $brands[] = is_array($b) ? (int)$b['id'] : (int)$b; + } + } + + $prod_categories = array(); + $query_p_cats = $this->db->query("SELECT category_id FROM `" . DB_PREFIX . "product_to_category` WHERE product_id = '" . $product_id . "'"); + foreach ($query_p_cats->rows as $row) { + $prod_categories[] = (int)$row['category_id']; + } + + $in_selected_cats = false; + if (!empty($categories)) { + foreach ($prod_categories as $c_id) { + if (in_array($c_id, $categories)) { + $in_selected_cats = true; + break; + } + } + } + + $in_selected_brands = false; + if (!empty($brands) && in_array($manufacturer_id, $brands)) { + $in_selected_brands = true; + } + + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'include'; + if ($filter_mode === 'include') { + if (!empty($categories) || !empty($brands)) { + if (!$in_selected_cats && !$in_selected_brands) { + $is_match = false; + } + } + } else { + if ($in_selected_cats || $in_selected_brands) { + $is_match = false; + } + } + } + } + + if (!$is_match && in_array('category', $targets)) { + $query_cat = $this->db->query("SELECT category_id FROM `" . DB_PREFIX . "category` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_cat->num_rows) { + $is_match = true; + $category_id = (int)$query_cat->row['category_id']; + $categories = array(); + if (!empty($rule['categories'])) { + foreach ($rule['categories'] as $c) { + $categories[] = is_array($c) ? (int)$c['id'] : (int)$c; + } + } + if (!empty($categories)) { + $in_selected = in_array($category_id, $categories); + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'include'; + if ($filter_mode === 'include' && !$in_selected) { + $is_match = false; + } elseif ($filter_mode === 'exclude' && $in_selected) { + $is_match = false; + } + } + } + } + + if (!$is_match && in_array('manufacturer', $targets)) { + $query_brand = $this->db->query("SELECT manufacturer_id FROM `" . DB_PREFIX . "manufacturer` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_brand->num_rows) { + $is_match = true; + $manufacturer_id = (int)$query_brand->row['manufacturer_id']; + $brands = array(); + if (!empty($rule['brands'])) { + foreach ($rule['brands'] as $b) { + $brands[] = is_array($b) ? (int)$b['id'] : (int)$b; + } + } + if (!empty($brands)) { + $in_selected = in_array($manufacturer_id, $brands); + $filter_mode = isset($rule['filter_mode']) ? $rule['filter_mode'] : 'include'; + if ($filter_mode === 'include' && !$in_selected) { + $is_match = false; + } elseif ($filter_mode === 'exclude' && $in_selected) { + $is_match = false; + } + } + } + } + + if (!$is_match && in_array('banner', $targets)) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . "banner_image'"); + if ($table_query->num_rows) { + $query_banner = $this->db->query("SELECT banner_image_id FROM `" . DB_PREFIX . "banner_image` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_banner->num_rows) { + $is_match = true; + } + } + } + + if (!$is_match && in_array('blog', $targets)) { + $blog_tables = array( + 'oct_blog_article', + 'simple_blog_article', + 'newsblog_article', + 'information' + ); + foreach ($blog_tables as $b_table) { + $table_query = $this->db->query("SHOW TABLES LIKE '" . DB_PREFIX . $b_table . "'"); + if ($table_query->num_rows) { + $column_query = $this->db->query("SHOW COLUMNS FROM `" . DB_PREFIX . $b_table . "` LIKE 'image'"); + if ($column_query->num_rows) { + $query_blog = $this->db->query("SELECT * FROM `" . DB_PREFIX . $b_table . "` WHERE image = '" . $escaped_original . "' LIMIT 1"); + if ($query_blog->num_rows) { + $is_match = true; + break; + } + } + } + } + } + + if ($is_match) { + $matched_rules[] = $rule; + } + } + } + + if (empty($matched_rules)) { + return false; + } + + $engine = preg_replace('/[^a-z]/', '', strtolower($this->config->get('module_img_opti_engine'))); + $hasImagick = extension_loaded('imagick'); + + if ($engine === 'imagick' && $hasImagick) { + try { + $img = new Imagick($realCachedFile); + foreach ($matched_rules as $rule) { + $type = preg_replace('/[^a-z]/', '', $rule['type']); + $imagePathStr = str_replace(array('../', '..\\', ' '), '', (string)$rule['image']); + $imagePath = realpath(DIR_IMAGE . $imagePathStr); + if ($imagePath && stripos(str_replace('\\', '/', $imagePath), str_replace('\\', '/', $realImageDir)) !== 0) { + $imagePath = false; + } + $textVal = (string)$rule['text_val']; + $position = (int)$rule['position']; + $opacity = (int)$rule['opacity']; + $size_type = isset($rule['size_type']) ? $rule['size_type'] : 'original'; + $size_percent = isset($rule['size_percent']) ? (int)$rule['size_percent'] : 20; + $angle = isset($rule['angle']) ? (int)$rule['angle'] : 0; + $corner_radius = isset($rule['corner_radius']) ? (int)$rule['corner_radius'] : 0; + $text_font = isset($rule['text_font']) ? (string)$rule['text_font'] : ''; + $text_color = isset($rule['text_color']) ? (string)$rule['text_color'] : '#ffffff'; + + if ($type === 'image') { + if (!$imagePath || !is_file($imagePath)) continue; + } + if ($type === 'text' && empty($textVal)) continue; + + $imgW = $img->getImageWidth(); + $imgH = $img->getImageHeight(); + + if ($type === 'image') { + $wm = new Imagick($imagePath); + + $max_wm_w = (int)$this->config->get('module_img_opti_wm_max_width'); + if ($max_wm_w <= 0) $max_wm_w = 800; + $max_wm_h = (int)$this->config->get('module_img_opti_wm_max_height'); + if ($max_wm_h <= 0) $max_wm_h = 800; + + $wmW = $wm->getImageWidth(); + $wmH = $wm->getImageHeight(); + if ($wmW > $max_wm_w || $wmH > $max_wm_h) { + $ratio = min($max_wm_w / $wmW, $max_wm_h / $wmH); + $new_wm_w = intval($wmW * $ratio); + $new_wm_h = intval($wmH * $ratio); + if ($new_wm_w > 0 && $new_wm_h > 0) { + $wm->resizeImage($new_wm_w, $new_wm_h, Imagick::FILTER_LANCZOS, 1); + } + } + + if ($size_type === 'percent' && $size_percent > 0) { + $targetW = intval(($imgW * $size_percent) / 100); + $targetH = intval(($wm->getImageHeight() / $wm->getImageWidth()) * $targetW); + if ($targetW > 0 && $targetH > 0) { + $wm->resizeImage($targetW, $targetH, Imagick::FILTER_LANCZOS, 1); + } + } + if ($corner_radius > 0) { + $wm->roundCorners($corner_radius, $corner_radius); + } + if ($angle != 0) { + $wm->rotateImage(new ImagickPixel('none'), $angle); + } + $wm->evaluateImage(Imagick::EVALUATE_MULTIPLY, $opacity / 100, Imagick::CHANNEL_ALPHA); + $wmW = $wm->getImageWidth(); + $wmH = $wm->getImageHeight(); + } else { + $wm = new Imagick(); + $draw = new ImagickDraw(); + $fontFile = $text_font; + if (!empty($fontFile) && !is_file($fontFile)) { + $base_font = basename($fontFile); + if (is_file(DIR_SYSTEM . 'library/font/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'library/font/' . $base_font; + } elseif (is_file(DIR_SYSTEM . 'fonts/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'fonts/' . $base_font; + } elseif (is_file(DIR_SYSTEM . 'font/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'font/' . $base_font; + } + } + if (empty($fontFile) || !is_file($fontFile)) { + $fontFile = DIR_SYSTEM . 'library/font/Roboto-Regular.ttf'; + } + if (is_file($fontFile)) { + $draw->setFont($fontFile); + } + $fontSize = max(12, $imgW * 0.04); + $draw->setFontSize($fontSize); + $colorHex = $text_color; + list($r, $g, $b) = $this->hex2rgb($colorHex); + $draw->setFillColor(new ImagickPixel('rgba(' . $r . ',' . $g . ',' . $b . ',' . ($opacity / 100) . ')')); + $draw->setGravity(Imagick::GRAVITY_CENTER); + $wm->newImage($imgW, $imgH, new ImagickPixel('transparent')); + $wm->annotateImage($draw, 0, 0, 0, $textVal); + if ($angle != 0) { + $wm->rotateImage(new ImagickPixel('none'), $angle); + } + $wmW = $wm->getImageWidth(); + $wmH = $wm->getImageHeight(); + } + $posX = 0; $posY = 0; $pad = 10; + if (in_array($position, array(1, 4, 7))) $posX = $pad; + elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $wmW) / 2; + elseif (in_array($position, array(3, 6, 9))) $posX = $imgW - $wmW - $pad; + + if (in_array($position, array(1, 2, 3))) $posY = $pad; + elseif (in_array($position, array(4, 5, 6))) $posY = ($imgH - $wmH) / 2; + elseif (in_array($position, array(7, 8, 9))) $posY = $imgH - $wmH - $pad; + + $img->compositeImage($wm, Imagick::COMPOSITE_OVER, $posX, $posY); + $wm->clear(); $wm->destroy(); + } + $img->writeImage($realCachedFile); + $img->clear(); $img->destroy(); + } catch (Exception $e) {} + } else { + $ext = strtolower($this->mb_pathinfo($realCachedFile, PATHINFO_EXTENSION)); + if ($ext === 'jpg' || $ext === 'jpeg') { + $img = @imagecreatefromjpeg($realCachedFile); + } elseif ($ext === 'png') { + $img = @imagecreatefrompng($realCachedFile); + } elseif ($ext === 'webp' && function_exists('imagecreatefromwebp')) { + $img = @imagecreatefromwebp($realCachedFile); + } else { + $img = false; + } + + if ($img) { + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($img)) { + imagepalettetotruecolor($img); + } + imagealphablending($img, true); + + $imgW = imagesx($img); + $imgH = imagesy($img); + + foreach ($matched_rules as $rule) { + $type = preg_replace('/[^a-z]/', '', $rule['type']); + $imagePathStr = str_replace(array('../', '..\\', ' '), '', (string)$rule['image']); + $imagePath = realpath(DIR_IMAGE . $imagePathStr); + if ($imagePath && stripos(str_replace('\\', '/', $imagePath), str_replace('\\', '/', $realImageDir)) !== 0) { + $imagePath = false; + } + $textVal = (string)$rule['text_val']; + $position = (int)$rule['position']; + $opacity = (int)$rule['opacity']; + $size_type = isset($rule['size_type']) ? $rule['size_type'] : 'original'; + $size_percent = isset($rule['size_percent']) ? (int)$rule['size_percent'] : 20; + $angle = isset($rule['angle']) ? (int)$rule['angle'] : 0; + $corner_radius = isset($rule['corner_radius']) ? (int)$rule['corner_radius'] : 0; + $text_font = isset($rule['text_font']) ? (string)$rule['text_font'] : ''; + $text_color = isset($rule['text_color']) ? (string)$rule['text_color'] : '#ffffff'; + + if ($type === 'image') { + if (!$imagePath || !is_file($imagePath)) continue; + } + if ($type === 'text' && empty($textVal)) continue; + + if ($type === 'image') { + $wm = @imagecreatefrompng($imagePath); + if ($wm) { + if (function_exists('imagepalettetotruecolor') && !imageistruecolor($wm)) { + imagepalettetotruecolor($wm); + } + imagealphablending($wm, false); + imagesavealpha($wm, true); + + $wmW = imagesx($wm); + $wmH = imagesy($wm); + + $max_wm_w = (int)$this->config->get('module_img_opti_wm_max_width'); + if ($max_wm_w <= 0) $max_wm_w = 800; + $max_wm_h = (int)$this->config->get('module_img_opti_wm_max_height'); + if ($max_wm_h <= 0) $max_wm_h = 800; + + if ($wmW > $max_wm_w || $wmH > $max_wm_h) { + $ratio = min($max_wm_w / $wmW, $max_wm_h / $wmH); + $new_wm_w = intval($wmW * $ratio); + $new_wm_h = intval($wmH * $ratio); + if ($new_wm_w > 0 && $new_wm_h > 0) { + $resized_wm = imagecreatetruecolor($new_wm_w, $new_wm_h); + imagealphablending($resized_wm, false); + imagesavealpha($resized_wm, true); + $transparent = imagecolorallocatealpha($resized_wm, 255, 255, 255, 127); + imagefill($resized_wm, 0, 0, $transparent); + imagecopyresampled($resized_wm, $wm, 0, 0, 0, 0, $new_wm_w, $new_wm_h, $wmW, $wmH); + imagedestroy($wm); + $wm = $resized_wm; + $wmW = $new_wm_w; + $wmH = $new_wm_h; + } + } + if ($size_type === 'percent' && $size_percent > 0) { + $targetW = intval(($imgW * $size_percent) / 100); + $targetH = intval(($wmH / $wmW) * $targetW); + if ($targetW > 0 && $targetH > 0) { + $resized_wm = imagecreatetruecolor($targetW, $targetH); + imagealphablending($resized_wm, false); + imagesavealpha($resized_wm, true); + $transparent = imagecolorallocatealpha($resized_wm, 255, 255, 255, 127); + imagefill($resized_wm, 0, 0, $transparent); + imagecopyresampled($resized_wm, $wm, 0, 0, 0, 0, $targetW, $targetH, $wmW, $wmH); + imagedestroy($wm); + $wm = $resized_wm; + $wmW = $targetW; + $wmH = $targetH; + } + } + if ($corner_radius > 0) { + $wm = $this->roundCornersGD($wm, $corner_radius); + } + if ($angle != 0) { + $gd_angle = -1 * $angle; + $transparent = imagecolorallocatealpha($wm, 255, 255, 255, 127); + $rotated = imagerotate($wm, $gd_angle, $transparent); + if ($rotated) { + imagealphablending($rotated, false); + imagesavealpha($rotated, true); + imagedestroy($wm); + $wm = $rotated; + $wmW = imagesx($wm); + $wmH = imagesy($wm); + } + } + $posX = 0; $posY = 0; $pad = 10; + if (in_array($position, array(1, 4, 7))) $posX = $pad; + elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $wmW) / 2; + elseif (in_array($position, array(3, 6, 9))) $posX = $imgW - $wmW - $pad; + + if (in_array($position, array(1, 2, 3))) $posY = $pad; + elseif (in_array($position, array(4, 5, 6))) $posY = ($imgH - $wmH) / 2; + elseif (in_array($position, array(7, 8, 9))) $posY = $imgH - $wmH - $pad; + + $this->imagecopymerge_alpha($img, $wm, $posX, $posY, 0, 0, $wmW, $wmH, $opacity); + imagedestroy($wm); + } + } else { + $fontFile = $text_font; + if (!empty($fontFile) && !is_file($fontFile)) { + $base_font = basename($fontFile); + if (is_file(DIR_SYSTEM . 'library/font/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'library/font/' . $base_font; + } elseif (is_file(DIR_SYSTEM . 'fonts/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'fonts/' . $base_font; + } elseif (is_file(DIR_SYSTEM . 'font/' . $base_font)) { + $fontFile = DIR_SYSTEM . 'font/' . $base_font; + } + } + if (empty($fontFile) || !is_file($fontFile)) { + $fontFile = DIR_SYSTEM . 'library/font/Roboto-Regular.ttf'; + } + if (!is_file($fontFile)) { + $open_basedir = ini_get('open_basedir'); + $fallback_dirs = array( + DIR_SYSTEM . 'library/font/', + DIR_SYSTEM . 'fonts/', + DIR_SYSTEM . 'font/', + DIR_CATALOG . 'view/theme/default/fonts/' + ); + if (DIRECTORY_SEPARATOR === '/') { + if (!$open_basedir) { + $fallback_dirs[] = '/usr/share/fonts/truetype/dejavu/'; + $fallback_dirs[] = '/usr/share/fonts/truetype/liberation/'; + $fallback_dirs[] = '/usr/share/fonts/truetype/freefont/'; + } + } else { + if (!$open_basedir) { + $fallback_dirs[] = 'C:/Windows/Fonts/'; + } + } + foreach ($fallback_dirs as $f_dir) { + if (@is_dir($f_dir)) { + $glob = @glob($f_dir . '*.ttf'); + if ($glob) { + $fontFile = $glob[0]; + break; + } + } + } + } + + if (!is_file($fontFile)) { + $fontSize = max(3, intval($imgW / 50)); + $fontWidth = imagefontwidth($fontSize) * strlen($textVal); + $fontHeight = imagefontheight($fontSize); + $posX = 0; $posY = 0; $pad = 10; + if (in_array($position, array(1, 4, 7))) $posX = $pad; + elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $fontWidth) / 2; + elseif (in_array($position, array(3, 6, 9))) $posX = $imgW - $fontWidth - $pad; + + if (in_array($position, array(1, 2, 3))) $posY = $pad; + elseif (in_array($position, array(4, 5, 6))) $posY = ($imgH - $fontHeight) / 2; + elseif (in_array($position, array(7, 8, 9))) $posY = $imgH - $fontHeight - $pad; + + $alpha = intval(127 - (127 * ($opacity / 100))); + $color = imagecolorallocatealpha($img, 255, 255, 255, $alpha); + imagestring($img, $fontSize, $posX, $posY, $textVal, $color); + } else { + $fontSize = max(10, intval($imgW / 25)); + $colorHex = $text_color; + list($r, $g, $b) = $this->hex2rgb($colorHex); + $alpha = intval(127 - (127 * ($opacity / 100))); + $color = imagecolorallocatealpha($img, $r, $g, $b, $alpha); + + $bbox = imagettfbbox($fontSize, $angle, $fontFile, $textVal); + $textW = abs($bbox[4] - $bbox[0]); + $textH = abs($bbox[5] - $bbox[1]); + + $posX = 0; $posY = 0; $pad = 10; + if (in_array($position, array(1, 4, 7))) $posX = $pad; + elseif (in_array($position, array(2, 5, 8))) $posX = ($imgW - $textW) / 2; + elseif (in_array($position, array(3, 6, 9))) $posX = $imgW - $textW - $pad; + + if (in_array($position, array(1, 2, 3))) $posY = $pad + $textH; + elseif (in_array($position, array(4, 5, 6))) $posY = ($imgH - $textH) / 2 + $textH; + elseif (in_array($position, array(7, 8, 9))) $posY = $imgH - $pad; + + imagettftext($img, $fontSize, $angle, $posX, $posY, $color, $fontFile, $textVal); + } + } + } + + if ($ext === 'png' || $ext === 'webp') { + imagealphablending($img, false); + imagesavealpha($img, true); + } + if ($ext === 'jpg' || $ext === 'jpeg') { imagejpeg($img, $realCachedFile, 90); } + elseif ($ext === 'png') { imagepng($img, $realCachedFile, 9); } + elseif ($ext === 'webp') { imagewebp($img, $realCachedFile, 90); } + imagedestroy($img); + } + } + return true; + } + +private function hex2rgb($hex) { + $hex = str_replace('#', '', $hex); + if (strlen($hex) == 3) { + $r = hexdec(substr($hex, 0, 1) . substr($hex, 0, 1)); + $g = hexdec(substr($hex, 1, 1) . substr($hex, 1, 1)); + $b = hexdec(substr($hex, 2, 1) . substr($hex, 2, 1)); + } else { + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + } + return array($r, $g, $b); + } + + private function roundCornersGD($im, $radius) { + $w = imagesx($im); + $h = imagesy($im); + $new_im = imagecreatetruecolor($w, $h); + imagealphablending($new_im, false); + imagesavealpha($new_im, true); + $transparent = imagecolorallocatealpha($new_im, 255, 255, 255, 127); + imagefill($new_im, 0, 0, $transparent); + for ($x = 0; $x < $w; $x++) { + for ($y = 0; $y < $h; $y++) { + $rgba = imagecolorat($im, $x, $y); + $in_corner = false; + if ($x < $radius && $y < $radius) { + if (pow($radius - $x, 2) + pow($radius - $y, 2) > pow($radius, 2)) $in_corner = true; + } elseif ($x >= $w - $radius && $y < $radius) { + if (pow($x - ($w - $radius - 1), 2) + pow($radius - $y, 2) > pow($radius, 2)) $in_corner = true; + } elseif ($x < $radius && $y >= $h - $radius) { + if (pow($radius - $x, 2) + pow($y - ($h - $radius - 1), 2) > pow($radius, 2)) $in_corner = true; + } elseif ($x >= $w - $radius && $y >= $h - $radius) { + if (pow($x - ($w - $radius - 1), 2) + pow($y - ($h - $radius - 1), 2) > pow($radius, 2)) $in_corner = true; + } + if ($in_corner) { + imagesetpixel($new_im, $x, $y, $transparent); + } else { + imagesetpixel($new_im, $x, $y, $rgba); + } + } + } + imagedestroy($im); + return $new_im; + } + + private function imagecopymerge_alpha($dst_im, $src_im, $dst_x, $dst_y, $src_x, $src_y, $src_w, $src_h, $pct) { + $opacity = $pct / 100; + for ($x = 0; $x < $src_w; $x++) { + for ($y = 0; $y < $src_h; $y++) { + $src_color_idx = imagecolorat($src_im, $src_x + $x, $src_y + $y); + $src_color = imagecolorsforindex($src_im, $src_color_idx); + $src_alpha = (127 - $src_color['alpha']) / 127; + $src_alpha = $src_alpha * $opacity; + if ($src_alpha <= 0) { + continue; + } + $dst_x_pos = $dst_x + $x; + $dst_y_pos = $dst_y + $y; + if ($dst_x_pos < 0 || $dst_x_pos >= imagesx($dst_im) || $dst_y_pos < 0 || $dst_y_pos >= imagesy($dst_im)) { + continue; + } + $dst_color_idx = imagecolorat($dst_im, $dst_x_pos, $dst_y_pos); + $dst_color = imagecolorsforindex($dst_im, $dst_color_idx); + $dst_alpha = (127 - $dst_color['alpha']) / 127; + $out_alpha = $src_alpha + $dst_alpha * (1 - $src_alpha); + if ($out_alpha > 0) { + $out_r = ($src_color['red'] * $src_alpha + $dst_color['red'] * $dst_alpha * (1 - $src_alpha)) / $out_alpha; + $out_g = ($src_color['green'] * $src_alpha + $dst_color['green'] * $dst_alpha * (1 - $src_alpha)) / $out_alpha; + $out_b = ($src_color['blue'] * $src_alpha + $dst_color['blue'] * $dst_alpha * (1 - $src_alpha)) / $out_alpha; + $out_alpha_val = 127 - round($out_alpha * 127); + } else { + $out_r = 0; + $out_g = 0; + $out_b = 0; + $out_alpha_val = 127; + } + $out_r = max(0, min(255, round($out_r))); + $out_g = max(0, min(255, round($out_g))); + $out_b = max(0, min(255, round($out_b))); + $out_alpha_val = max(0, min(127, $out_alpha_val)); + $new_color = imagecolorallocatealpha($dst_im, $out_r, $out_g, $out_b, $out_alpha_val); + if ($new_color === false) { + $new_color = imagecolorresolvealpha($dst_im, $out_r, $out_g, $out_b, $out_alpha_val); + } + imagesetpixel($dst_im, $dst_x_pos, $dst_y_pos, $new_color); + } + } + } + + private function mb_pathinfo($path, $options = null) { + $ret = array('dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''); + $path = str_replace('\\', '/', $path); + $last_slash = strrpos($path, '/'); + if ($last_slash !== false) { + $ret['dirname'] = substr($path, 0, $last_slash); + $basename = substr($path, $last_slash + 1); + } else { + $basename = $path; + } + $ret['basename'] = $basename; + $last_dot = strrpos($basename, '.'); + if ($last_dot !== false) { + $ret['filename'] = substr($basename, 0, $last_dot); + $ret['extension'] = substr($basename, $last_dot + 1); + } else { + $ret['filename'] = $basename; + } + if ($options === PATHINFO_DIRNAME) return $ret['dirname']; + if ($options === PATHINFO_BASENAME) return $ret['basename']; + if ($options === PATHINFO_EXTENSION) return $ret['extension']; + if ($options === PATHINFO_FILENAME) return $ret['filename']; + return $ret; + } + + private function checkLicense($key) { + $host = explode(':', str_replace(array('http://', 'https://', 'www.'), '', strtolower($this->request->server['HTTP_HOST'])))[0]; + if ($host === 'localhost' || $host === '127.0.0.1') return true; + if (empty($key)) return false; + $parts = explode('.', $host); + $v_k = 'k'; $v_to = 'to'; $v_bit = '_lom'; $v_end = 'aet_'; $v_img = 'image_'; $v_opt = 'optimizer_'; $v_mid = 'tot'; $v_core = '_redis'; $v_sfx = 'ka'; + $salt = ($v_k . $v_to . $v_bit) . ($v_end . $v_img . $v_opt . $v_mid) . ($v_core . $v_sfx); + while(count($parts) >= 2) { + $domain = implode('.', $parts); + if ($key === strtoupper(implode('-', str_split(substr(md5($domain . $salt), 0, 16), 4)))) return true; + array_shift($parts); + } + return false; + } + + private function mb_basename($path) { + $path = str_replace('\\', '/', $path); + $parts = explode('/', $path); + return end($parts); + } +} diff --git a/upload/system/library/font/Roboto-Regular.ttf b/upload/system/library/font/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ddee473e02035bfce6244746d5d2a281616f7ff8 GIT binary patch literal 515100 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 z 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
    mu2rPCfyzpnd_56mlTM8BofRO=&{OOj)sMp!DFP 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

    tn_OM$3a5p9KO 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 G(WQ!j;`E@ht~h$Q=Ys6(9N2FgaRcmQu6?XN?)= z>G$JKizhqQ2jNvA;j(;~<4-9+MeD~uM$e=^i}F}3%1Orf-StqiwX_dc#mB!N+w*MF ziQc?P=PZtr#P)n+QJ!W+Tg@?0a&J%3KHCK1jh|x+?3I^^VS!^9M}^q)f5IC88=E!J z{He^mG&FVdU*+RZk{W`AQTI~>@29{p z3D4aJbLQO#FR2XpNkD;a9lAY;bw^X|k&|*;7tF%gRa{u_P5JmriItCMX+O_%YiuHG zsqirA!wP;U!lz$axT<{wQG}f zET%Fy75V_q1eVX7aY0GCL*!0}yH?cxqmQm?-aKn(U&^?eG`V(GgQYDB43^hL%5V5x z9BG@wUE))M0rGbwZz&i$FY5Q{#o;2LfP5_{i2!GtaE2@R1nP7b2xR>mVX@M}c@Df^ zjPo3w>Aa0-DBp`6{QexYe~jCwI_*31`w2L+`4+zqIq%2t`_b`lLN(|;PSr^Gck%lk z@vlSexqaAqzm4CIi+>wD%Nm4!v*i209)3Rq{r-dD5Kj`G#rQXH>YvsfMuX5fID=FR zH!hAeQ16qYuojRz>`0?g9^_zFO=BhFIoyWD$ZFPoqN6uB}_M4y6x6Oo!#l;gRwCS5O|F#7=;@la3 z%j#aed%N(oW!YUad$qXC6d^C}dLvSdIx-k8)%K5|Bn}r1bcT-m znHn6xty=Ux|3Dvj;e)A98%Hm$!T?J{m{u%2d9o7zs>pAv_Z3>ms~Qm z4F3+EI(g`)NoiO6s{V?0y*$4&9c$0;+wbbyy&RhM>wi`4WJC>|nrYn;{~0u`i5qc> zj1C5B8sh57wvh&mT+O(ucz0{2S`WYGZ^D_cCv@U;3Rew8!%DGv0 zq~w?MFKXStQ+`U{lsfrE4H~s=QYSUFPJY|GhDG_c@iMhMJ)oGGA_c;IPpMY<~l8T=?qL4e)sdJ1}hH*j-SvjdT4RM4xEY1E~>^e?B5sp)!c z{P-%w?y>$(O|LETU+a#xBq`^PCR%agnF`M+w zXR#mSyW`uSZ%7W|*VHbe3el3bQW9}eU#cHm6Tpa;)(d%ArV=8H44h~ysfqKQ9W%<; z4W7zUVU8U)E|tiOgo5_pn!+`8i^z{q=z01SAip z5nmMAE#e#E@8N7;9h?|!Nqmu26e%c*WTi_wIcfus64?T!FmU}u3lD?PabL|BQvG>^ zS~GcS`$Zr9Uy<;f}~6VX~W6rdPR|@EO{-xq z%lZr+(tF;jJFX%d9XK_`u4RNkOOm-F@lCtfype`VAOpFWj4I@h^t|Sx0ZTeJ?{e+cV;Xgw zR@9*78G34zNM6#CM6&-?IbzO?zg^tDZTQmV89mbAjs(j4*cmuqvIOJILER?s4CV=L zS7q}APm3qUGZPYX_M&Jeu$sy#FIQ-qcR=S2o;-Q5-E*p#G}Uh9_(|CV+rm-W+q!Sg zRek#{$f*{KK7y}a1AP|0@*YADr#p9wooP7w}wHd1#w!Uy6r+;jn9!2!~Ovc@MK zvk#!Q4lq-9z2WvMk)!opqSmlWQo5r$eb?q9ZEEZx#pAasvfB65tktr6j}|Svo8s{v zv3N3W2SrJ5Tn0PxvbG~d4!4srsYGwUm`JaUd8V|`H7TS|;FPeb@z6LynRv}hp4Zeg zReXQM;d?t^wqsKx-g174{gRyj#V-4+bEVDEzo350eAJ^@g4#t@F&|w$5f7?H;XQOq z1?&7L<-Jw;FqCwokrC)fKeGy1Se?t*N~|Tw8Exvx6VqPW2JeJmfhFk(PVcWr7+A> zh!gzZ#;1IM!iSb5LoYcUJ}3YBe;=Py^&EVx`oD*db@B}lKD72h?R8?Kh7j+DYRPse4rvRP5I!d z(+0*&=S^TsC4(f_Z9#UWtYKIu;mm2?_VWeEFXp`>#j-KQZ=` zBh^TWm1*p!YB+C#fB%zX7totV;B}nO^4HK*dzdN)MYTpe$(|L84!$1y_cpUftZ86P zu;0n2g0-pWplK{a-JT8z)14^ro0WEd*{nVL ze<*u!*^@)h-@5ccGjl`vt*dVc8L?fr+HctL@YaKy>{kD9&hK1=_|*4^2Q@GTL=CPI zD3tq8_<(Lp{Zw=>E8{q&1UQq0EG5JqE(7qx;bimt~Gt)g_!g)ypWP>!|{Z zJ69QV+A7U21kno$3i+nSk__?;N6&s`;lF2=Jy2uj3)j4Or6_xJ>EpwOZ(jb0S+Jpe z`Hg0Wlg9jI%TC=LDt%{z_~D*|at!tbmH}GVgLid(G*{NB#X6=c#-Q!F8UR~T^}t1( zQ1OVHDHV--(YW{=O8R+QBkw*SzFzk>k9!p6t{28lcNlh-L-oxSm*64xO5pq?vtiKn z-=4|9N|a_})WVN%J1pv6_vsxgKfb{%dwltdEv3tEecUX(FTTuv=~Rt%<;xcTc5ju) z2S;~5e)I#DU3;8GaZ{4rSdv44bui;!ZJ zMaC!`DQn^J@u%i9?Btpp|XTh4~40^ zx70)3E?!<`W5zcR9JWu*d3V*Ok25!BE}XP{?Y%e68&R;R40{Gf?)sk>KX+fjjO$+9 zk^eNjUeg!@Jbwb7r>&`|SVcL&QC(AU>-IXYqAHWO#v1(MQcr0`a^^i+sck#dMlHl8 z7Q`ntYvz%ha!tjMtf@HrQ~PjLan0d{Z+#JKlCtxj`{$o=ll|QlcW0Q5mSl^Zj|ZC;(S?TLed^qg!o+VeWT)kxG zj(>l>ZS~^Ehn>G^$z5jClcMO(>&t^*#alOD+W(RDT^5S=CsY3Q#{J^EwFjVvFy_BO z57kHg$Q+}IadEVEQ@%qMauRangr`fn23$Xo4Tx{2FIpvoZ}+KEwFq1D!Le1O9gQ(>j2-2gsW;WG~_1%m})}6(jq(3t& zBV87S&cbV{jxj><@*~@=*_&TFeq#60Qvv&<<7;LV%!-b`qw$)>SKfB-jYnRcENUJ7 zTGX?T*uUEUgH`CRE+~3?WaRKQyZ1i#*qVJbU#RzKJrj)Jo~SI;q|f5YvJ`hUow2d^ zrshk@S_JnvR&lwDq|0;r5Z{}LBBeFc4S8)QVO+QoRbE<(LG~7V_nsHN_(&XiI~cK_ zvp@I=IRbmBRQ}QTW1qe)2&Z}RD!~wVC<9exheX4T@92=0bQiAA%jv1}BcYaGlj35Bb)0hXIaC*y7|p577-*IqE_q<|mhGb2*N)s|Gl$%)$LbQyXolp-TrQW zzFXY%X#Z)wkR#WBhy568G8Nf176yXr(=J{lE{|;wq>G!L46fouE!Fz|+A4&c)HVu@ z(Q;(I(#EU+ z>FNP47teP|m4-c`U5I@Rm(_43UNOWx-OMk9s)fnVzxZ`gUz~a36Z7`l#7nQd73hh( zo{vHIe17My7himn_r^y+I2Hw;#4@ASV^}tF{l3LmbJT{tXlvXbwc52ONwIk62R~au zUJ$ZvDVHDYu#+iF3-P5AX+=kZJG%Y0MhIUoxTyKI?3yjQY;Z!C5CM#wMG`0^Qq&rr&Cr zH^mBrXScE6Q=7tw-q*gmU!SXL^Y%O0S<4uU9D{|RTMl;2+912{K(t2JGl_CFIFy8+ zE%?gGBGAt%$nJBuRS^MJIWAeVVV#QTN0ymQ(6WlWDhFjAqgIITXiKZk7u~^9)S@t> z?=~B(ue}-xm5mY8!md8zRn3bCFZe)h4p4}=MS7$dFViC(4n#T>(fcmAGp-BYr-eGJ zg=_&K16W4U6)_!)YZmie>2l$eRY^e8+WgF>)R}vq`SzRb|DI|8DpJaxDW87#qR4r} zH!r^X?Qsrv7~%m zEX=sHN6#5qeGC3_UP1H0=QZz}b!o42rlivx1dL^|pRKRizbnA~q#L4v;toVJuo$v6 z@Uzg-^**h~sboq8s%0k60>@q|q}-YqCZmPmS} zC0;uFHT1syfk=DWHlSq?ZWbX2ko~`osH06QA|!5ALP*iR+ME}+K4t#n?lU{p$nUvy z=CzUY%ETlUgm2F=q{&0AF86LZ@M8J#O;MDme$M>$)`ho}AGEss_Dqspb!tQ>Gtf&W&fQg6k1M4jZW1{6fwB1%6&Ft%-a zq|<>&r=my+(h~5Clej{f0*ihMBB5F^_?9kR0R5d{65I{a8i7ibMzGDNJ6@iZx@r6N zZ6aXDX7Aej;kG-Lj9OIim#w$l@x{P_Rn8uJ@j0>d9^=RLOa&)r{ARy-`?b~!cduW6 zw|#P{eb9~tcf2@qsNH~|8MJE?R!A+N$#E+i3qDjUUb1EY zwU`sh#8D}1cq?+%o>Lix0u5WVeWK^kbDL$EC2elnxYZi{NKt;VO7GaCH0!GH+VZPL z4+<}={?IaO`?a^%`0Zq+N;P0i#+~iATk8;~BYD0z+DghZ_N%Z3ho8QCn$!l7a@Uv( z>w-V)l6`7Xoi1*U=E;A;Ey#t%y=N`(4|&TTS3jEFkQHhz-29C;^$mWilT87JXrqR# zg+bxAUb`FQ(#$0#gF%+2I`M%)<61B8*+jT7m4G9Lj0}ZA0RVe;gb< zWx#+jkKMMYbZvi8t@oqX?|c2?HLu-%sr~+>fzxVs?{xO?erI>?m;OIDT(D?Bry*0O zU$N@0S+~vXV1GPk-O-0%A3yt^0iw8n*T3{16EM2;Iiu~FgF0YW9{vOVGXl4wUV3$7 zeAK8xCRMT$D>OwX9mT0`RUs-=$MJVla}6vDY`%}J1n-PsPXzV@yCJE#rIrkEFoQ%X z+>wrgG&Kj=Kb!r^<4c#8i7Mvp=i8&#m}aYMTbVWNR_6mP6YCly_ycfDGct|E(O?$S zGf3NFr@@iuQUzqAX;sJ-g#z(~X8qVkGi0L-+@~A%*;_w1+hbfv zL=8NNanWt;7e;GBuLDKYuut{c97TKOoJLK9*P$kE1D3yGP~N+xaEXksaCH_8uDtFb zzh*wpGVzkSaB10|-CuuoaBErF6AN!%JkMJGqP6GYjsN~8(83-yeZshT0=98ne4jPS zdIH!rgpR*78fw;@FvNx=c7XA-iG!!QLV!uGGJ)YS^vIQq;?mk=rDM4?6M70%}reo z?g^}!5WA?u8(2vbueF6^_D{@77BuSeXrOi-8ZrEUY-gR zbIenl>;dpHcAXcPR=&zwUVdj_#;Lj+|4HHD>}(*4><8LbA_ zvRYB?#2dARyipr5@5Tjo))D*o>aa|y3K{l$;*fJ?HN>rag!OK`Zs;UxEoUHmalV0t zhs-eAnpQWrE_#5n3gb5M?crC$N5bESaX+c=zWBtO>k-nx9(_92Fe;x73BB;#)Y1j- zOA9=Y_)~M5OmzXc&ST3VpU!g?uaiu3rIX3_c*%5RiIx0}708+6-NfGF+?HpAR{E0I zZl1mln7Q8uGYn!Xu;W5llC84Gt7VXXH@uOHI(ZChp4S6yId%25=L1 zxd*wM~!uK$*`Qx$qyPX!hP zcbsA6mx2CqC2uLp{V7TU3ss!Hgs8`p9eZNU{r2T2?`tW^6g=OlR=-hCIr^SqeHX75 z+K2p$Gy`|GljL%!q6v(AYK?S~Pt;)#xLsiZR$9%SG#)=+%|~txosht#=$$%s?%WB< zJ3IMWod<3HOLXBK7+O5)j;n6DqiB|wh%IXLtJRz0Nq{CSc-9&)Pi##0N_I0 zCVreCezU>0j|OY494-m~FMu%|!x&!T9FlrQFF6W!RcI77>+)U(R7sj=6oCV!sh&qL zCW2QUa`KJOAR`6^Hq-;8%f&IZYJiWc-@f3H()7|uTdEOfV8n_1g>xIH=P%}~C(<%= zGmA2jNXG$yWNb$CNMtjMN#kd;mzYcy>287URx-MWET((Np*yiN)BQK*;aV%jRg!O| zzW5N@C*K%uAU!ae-na>IOgbPpX5K;Wc3t^fT^uQLDNIQoHHqHu)H`%xG*>o3vLLd+ z+(`%^Yg@{F4HH*oc0)oxNq#qzui^eIuj}B}#boiU_!%bs1kHN#W- zWwCMQ{lP;eqq;Y4nBOp?Ve{JIlF>bzG;Go^Be!|&X*fj7d_#N~`UHHKhIn32nit$3 z#@yoPM2fOjcey2+6=nhcEE1A^GEZ*OlfU&8HUxely@bR0DK(=()hh&6l8}0y0 zThExncM*DZ0$E*XCERC8Xl$NzkLQ794wA@oa&wAu z0%Lll&dj+!XJyWY91GX&;-xv;XNaPiIkfzUV7D%dR{j+XMnGx9MEST(s>DP^+)rKP zL2Gxh$DC&VBhX)jjjIGM2|+9mwpQM=Q+~7ciT!=>E9h$6{et@wSt~!0CUQlQK=!*~ z2||qLN-E`Wej#oQc}nKA2hZo^b=-NA7x>&bBUD$UiCwYVQ|%olc9B{8@;aiCyw?Y3 ze{Qf|G~WrnZHB}Wjw>`ZbIt1J6J`ju0(=4uYFM8jZkvWx-Bbx7>FFc)hSw;<{sJ2`eVAyZGvJOO_tM}jcr)!Yu@7Pv;BBoIT4r;ah*pj$u5u=)}feAn>kwOCOOMRe##&2+isDe_GR0+ePtFW0a>^IWn; zE>>;n2zM%Olm+h+@oMZ7YL~d!y!h0DJz_+lOiZ0QYH>l?^Us%^vR=18U1MJx>uA1o z<*ado%VXB-Yu2m*U&)#ZYnZQa<0T=_9Bm*=7yN?_CH+YcVJTU(ddtv(Nwl_B_6zC zr$RP*gH@zQ4Dc@GG2ORf!22LwjEk#$u)^@-f<>2?e$bIHw6IXnh+uLA$nhz$I-K;H z`8DYxU5?(=B#)pYuQO;6;+7X)`0~q6JsWlI-}{>D-g|G)o?s9A}b!P)$Yo^ zYpEE#X4^^;qH}!pu~u&o?qn9g>oK5RdZUbgCu9M*%mMnAR!ceV+$ERTey-T=wpUVK zTD`LNVxFC1lV!5$LbW!+d_%$qF`_L)X-k4_m2KYG%_-9zhu?A*! zsmFY~`bOL;hKimQzNry9&g_dR*Wav?-x%TWMlnh>#`}48FYBG)ZoH@a#>2+d;`R71 zcu#x0eZptseG5rfL#e2BzHT)DchWJ}(e{q<`An}y>dv0PK(8U>ZOIJpfjxEjBV@Pa2 zS8Q|JbNHHQRI{@781rtoy@sEo$IS3qrX@bpjN`6M>-KdphI~B+Y#Ccm^B8bG8G3B4 zSnT$p5-LioYMeM%AY_0}h^La?Gr(PnuAKb?eO}rkRq2_;_H)IgB<)*P(mo{ST~-l# zO9AN66%0H0je|=_EWgyMfJrM~+dUfZX;ht6&9BD0tvDVzL(@j$K3aVvZZ%Hux|DUi z{-zb^*(u?hvGN;woU|=?I75j3bk3m!E`^`4?iEipXV&$7fsvUA<#L-iap*`u$kTH9DifXO2 zNTda|R(c)@AkWA;AV%3)I4D z##v+5!bV^%SRPhuylc(DZ>$rTR_cMH=ZKLfWZBRO_mtr2Bil`vYpK2xaki6-^X9K&r+vHph6IwNXY zv^^_-tWUKB6inq&9IlVF5JTN}@M2uYE}?jur-k;_KQi%L@266k@f zBxJTVqK(idY2h3GdH>5oe7SYb?1_uEU9o5G({EY@c`lI_Bg5@>d-I%dLGMNXx^?5z z=a0W)Qs48|jC}eby9P4cT$&vl)$fCSBPI}E&4d1WCp-dt0)LxjVy_sXWLhPKmgZ_Z zPx;s1unfUoaeMfIsy(lFNe8%gCILQtl>^D(YhT3=A3ETyB+wllCSli6xWmJLED3Da zmt)w$ooLUvqkY{ZxHlo_yhcgHeZKLj7^%jpX_Hqe?(>HU)ml@gv4cCUANI0u-<;?u z<{Rt9IjXhxuj*;olq=OTWJ3D;Q~PkUz?-N^)X#_>%N3VLb8{)uLqG7_?5(fn9?3Ol zM~~2-#?p@=cpH5cf8RPJcPy&0M8{Fvqx2R(5EbO{5>9zPrReai! z6Hb&OVcVcK4OungFVVU+SrH;c?hVEzq@f_~uZl;N)Ji-e zpg^4o%gm6+8L-)eXjdMxGy|XZy+XJno$#K0V!`1X-uNn(Z~b%WgvG`7&RKV7nhjGH zKYLjxJ@DzHERpwp-UG3fIItWWbpM{AXPXZ^cxR2}Ye^4E{yAIo&wOK}E02mlOEa`Q zGXJn1WZoC^uh6otxkt(R)zGnrSjU3wko9@S^GWnnV-1_nTrli`Bv)b{{0_&<(*9?m zqemp{BH%4yFELQaU&)oyQIW=4P8fEOzu2Re|U)fbHK;CsF`@rUW56NG`Lg22|poPqQZltFEpO) z9Vy*~xCG4~3OF&1e{kiW{XtqoyYZW4bq2Qx^6YN_!G*N3APy27s{v6i168|OfX|B%I@WUGZ>uBE``w$BM zCGqnxCyDTNd!x2JQp^uV&>rxOkXZIvD$Pawls!aD;W-^F-|xpiQpUv1VbakOpQT29 zca9W3rBxL@jVEFAeuK`H@WV!tiMa)ws#q-w|?lW4I&W7s)vl z*NBda{+!A|^OddSEWXtqV4lIU07?t*1>rm%?hW#1dKLC4LKim%Zz`l~Ngeq(86nq_fP0_y9ouGe*@LgyRx^w&5;x5&m zc^-R4awRP3X{X|x644*<8K3F)CiFW3wTaJXd;o{uV@Bvb>A3pAQiTs>?JaAk;p|DN zIFK{e`+&=1>~AnGXnO(tjiE*~H-%gc{MeeCQj`KO>N(mcB{c9wOf2d(ERmNgo5NtDZMy$;=Irv_7)uH@U2 zPCCH!TV|&Qj&Q67aQJ<(qN^7lTo;~=wV4B7+YUD-@5d)3=3lLChtg~~o(QgkEImVd zDnv5FxMMEI1#8lrqqT{BY>VvD46}Xxo9uIotSB^icAvVwDlsXX02yko-4} z7Z|fI(f*r?07M!{UFgJ3(-my71UJy0Q31AVXC!R0GaO%low?JsGa5R3hs?_|?pvba zuMpFc#90ecjw4Z=hD&-t_CYZw*~bdw3;e-Zp{qVh8>b$};Rp52qu{mu%sV9xFD!I< zhrc1-sjFoYb7j8Hyn|C8ER#>54`*)KzS4RpUGvVPkjeebJAltTGLd;?xSeXQVcr28 zmdRiEeF8pM2W)V8N8{5#=`3R+@OfF|1Kv5Pc}HWD>G%dip${+T*n4MZtjpclxaP~| zdaPJu1fFKtBc1jncg|S(Y&?y%xgKi*eE2nMlEK%w`{Bc{ff!bRu5h;pS(md70qz_Z z-N17`*6tm6n&}7}`Tdyqui%BDvADNAWcEt?e!L%x{BTcuyf0?|A`$ob#%AzyqRh6e z6!-bFG~G32RQ$OeaKXMR`0@-n*Wwb!B^9?RToCMm_f!!Fdd1ko@7rOlp7w|VBbEi+ zQ|&?KJ#KrfJih1mV1H{3^DyAUZp}9yaCt`0Yjur#D)v{u30(y}ihgPBC{_oTBDWx# zR+khA{TPBD9E+8E5siT(?-gKwWQV)IqxT|eX`3kHa1?8&I2>Y*7wMRzwxHS0oXUL& zijOgDW45u=jgKkVku2qp$ao{`Cx&gzzF6xg72%6yI{HcCP4O{__Y&iK(8Tdwlx>UD zN`OuJiC~i-e5%??3gamF%lSsn0^+Pl z6yk3%(q;AnYZH8{M#lMa<>xyI$RDXCET3_t#VxYu8sYF%RwIh&(TNFn4Tyx@gfprc zp-{qtE{p@}lnu+N%Nf=tvGk_3D!wjsVsO#n9WPoq{a3Rc( zaA`=v`p37d54;cD7bC{^8EWO`;^r{lo(n%TKpd}uTEgbyAy+NT^%7R+I#Ad$=otly zY@{Kt0ZDF|T%-kyS6^|7yo4cfD31q)5GKYJc->uGW8D=+=) z_51gjJ7T?mULVLhgBRK0DA;vlidBhme_-RgZ1=9PWvX9 zZ%21=IF|Hu8c7|)PlLYj?BzjE-{Hd7v8Jk8U-;wX(9=ydbRG9rGR?4gZ?PNUf_#CU zF28DDEFC=V@+)=$tgCsvI4i~e*exzTdadUAZ_xCCfAV<8I_>k^@oG7$rtK2L5B&-m zV)*bIFG~U+ISdYc7(T}Z89v&lB!Q3Xyq)$5_(T6b?CM{Qzw7V$@rTXsp>2o8o@WdG z-j6U0BX}K4*Hgh)HTSz~B(2-sz2L#9_k2xAV63+|?XQM}IJ}@lp|ppl&D{8d$WK{_ z_BT7U{8B7b?Kw(Gyw*zl1f%0uJNsKpo%ZC{IPkTPrG0|nbK*H@3g35j zG&ctTj?j;p__6g!&iBY7q_?x9W8oHWrA$N0d4!BL0}sU*FU*Drg&*yG6dCr>KouCj z2!60$M7u2DKP%J_`2UN?OZ(UjedER1fYNGORubFOKDKVpYdmW2+G2lu$kt`LHB*yX zrx9mrT&g&@&^~r!g^7k-!}PQb!nQhi(>``XWdk(iOw*gUBXPM(eZ%|M%`1H4&)bpT zTqD1M4}*PdqY7{xXP;}ewbtSNKfxcgk3ALdYxqkeaMiT#N z#*;3Mbo+eJ$lcGLgFdS8nC058xACfr6MOYE<}5vi#P+mzuV+~u0(0X_hQB>{bBb=Q zp|d9q{^7Qazr=<1<285c)^&ityZ)_?Ij+z659c|XD~erw*sc@)u3cx@4ZbL47yLYX zdV%tgm4=YJ)4)fz{n=?N_m6WG|ESibPW~LCoLSf(nT4}ep8aEvxB-Lz<^D0J;V14N z8^61Me7AVVvws|!68rVCXT_2E;`V*k@>5r6hiuh!QN{k!S{}<2G4jQT@7gj=A$9DV zOm%6>D{P`^uI34Ud(80_dX9DLJV*a<+u%{qlwrfq7@7n&c+?}49$esOEQZhP$gm4t z<3ci@%ea&6g`d$*x7OTHms(?-QWp|0NL@I)yXFO@3*}CqZcDttZN-x58m?+vnnoUj zZcAgNwqcCXquX?AXjvK~Je8&J<+p%u>KV@2ZQfT)$~Agl4W}P)0JC0EG`AkC_YwT? zo-2&t1ibfXH(CcNSNYHHtc{3$V@^BE_3YQiUI;Xdtu?c8M0d#=%}PtBTFdXTL3-jN zrZ3Nhq}lunD$n`6Pq?kM|HiPH z=VW{2D!V+V+c(ubH!)Ujb!0n^_8h+&Vy~0qfjuF=2zh$wcOLR>G?v;fv!$i+$5+E< zN&CY#3wt4TUHz>sVp#PX2bIioSvN@=H#=S*@e1$W1M!5r7xDty=P?~5{3Y@JE*-Q@ z3~M@suqSZ|`+sOpdlhfWd0LFoOl0~HoRC<0N{>oIfn{ahZi#uS&NP@HX#oA8TPtlx z>v>0hm)b#V1ot`Kp=GxHFlY(SK5#3;8Ap8@b8$}2nQ!gaah5L8-44^abkH4bc|OwZ zN9BCr{R;V>V>;IbHuz%spqIjf`g37GCNGJV_~w*-1jNNxy5r%s8|gaN*IjA{==?1D zM0>_%y8Tc^IM7pD+_}&)@2-c~w)0#|TRq$On>*KvCuQW>^-XxLr7y6=h`R7qdryuE z!w)q8p0fQ4o+l?BqT9na@z~OFuwSE?@9_=pyIDrpV>@GO%5%lB70{^ArIDVkI<5}3 z!&XzK5!y42s;l|3TsY9peY8H%*0jE(o6m_K3moP?IbMf8ySaU8U!a-p)88I8sGsI1 z-MWQigU+!B1rGB#IR*(oeK?OZ5yt#68b23?gI_aSxN9jMW48Tw(2Cbsj5n{b#QdQe zhUNlZ2~k_5vt5alpxuT^_kH63}BJ`>q6Zj1d3tuYkNGMcF1 zG7qtT#rqK&JJs5gV=eKZwUKOpD`KyDZREBG59R7O9DS3`d?&x5{Rr*DY3#B8sLnya z{>`!HSXE&E{BbyC|5QaWcA*mDa6uV|!%^K-PXw!C91ivV6QWkAE9QyARhd7t{d5(F z8};&U){`s$cGF56OaJNWZEc&49wx(ZQ!aSrsX$Ko@vCmQenFrbSQ|8j1rPqf^Nj=r zvI)qc(VVAse2&(Ket#8wdX{djwGE#f0Bml{{xJ9xI&#_!O-bE09o{fUmC(K~blg$K zWfcN z*+et5O}s_?8=S&oZqso@%ArM26v1E4q*dd?{ziz#hiBSXUd+#h`Wd-W_&dglzjM5d zpAnCf?RkGSMB~NtlQd5LJI3k!o%Y#7REsu_!$%jz21Uv@%!gukf-2TZ~vJ%=p@wRn-}T= z`g5$`Y%Q*gpHiLxOFPAJ-F(D}oO^i&+5>OQS8LHSep0npHIGW0={V)>_V~athTp?! z-^Yc|@=5SHqnhck3;Zx3)YRbz@NJ9F6m-oEPMp$M30!_=-Z>YrLuQ*i%{lA|5#$Eo z92~f7Kvl%p-3RUO1+BYVT`*TGxli81#eQCh_HgO%s8ReK|9u`mBhnk##?RO+#NQ!9 zffx8$^7f~|`7+9BU>=5^Son>KU}>BYt40w#V1Vx#Z*I?c8`I|L_8RZHXrJ%2XZ(nU za*r7_KgXpZTW%U@Lrrr@7os7-r#WgI*HqL!xHLz8Rj5t~`)%eR+1?x;|5#C7w=dA} zN!I%CnJvf`^jz|DXUKDuQ7ooeDZXh-F=Z3-#Jce|H>S*EpqQ_dhXR|#d!w9RGbw6t z^>L{Z*odPk>jzaPiVET2m^w<>c*-h93+i%dJ=|^Ot<{Y>?6@KVxqqIFKqP@v<+dg$ zzfh}YKG%6dwzEYQd#wD%O$tyEiZ+Z;1^0qqJ``Dp?)v(M=N`Rh#fCLv+RGE|Z$7`v zE?@rKt_Sb_`(5UezOUZB`NKIcU3K%CYiC}#;F1Lo%`AQEis!Gt`JQVIUWvI+&@+EZ z&XHpjc!_ld>s)xE(Dn2j-omsd-4qPB1-~d-FA#0je6qhtw06%O4uTGNk#e-mIW#x$ zOWt#HwAg1Lg4fjhmTX(N|Kz%t=bHXbbB!@Gzi@l-3!drkaqbVchx$W#Se=K7^bp*?)VkS~0H zAK(kh6EGJ!^!t1KTeWWF@jyO3<4M#v%TV9FA;W_Y1K#HxanzHedk)h7FWhsW>x<{} z;cdR>pd0Qvcsw|F&t9n(+>VVa9ED7DDN*sd3ms- zPs5J>hi9+i+wB43LWVI!&u{nO*TCr(iIeCW|GENBX&NVaW{39mK^LqamJHRWUew;D zUK7CDd-q+9-exOzpOEb%$*Ox!GhyEqXS;6&&v@f;WD9C&ToU2WHJ^6jYrX64zPcOX`Bb=taCN~bzuwGnlU}O59W)DoNr)vmBrK2?N7O9U+UY_*fz*@Kv+B^uIGMd z*wg5Y65v3mA=dDau}|UiADwZRx}5Af>vHhp4A-vfd2!F^`g7K4UiP^7D_!Y1Qz~^U z=>yiSX#at$4|MygdLBnX_vf+h@66*FYc~s?LpsVF$j`N)s}G010KCVEeqx7phH+By z9`l7mZ+MN4zX)H;u&M^1!#5H6P2kP=8|oXjk0f)R+!oC1XnR0P59o0A*}yLsTeorl zbL|1CZP~BEI)UB?7xMd|_IcK&Y(r+#`|yY2KHPyBH%8jSP+N2m`u>~q{+xIj-p@mO zPmKb!|CHNbWS{3f%ZvBJ_&v)H+hE8KzHb8`_XyJka*O8#zXY*xBixZ~^ei6p67j2R zqnWSCM%U3kK~3)r*KqUvB_KE$~Cm-f_&lc9u{mJvyq+`Lq?6V|0Ti8ms zXZXKcQ^Hp(!8sdA=To^4__^yb2aax2=NC~?)N?Ld=1wr3VZT6E&W&LE1lxgU@E_*(znGf0rHU}orD@XPl^7vTJc$W}PBFHijKuN5!>{^Xa|5Y+yu!8r1BaIaZF z&kW-hcqPub^?Cqn!boc+BM!fyj)Tekf*J)e=mF;&#V@4|U~hV@wG)i?pgVf@oxj0$=;9*39QdxmYpv+ z5=*_bK`$lFdW&@$or!RC+H7IDXCjnNi&WLTC3Vz2Xiqu{dzx1sSKc4TWdL8c#{5ft zNpzC>a+>Jm(n-NCrP?NSHJv1UqLb8t(~L)4I%yq<+#t_4G7tMds~I74nTg+FkD0c2 zyZo+wFaLS$$(WCm(*T%};s8e6W(Ajd~s zK2fkssa}S=7cb${{7Lw~cUD}k`w;VU)_p5bTTIKW&dG!Csba;zi~4wt^E|1KuvlUI zkIN(4ud0PS&rHp~cR}Yp&tpM6h37og?MKV3-$$H`b-=S6BUvfuVv^Pj9Qh`h)hFwL zR}}WAZ5VYP&1+!l3XL{anbX%BZeJ|zH1D~fTQiN{I@q4;gQfU(6~p#i5A2*{t+a$5gx)VEYjvuUTQgiKZO8C1J{O~SgmuREL!VnWgZP8}eayE}h&^o7&Vn2t z$GjT_RC|WLA}&&2>Hb>N?CbG7smJqz@s((;u+x3i2z&GKB!AuLj3*p^0ny4|)i=5a zsw?B#S4lUwpC>ErhqKzGJn;Ox{XAJ|KV9^Eaz81w7s0X;e$n_Y8J}T1ZmgHb6UJVi zloxz+C%kUQ4*15?JE5PNx}T%yr?>jXN4LcB9MSz8ML%ik8(%-3@h~scko|xcxSodQ z1t;%7^%wE@zNAZd%}KmK-@*6Q+~9nt`i!_f@J4$xogYVk&~I4ty=m>>yvZ?uhkN4i zp16tLW33GSk-jSYK*v|zW|fIyirZ8m)^o;9uI)%S0S@rlX5HlKCT&68IJJb|M#Eoj z+$OZ{)9^VHmGNQtTw8|VV_$ZZKIfsK=Q+Gjus;jGs$fI!t-u))XWrQ>BRh}=ec{S7 z;9C*adkjCkYoykDE_~<<<}R%LkFhV0yi=wC)RtikKGq( zZuIrR9Tfv@pXomK zVH}xB`=A==U=iRY=bH4M>|@_WTJPyTP*F`OvTbMm9=Q~L5OtG3a9cDabf1BkFo(SN`bBNE#_9REabkR@YqkZHy8h`eWLnL>A!*aLWl{?lSgntW-KfymM zOnxHpLEg?T)D&r}=3B$h2j4c=^G$thW7(iS;1f*HvY~tM@>E~+ks3zK#OY%<&VhEX z&_^En(0*8aH~Q$nvLXA}eTkM0-A4oP6kQhIEJ%q$UgQ? z*0SO2L+!oExj3Zz*oSeXB<-Vxk5(PnCy;&YyF|-|uMZ{T%pa;gp+PIaf+5T8_- zceh3Z^AF1g`XSkvB4qErh2@q2l0kJb`s&wAiX zq3x+&`($a_$g_b&8$?aH{oh>LsK`aBd*%r6Lk(Dm?{awIO{}dP+Vfs4*;Gg<842(j z;0M!~HoKkn$R1Gbb#6g@J-3PQdyC#Ke0}b)ex>kx8*jSsd9FzB)k}a+c7gE+?*v@C zpz(LlA~XJ>S!mDrgLm$Tmsh~w&2^CYlRqQjFULNrroYC&q2?V)ABtfTeDKcHV-=ty z`o+HdGW*_8gAn2-1Y7bBpE(9E1kx=C)6L->?1W)XYS}nz$h?E~ zJHx3TpNRJyALex^+A}Xa>Eg~7mgY89)1G4uY;$BBU>jBcv}Q^H@) z{+Po%I@VB4(?-TJNVgMw&?ZmI6}P6D&2nfn)_&RA9D)XQDC_)+nI*%!L4m$4x} z2^G|%MGe)NE~A}&kN2E&_(N0X12wbnk#sT1v!M9q;K76a{54Z?df{w93T(qIH9sp}b&WQv5^#o!bP7JI7w zWLDuj8gXS&jWn*MU8Bf**(?>9uR(WyL~wUemRlByN^5)4Cy*(Tk4i`u5+lO62Zyg@ zD-NJSJ6(@eXb$~WT>6PHZ2L&;jaOgY@$I)_?8*)IEVqXK82DuOp06xx=$ifeWADZe z-@S0b3Is-y;;&8sCgRT*Jplg5 zo10^QF*mOZ$4&M7fr+wJ5L;0 zBX-|ozlHvPK@6=O=#XtJ^z;v%!TtNDR1t;$q7q7@H6(><6h(__;6%l%_~D&j+&jt? z#FZxAo0@P<4(@0|A@ypwd{EHE#l)d*HF}g&FBZip@o1Km?qH6*OT@m9y=k8RwYeg8 zxmB{}qmS0uznC}1y2Ubyj@vPx&7e!NRULSqPt?3|$B{_XgmI)R>GE(VuxFnAv)GHH z3%7m#`L_4pzfN?0-(DoPyf04JRjudNJo@OG*vn$GofX4z9^kL?3bB3}5v`I&ohF$M z6?0|G)pSHdlENWepyH8#+=yDm`2&fUOI{)?Xd8I=!yWIhvd=ptt`^t4XP}*ys~H>t^GU zOL|b-F=$&%drVk&(euTKq5Q_ravj|fe;*21A=Hhfzu${BUTc1?%g=Y?xoxl%nH%-8 z>F+n=xiR+OLq-FBo)nJ#*7ywp@o1ZD<_^*jH-UdKIH;PD|Ja73}a+ z)+g9=tS^3-N{Q zZ@uoXENOqahbYM!l}lv6Nc9)SSv|S7OnKw<{c$ML?eBHn-!}9&Na3l+SN9Oo$&kcb zd4Gpb-=C&S;`pAfYBjfY>HE|4Ow6^AazyjVa)kN((w$EwN2nPD2PaaCQkR{z zwUj5CPnIXl=L&Z|b?m?GA#Bso-O6XG?-Kia2K@~%YDxal{be2E>n<^mvLEcdJN>-H z{W$Ht{m=#%l@YWjPA3ep2AeHjbN9qmOg!S&G~)c2;1;yUx|Q4ChzgjAdl=ny zu7#`xIs)xmNx3x+C#i?*%57pD)A^Tb+0xl^92vz}5eN6?O+%;MD=OAXbnCA!hux{h z^*e_;xJ*R60`)s_v!bdAH5WC_y|LeA;ttlIqjA>H8~a@*q;2E&s(zkPh%(ys`iK;19vBOL z>=SN<_Sj>A&D>#i7Tpw1nohW%2+Mpmj^?x<#pA&K3V&m||>|3&>e^BBa)-%PwK<7(Y`g~ zG#@^r_Kmqcjl;E5tPeePrso^&rJc&abrJ*>II|AqdSy}vE)@^EI#%hXQnwB<>n5&O z#{LJ+u6lI1(oK{bO9Be`8|@L^+yk8XzRSry805`8!21c@Uh*`>##wg4Pl2ZgDnC&3 zbW8SETc$@kup&S|axawpBIa%QMT;>hj-RV}JO2<$@I>;{BSq@F#C^W!(cf_87wP`+ z9bKoS`}tJ&^B(%KRD0bI{Nj$XJ;M*3RO11?o_Fh%D)^L_>(GnG!St0fx|C`GC>yEZ z;|!Z>pGdD~fd3$ckAfd*b|}))i$D2wl1^_ioxFY^#S$=H+N8vIIeyRg%3%G@{>W|6 z$&T*RG0!TRM`ipzX|AJ+-$&9N-s7?IT6YW2J@zlH=A4^;13%w~XM>-Y@^ixZHyb;K z*6Vz>_`FH*V{qm;m)gbO!~4rP@9;dvd75!0_}{v5ZdKek}6xzb9JI ziDi6$7TSNy@XzGumhgq%AX*p~@iW=t#az$4kafXQK|ZMCOX+ zSsS=L-><0ddqE$3zan^Qu1I$8i<{@b_mpw}m>d2M_muH>6ti%~tZz9f^7@as=fuKz znmc2@Bngd>o9@US=X5fS`aA7gNE(HTq*Ziiw3lfFm`sZ@9`ji2%~C$7+>62B)4F3fUe>>pMQ|&eU>W4UG80{0`zgro6T2=F0%=Ow9jB6{?c8opUYg_Ps zp+no-eCEE&Y+D;uTlEv_InBdn5f3(m~k6 zWPDFb`GP;fUXhe9r~W-)l6(bo9i6h7_doetmb(Y|y~F|e@)8G}E68(oy%z`1nRJOq zNa2AT`$52?8sEQB=a7?F&$w*x;c^7G5Kz6zW%dRiE=R1@8khg%{Pl9owhA6Wb?WZ!LXEn+ljJ|UGyCpxB;W7%_lHkzclOPWtjzAt+qWxZ_^Fg`P-SBOl0FmrOz$(VPjjE6`<&6|ias_RKH4{U zKfbzaXr7+PHy2j81C@!Pd3~Gv9^LnhzE|{J)0YKjmGZHo;Qj~Tk9$f_guQSXVFzLC z<@Kab_L5XWa9L79hnpFn)x9=pco&+?ahtxeqwBfOCECaW{Es9)AH&65 zIfCt1D>%PD_{J)KJ~_+;*fKGfn?d;JfDXm}4!Y+_)b%Z5jxbUI`l9W>F62D5^A@&@ z?+M`Fev0(BgZ5?%d0-rbI$No(SA%b3rG4e!6w&x5+P#nmeEI@E(Stnjbwx0DzlGXA zA7^=A94`2->V&>1XZqDOyTMS!=X1C|=>$5+_jSQ{%L4x^pLd6GeL{JVH~+o@c|Q!i z8^Dft^CPSovHq?JBc_dj!-pRk0m^ob;HL`14><}XZ!vBo@3(<>FmB5=1&kQt6AHjb zzK;fc9|69Ly1ADWC`f=aqdy^jtAc0L0?`1cjW|gSLuIQ zyLqtZNSu>k{sr{GBO&n5hJ6RrkAiR0^Y@%0eLgM;`p_>5=Eed2A^Cp6&tWjvN&0}l zq6hk7hq;(X2!Dm>cj&KJsn+HDT`*3=w?_GX2R@OMqKZTa2ow@YN=j?R41hR&ZAbl+|-MNAA3+@a_$6qwuZ{SO>LOe2=w| z$9l}T7xq}w`d7-cW~cSyBPVF9LLO^7z!&n^uYoO6B)*h~fFKcEl(jyU!mp|31YQP-fau;o3KYgxWrd3nfk75Sh%M82NN zHy)HPjMm2aqOHSj34xzeHyva0-)ivtKIX7vK%cpY4=dS6_&BYMT{D#CjbW&>f;&f` zZtfR#=5$_Fct;7O<9q#Z=iA-z`8t!^RHyT8edqigB{0_siS%%1LpS`70{jN;mje8c z2!B=Q72WpRzGXr`0{SO+UeRs8?OVpHB7IfoiQVu&H8v3biQVu&$4|$Ns2f)LYc>*2-*26R# zzuE@5hCf{#O+I>4IzELE)w)%jq@eYCm)zx_pR-I zRu}T%wvqFI@1SS>)h!2qlLC8haU}(HfWE=M0blqAVccxWmua9wa4s*N8zjo~5Yj=* zmuawf7;d$uIsi{t;awfzJ2*2>KUmx4&*KLRU2sz2Z+$J_meis{#ET^}=_N#XJ(qb3No4{XgcJsP5iF9?nO6 zp(FZHF`uOJXAgNepQ1dxZ&P_ysA>;+${ZZwv+h%ho3C zA%SC9#(x1#7VU1g^s28#dgnde@GWCPC$zz?uFl_z(i5G>^YpIGi_#}{t`q5YQF^lT zLXqCg)7b^kmf&5IVi^@(uuB@~fOEcEdaCm;o{oD1`Prj*rX;kh9M~!9vB8;=BZGE= zXHe$xtKdBGP4Rc>6@LPpGPY2*tAPHlP^S6s#JclfM&hVVbA)A%9(0(f{MrkT{C8sA zd00jg`f1Nxf;^qKgDtES_oITZ7Rq*fC+uK_y1Sz&y{dCIPw&{FD1E}lr$zd_qV&YZ zUqm|W=;%g&V#i34-d>cRgu}9cKJHUw+$K!qwjAp2cxbOwchXb1c3@B!cphjTzY6Ml zQ~WJ@#h(D*NCo{NpuZ*5ME>59Vjc;o?@jY;^FQX9sD^sT!}$yrrkS_fVm?VYL#t;# zMR`R3#`3CAUhg3f@86qu=Tnd;rL5~A50qK{4!6#Sps(2`oCC!Dp`xA4Q*>x2j~1m@ zb=L6o4w&C}!=JG6O_APMl%CkwCDP9_*!%5?x2Z>Yc`mXpBhH<{vZ=UtQ~d1!U+h)_`kv6?r=ToTmu-p<<%6fZ zKpS}iN_?q!Zvp#aMBAAUU+Q#qK3$Yv)tTez9k5F3hCgBB`y#!qC_T|(iuCh~(kFI| z7wM-Ir6)VK66wG2baoN69h9e{<9Lw{!(6xYRL3%&PChoUG3bW^n{*8H7qUsx&$V`F zU}MlHm&dPC=l6gQB}#L$V*Cm4RxarG0ezSU70j=S^CXn>d&tB4eSS_>oM)oy_K=73 z$)9;roF@t27H%pV=M&`VYJl%FVg3N4wK&V$7~cgmdT9GiMd?*t19*DJCy+i3@nNzE z^M?-T_0T@HE=o^yi1m5r#G>?x9iIvONKtyS16m%`AD&;#`>1%o2=zA)`W)3?(EIaa zN-=&F?3LIQKR>1v<4;h|%=hVle)ln@dmfk%ED8EhkRupTdeC8_@=Y&1@?%4H9(+D< zbq{$sm*PCUZ(~_iz`YlnO4fZmK|c9BDY%zw(>&eBlS^Uze?ZJJVT}iMUe!5Kw1vHk z(i0oki}ZO#=}DMh@V4_HPZx7Tq?78HCh8pCPr@10JA)qkK(B^*gmxzMUaIq8?ms-} zgT?q&+L9jdcj^^?g8De%lN}FgaAzpn{9L@4M?$^6hdg8d$2=3Yt$N7A`3R3nkGdz- zH+#s#`4r~?KMa*uh5B(1dB#DW{G1l`$>&MIH?EtO0hAeSi-P*WJOOXy9`)+iS9Z!pNXoqHh zb^(`1(68t$5$R_ZrKdWF^K=E*ZDXOFgb&S_q;3oLu((|nuUYf>lhwC+z%O2lfNzV| zE|cK9-c8pcz2i@Y^LsbNhxS2Z4bCySJkZ~uUrbVd&dU?bfAhAm7=N;QT@Uz!1N!A+ zZm<>5$GOC$w*t!<%x>g73+H4K*D@x<4O0Pj(1-d5v~er~*`bx8>_6mX3C79daX61Z zS-rXk{9yrov}0Tzw0)CcpZBKn@S0XKH8*7a6MaKlLCE< zZBib8vUX=4Kd@ooSD?DB?f3@nUf^pXXq(C?V>h*i?`MMXt|!0BnEy3j&;FIt|21FF zewJvV9Z-6cwP$}z`Tr?z&wdxqSDY+|{kNAg>e>Io`4;7szLbLU?b#2LfV|?gxjf$< z^7ibHfif;U53S?Z0>2Bd*YBZz3;Z&@JP-UcJw7k+)AaH@@YnS6Jn-A}@;vb0@aKX5 zM#^8{$LZy{^yi$}u@Ux}(HdVFY3w!{-a_``8Q7M$PO#HG=jquyAbMP{d3yE`jDQ*v zGu~eF_3S0!BlZ8BuV+sI=O@n)>RGp*y#<_Kw|sKlACzOy9s}M2x92NRsoy5~diEMX zS&G+3e2jjmV`IVE9qL_kzyl05*GayEUY-X&gkGKpzJy+$2R?;fo(H~#UY-X&2L3$o zG34bBd=0%kmp+FT9pB{Vpxj<)r=UIVaV}aC*wx3SE_Nh0X zJT4n9hLjrg?jw!$KM3 zjvJo0s|oIt%+F!DJ^KRNwVccL@;sQ+Lc7V&Y4iCfbezcPcMj|2c`(QA<#{mI?d5qe z=jG3XId7hSF!$}{dAB(*_vhdo81}6`1$zr<@4lRu!2Tgw)?&`g_k$ol>>s%R-tiXW z?~unAV>Hc~3&z===h{W{7`}f@%zrGL|3F>iTw_yuJL4U%zKGA~_XAiZnGJg}f z3-lCnOa8oEasJhFzHp$gc;C|7S@pr*#r*v>0$=RQ8Oi6tP@d29uph^l^t-rEbZdSl z5WPQ{)ODEX1AAVc!@k#&iD88R&z(7V`}#M;v!}IZlLhIS%){g6K95i#p+X z;KYN`uHfA>t)5pGJIFCz-2KJN7ijVNP(I1mhg`eB{sr0YkO$!h^7fo-d!7sW-V|7e zR*JI~cLsT3ebn(ae=pH_R#E!o&M_h#b}hgd_cW9h`o$-8{K@^zomUj4Pww1Z_@Qrv zzGaK|!-y}&M6f9vVcj&7^A+nR*bCluSpL2PY3mF2udWCD!%cohJYQ$={jO8Hf>Yw< zN-O`ZTGu;(e+;``;v4C%1+Z43@0bGo&BWe}&H4p04#m{EEB|Bws!=YuxC zYmc*X;>Cxov1eC3?8q&a3-*j`^D10|OdpK`2bckC zNwJ3>e7LaB;gqh0`THb9Ti|<5m~0DOF9Lpp_>NrQLwW6)|BhUK1Dd~!SA5yA)LIPn zD)5a-T~DdE$#uTOUuy(sF5C9o+)Hh#i!ks=X!3|GA z8Htt*{SI+?T?<@gaXP5!09SkgFL6bE30xzc3F<4r9kL#rQfr3v6YifL@>Cx7Kjhg7 zhEQ$`U~l8LY#hi`Df;4g^=zeDmXG9A;1e&#uY#}PH^tBU#BpuS+Yr}-_w@&%UC@{z z=@;~M#OLh@_1^Q)edR8q02@wu82)O(8KmHqHXaam7kr1j4Ji+)amge21}(6o|F7Sy zb;5fL@Nsgxi@wQ89dC&7{fMIU$(>t@bns}xy<+^mJ9w*vPxKtoR&b6)egHlm(mb+f z|3T3_lJ7a;{swNxc|NGS-stu0Ux4*9H>fb#co@0wPWKDiU#0z$eE<@PbhC!gY^PS{Bc=`)Jb zCwKlR^jHk@vlm3)E5~x-^Ec3M@2)?rJp^Cy^}+cC!}VtbKBSw8f^?-HiyE!yLWRuZFJZA&JRJp8rWC#EqsSE(zVUG~l0O!d>B3Eu}vv@L~6ay4u_Z(rYOFrOuB5|1G$qw^raE>>~Yb1pW}9v%g56 zN%*xQ9ZCVduNwk%S_wbixgPWva>6~&jLQk|N|5g-0pAtCaIZZ)ze&LG)t>%|fT5%j zU)-h5_ zz@Oi@%q-ZU1A(YddV54^z_wRx{FlH|7;`hdr&tBMcem6MW zj9&2WU(o!2xU-j+*=+(J%1DL1ZgB5CaS%HbIfsG3L2SrEo%9pL9lr~NOiR=#$W-`l zk>|*d8{$rDQ6_VRjzwks|4T=>176=plvA~!2|5`237Mdtpq!SdBl0qd`6!nuf0w}a zu&>k;_fQ|hbyiQQ3+@pPN>$uA2m1bB+Smy~7AOxYpTaiBWy=U&M{?P~WwDZwA!R3Lw&u+4G&5>fupn&5eH8|>ERU{y4!bGrVfaVg9v zE-lyx3o?pLv0IKEvUW>(6x|j8Y{3tygCjBvj{CZUxm&`KV0Vn#(uLsgHKWFEamUZ2 z;A0GB6nvI(1;p-}H>!D*cJLh~kbv>UYe(kK3%jEq+>Je;e9e~qU=PS0TMIG(tR0p| zfg{E6meL)fWpHWfW^lhYzpRv5aPlGCWLmm8zpgYE9RbI+mFf1%BN~sn|AzJAozyIoc)|tmGKl!ZAu1hcZNuNIJ;QgnacJ~eE%~`$E@UsqFb^CzM1J&yX-Fotz zdGq#v%)Gz^8>lKP;oIMTjny!w9;&QS@vt+oEnOCkm@~Q+RBfb110#;m!w8u35KEJ80ciofpC<7Qc4(F;;hU zX!=GNNVsh9ts<{uXjcbdor&Y`dK66b23}`+KYrvUoOn8b|H1{tYusJ--|pQZzw<|H zhjs&D|1*fiuynOpELz43eK6jhX~9jgP}p&!WY7yY-hb~qhkbRyi+>GVH(>RV7hbPk ze*Q_T2dT?%cxF)NJoTz=)NMY?T=UP#Z#-~p+Y1jZc<7a;<3yV*wu4&dzUD$`vm-6A z7UIm=2gMz3*q8WS^P(@oI`R@2b7?%6eW~DHD(vSRU-WZj@%R8++#tkDNZd{Y{bC;U zJ0|+kVj)wpjXA&TXEO%%4`Z2-sapM7;5m@*7Y6%VZJ4pc7`#NgOr+22dd4__+a&vC zOjnOkwgEm#_-6S-p&Qsk&}~O`C-KcP%vkdGxC5QEdX>O0PG6#h^XbBF&ldC-stZNB zC`(}rVNBt4lxsx#P&kK)-;qmeWH=w`a3SZS&S~B5U=p3cvMfb=%7<^Nc-0sl*GU!mOgQD@8XNLVO-#=$OueJ#dc0^bU5iVwH@kUcKK zSC_*6@gDGlbrs^n-b|DS_WEy7=WZfT{yQJUhj(}+|3YH zp?>(8lmegEj}EiBpnmR@>DGJ9Nn1oA9Y!A47Osspj~60SwWuD z)Wh<62YH}A3%8C@IRkQ6!(SWx&49Ezr5*BR0Hcud_-=TdY9Z$MK%PQs z#aMF$-&T;P38dsYi~%jU9wqR>d>lSH%rXIFUMbD8iuZiAfT!I+FymWuylfKHy)7?7a_>S`?1RiPZM zkTL~YK@uj^QEqy3_wi8myTf;ndnyx@iBMz-5I+T?S3*@y24(hw z^Kz!Z88lOsX>fbPzEBx6VZYXFD7^jPEAP3mnmiD`x;q%YqB|7UM;X|Sd%SXj@{n>K ze3Nw|oHKbU_y}%<^~FlnRE~rD{(e<{Q&xi)?PORJe5CvWw?*Hi{Gt2~Uy5C&oB{jx z&QyL-PEoEKM4mwL}fW z{Hj!qsxi1>G7dLNm8;{`-PGOHJ=8tb3F<_(LQSYiIBO=YR;rWK$!e9lm%6t)1y&GK z)oJQFe@bJYXX1J#4ngVjUeeENB62Id2s#cGS%sJpfLE>oAo6#<9A84pLON2*7uN2|xcSbUs%yn2Fq zqI!~gGRy~8s;9!rX0>{ndb)asdZv1odbWCwdain&dcJyrdZBueda-(mdZ~JudbxUq zdZl`mdNtf=e=VFQM!Ur=9EUxM?CUQu6#lZ#(h-%#IF z-%{UJ-%;OH-&5aLKY;bphw4Y_$Lc5Qr|JgvGnkWpp?;};rGBk`qkapc*}KZ8>i6mo z>W}JA>d!F4`c?f+{T*h6f2x1M9qRw6|Em8fA1LdU50y`p_uy9FjWCn%RJ$}qQ#DP~ zHA6Gu)OSm>HAizbPxG~q)<^5B!MzgN0BxW)NZU-?96Z)rXj^JqX+yN3+Sb}O+P2y- zZ98pyZ3k_*2D5l=l(wU`leV+Ai?*vaS{tK{)k?Im7ST$zs20=8w751-E7!(ryJ@>? zduV%V6SRq1g_h8gT1rc6mD(h2vR0+-rR}Xv(e}}%YSXmo+P>NhZKgI$o2|{!_S5#) z=4uCM2Wkgt2Wy9Dhidb*j8?7HXti2ao3GVr^;%9_pe@uIv_)E@)}%FSi?tT5Rcq7Q zwI$k8ZJD-QTcI7M9j+ar9jP6q9jzUs9jhIu9j~3Bov59povfXrt<+A{R%xrX)3npI zGqf|cv$V6dbF_1{^R)A|3$zQhi?oZiOSDV1%e2e2E3_-MtF)`NYqV>X%eCvY>$MxS z8?~FXo3&fCTeaJ?+qE^y#oAi!4((3uF70mZ9_?Q3K5drl zxb}qhr1q5dwDyeltoEGty!L|jqV|&Zvi6Ghs`i@py7q?lruLThw)T$puJ)exzV?B( zUi(n{Nc&j(MEg|Rpnax&u6?0>sePq=t$m|?t9_?^ul=C?sQsks{N+@uKl6? zsr{w>t^K3@tNo{K)H<|ItxH#QRo8S~H*`~Hx~1E?qr1AN`+7+4qxaSO>HYNq`ape< zzL~zcK3LyE-%{U7AEFP{x7N4Ox7CN~+v(fuJLtpp5&B4dl)j_BlfJXQi@vKqS|6j2 z)l2lS9??tns2eKY;`o8)MeWpH3pRLc)_tW>+=jsRO2kHmu2kVFEhwAh6j9#tR=(T!QpRd>H z^?FWUpfA)L^hJ83-lR9{i}e=0Rd3VV^(FdJeVM*oU!fnSAFdyvAE_UuAFUswAFCgy zAFrRFpQxXtpRAvvuhdV~SLv(u)AZBzGxRg{v-Gp|bM$ld^Yrue3-k;1i}Z{2OY}?i z%k<0jEA%V%tMse&YxHaN>-6jO8}u9XoAjIYTl8D?+w|M@HTqip4*gF3F8yx(9{pbZ zK7E~jzy5&!p#G5lu>OetsQ#G#xc-Fxr2drtwEm3#tp1$-y#9jzqW+Tpvi^$xs{We( zy8ed#rv8@xw*HR(uKu3>zW#x}UjI=4NdH*>ME_LZpns-+u79C_seh$^t$(9`tAD3| zum7O`sQ;w@tpB3_s{f|{uK%I`ssE+_t^cF{tN*8O)I0P}y$epcR1MA04Z|=EW>|)8 zIEHI@hHr$7K1N@opV8kKU<@<{8JiiK8-tB4j4h3=z~4R8*xJ~}*wz?kY-en5>|hKx zMi?WFQO1sNj^@tBF2=6LXk&~q7QCckBVv@o$>TAj%!nJ~jB;bVv752Gv4^pzF~OK< zR2T^(X{3y_QE5yvCL2}8UdGHBER~T0sR~c6u*BI9t*BRFvHyAe>HyJk@w-~n? zw;8t^YmBwV9mbu;UB=zUJ;uGpea1StdG7(^LE|CgVdD|wQR6Y=apMW&N#iNwY2z8= zS@^Q+PUAV_dF3AEUgb&S1-QBIZsjTCML6C5apffACF5n|73F3)59Dd%HRE;T4dYGY zE#qzFVdW9y9phc&J>z}j17p4Mq4AOOpz*QsiSeni!T8Mh-1x%y()h~w+W5x!*7(l& z-uS`z(fG;u+4#ly)%eZ$-T1@!)A-By+xQ2(?*ADZjSi#J=rR>kH8oQ=4LIA4nHH=# z9n&>E(>FtAAGm9;AM8^bU=B0~nVXrLn}f|Q%q`8W%pvAb@O^G$Zfg!Rw==glcQA*W zBg~QJD04@1Cv#_W7jsv0v^mBcYnH$rP7$-zjG8gC%#54k%yM(QxtqDWxre!@Il-K0 zR+tGhX{OAyS!qr(C!1B~UgqBB6muVQsjX0y52Y%yESHnZJaVlFk8 znaj-;=3(aH<`L$Ra7+8q<}v26=5gln@Fw>}^Ca_R^AvNXd8)a}Ty36ao^GCDo@t(C zo(((JE`nQ;e}dcT&jK&`^~&kWIm)@_Im$ZoT=P8he0Z&Kp?Q&cv3ZGksd<@sxp{?o zrFoTkwRw$st$7{XsB(jOqj{5gvw4eot9hGwySc_(Yu;hrY2IbtZQf(vYu;zBGw(Ma zFdu~5+a5L_F&{M_Gau(SrI=5dPn*w}&zjGf&zmopFPblzFPpEJubQu!ubXd}Z<=qJ zZ=3I!@0#zK@0%Z(>&*|%kIawFPs~rv4d!R&=jIpYm*!XI*XB3ox8`@|_vR1gkLFM2 z&*m@YujX&&@8%!opXOiY-{wE&zvh4DMzh20G`pC>RHiYV8O&siSNGD>j4;g!Znjbr6(Jll=!&h}t?vI%S=t6&M1WGR+rm2485%&OR4 zY;QJ&?Zc+BX>2;%7tYL^$!4+HY!2Ix?a$`21K5G=Aa*c2gdNJ}u?(wbHLR9p*?d;V z>RFC0U<+9TTf`b!6KiIRSqp1rZLFOwVN2OEww$eChq1%i5$s5I6g!$7!;WRgvE$hZ z>_m1FJDHurR}+-pJC~iu&Sw{}3)w~NVs;6;lwHOyXIHQ* z*;VXnb`86hUB|9xH?SMoP3&fN3%ixw#%^b8*jjc6yOZ6;?q>I}d)a+#9lM`Bz#e1| zv4`0s>{0d@dz?MNo@7t4r`a>?S@s-zp1r_cWG}Io*(>Z-_8NPgy}{mOZ?U)8JM3Nd z9($jCz}B-5*+=YS_6hrxZD60V&)FC3OZFA}ntj8*W#6&y*$?bT_7nS={lb1_zp>xh zAM8)|7yFz2!~SLev5l;Qb+Rr?u~bX5bjz?zi&>UsTaM*gp5Sy)023P~F zLDpv0=GI_q3u{YjD{F`~)Y{tG#@f~zW^HF}Z|z_Ww?#^3WF2fBVjXJDvocn-Rb$m!S!=#k zXVqIdYk{@UYOoerjaHM@Y%R80tX8YdYPXhHOO;2hW!7?Qg>{&9xOIeeq;-^av~`Si ztaY4qymf+gqIHsWvUQ5J(mK^zWv#YOvrf0pu+Fs3vd*^7vCg&5v(C3Jur9PNvM#nR zu`abPvo5!;u&%VOvaYtSv97hQv#z&pux_+&vTn9+v2L|)vu?N6SZl32tUIl{th=py ztb48dtaaA?)&tgq)GEg)-%?#)^pbL)(h5))=SpQ)+^Sl z)@#=5)*IHF)?3!w);rd_)_d0b)(6&l>qF}!>tpK^>r-ok^_lg#^@a7N^_BIt^^Ntd z^_}&-^@H`J^^^6p^^5hZ^_%s(^@sJR^_TUx^^f(h^`EuT>aaSkE?coxTeEfBuwfIt zZP~W%*skr_z8$jr*nRDOc7J<-JZS7(9cJ}u6 z4)$<+ggw$8W$$S3WbbV6V()5?w#V3G?Gig|N99`>I0 z1bd=gVJGaQowCz*r9H`>Y**QP*?Zel?0xL1_B4CCy{|pPo@vjrXWMh^{p|hix%L6} zf%ZZ6!S*5cq4qpGV^`ZXcCDSY=i7C5y`8fc*bD6jdy(B}H`&ehV!Op|wcG4=dx^c& zUS==1SJ;QyhucTkN7_f(N888P$J)o)$J-~^C)y|3C)=mkEA3P5RrYH8H2ZY>4Es#` zEcY(HW@YCmQ_Za-l^X+LE@ zZ9ii_Yd>c{Z@*x_Xuo8?Y`qJ499esV>!0tIIiP4z7ul#IDMUdPJd^BGte32Z02n440g6~ zwsf{~hB!l=t(|S0ZJlAxcFy+B4$g3Agfr3^DfO)Kyz&Zsf%lT~GC0THP+nDDQ*H-W(%Z_L@J(dB z@}05(TuKYU5!RqIIx%oGEe6L^3%I%3!6~%_oKefbPrY0@OgTb1TscZPQaM^VM)_X3 z-zjtA&N!#s8Sm_-bSRz9?#>?0p3VfNOI4hSPKA?jl1|D=JC)8PXR=e}?B(q3OmX&6 z{#O2RraIG{>CV2+3}>b@%bD%WarSfecjh_=I0rfhIR`t3IEOm(oQzZL)HtP;m8k|K=qtoOxJBytbr`2h5+MOlNQfHa7+*#oq<{a)E;T-841&gIS(&Xvwp&ehH}&b7{U&h^d>&W+AZ&dts(&aKXE&h5?` zXRULGbEk8cbGLJkbFXusv(CBSdBAzldB}O#dBl0tdCYm-dBS zdBJ(ndC7U%dBu6vdChsFzH`2JesF$tesX?xesO+vesg|z{&4kf0bbGLVQaEH4i+>!1mcSm<8cV~AOcUO0`JH{RBmbhUz;+DEmH|CbPad(_s z?v8hNb9Z<5aQAd4xD(w9H{mATl$&-d-AV3bx60ki-P@hw?&D5%r@7PJecc)EOm~($ z+nwX?=kD*$bq{b4bPsY5b`Nn6b?3Pmx7w|7Yu&6n->q}&-JH9?UFbHri`+)H$!&HQ zyDe_3+vc{rOWdXIGIzPV!adAA+&#iQ(ml#O+C9cS);-QW-aWxR(LKpM**(Qw>7MGY za#y>jxu?5lxM#X&xo5lQxaYd(x#znVxEHz?xfi>axR<(@xtF_FxL3MYxmUZ_xYxSZ zx!1clxHq~txi`DFxVO5uxwpG(+_mlDIZ{mlK`{lfjy{mT8?{l@*){m%W~{lWdw{mK2={l)#&{muQ| z{lop!{mcE^{m1>+{mg}0@*l{ds2>TT_9<8A8=^S1N0_jd4xdn3G&-Y9QJZzpeOZx?S@ zZ?reY8|#&LVK3s9dQmUtm3eV*oLBCR_jdDk_xAAi^d@){y$UbkCB2lF_A0$e-ej-J z+soVAo8s-`P4%XE)4hGY8Qx59mN(m*KIHF%4>Mz6_h_7;0BUaQyUwR=mvrQR}cxwpbQ%sbpW!aLGC$~)RS z#yi$K&O6>a!8_4A$vfFQ#aro}>aFrtd#8D)duMoOdS`iOd*^uPdgpoPdlz^YdKY;Y zdzW~ZdY5^Zdslc@dRKW@d)Ii^de?c^dpCGDdN+ADd$)MEdbfGEduzP4-W}eZ-d*0^ z-aX#E-hJLW?|$zA??LY&?_uu|?@{kD?{V)5?@8||?`iKD?^*9T?|JV9??vw=?`7{5 z?^W+L?{)7D?@jM5?``iL?_KXb?|ttBZ@u@S_mTIp_lftZx54|&``r7&`_lW$``Y`) z`_}u;``-J(`_cQ!``P=&`_=o+``!D)`_uc&``i1+``7!=+vs(8onDu(_^Pk@x^MWV z&wR_beaClw&-eY1-^cIk_w)Pv1N?#hAb&G|bAPbEg}Tm6D<8SK^^SAT2 z_jmAz`y>33{wRM(ee!aveK%0Jpa#y{3S&OhEi!9USI$v@dY#b4>4>aX%w`=|M*`)BxP z`e*rP`{(%Q`sexQ`xp2Z`WN{Z`q|B?T( z|B3&pzrp{^|J?t=|I+`;|Jwh?|JMJ`|K9(>|Iz=+|Jnb=|JDD^|K0z?|I`1=|J(n^ z|JVP|-{^PvoqktH38^71q=$@<8Db$TWQUxP8}dSaC=}`w>Kp18>K_^q8W#8WS2D zDhY){kx*$U8j6L=Lh;bJP5xPHSq;HkwI}Yu1S06rhLAl*pZ; zT(uZFwb_QYOj~Y0ET-OtZvq8~?RtwTRH1Ef}=(Uwbx+9$g!ON8V7)Q#tQfm)e=s`DI7;(IfWGzCTuR-d>~`<^5v2Kbq!5zAWbWlV6G9| ziOU-sjpkf)*5W_4)eSC%oKbdJ4U|k62_~Zy#waW^CUKGK0+G^be3Guju(FIo6g3h~ z3OEu@VHhc)5IIC5*y19QQc6dTkqB~(loDJjVWb}^CHkdU6p>OaibyFIMWi%=^h$|; zDe*5wc_XDLZzM|cL`j|~$rB}cqQpN+{G-G_O8le5KT7>qr^W-{G-G_T4{=Q zY-L(&T5`>89)+SER*NP8%?Wx&4YZ2}tCpHht!Ng|6_=Q;yy-jQmnYg;>I9yQ08-zA zI7jMV0M|4$!Db0>L2Xl8L$Pt4>xG>4 z0>2*mJ&zbUP9xXUmI*smGON?DC|BE-YpBhd4b{+jtp?JhVR=(sy}5|f@x^a#wiV1w zt+_}LZETp2ou(1-ovD(dJ1s4vUels%T?Q4go9lDX?Q)G~3#aLcUno={iE*T3w-6tz zg$TFI&$S9g{2&G;=gg2y{2&H>$7;n~I`nsM0hbiI52)<{+zK7K4YFDg=apu*aY3v$ z%we0AhVvbNq z9W*Os4+nIe`cVwobADY8{5vQ;UvRVlJnskF1WX?|^{4r;*_{TR!rwLaH~sdigt ziHNaf1x1r4x@n@DCc0^&n%}vk{o0{vhs3naYkxJ68 zlC-NN?J7yTN|K_Iq@a-|Qb|%&k`$FBMI}j5Nm5jj6qQt=l~lTwl}=qtW=R${l~p^L zn)Wsk>r<0!scBy{zahJ??upNR@qSV8YdssfH?R7nB5B=-(XZ z2VyM};|f$?o^-ghuXq57AY(y#%onOdixZ@TPz3aC%hW7v$j)ygxF3hbPQd`Xu?_|f zUShkTeuO?fYYkx^KF9=eNhPn^p!OtABwg^Kq%2VGFB4IH1r^f|wS~qbrE6W>o`EJy>bGVy zu%}ZNtIcf!r`%5n#Vwjl2s#3Z9TLroHv=J!6oig8rW#gG=|ouG=|ouG=|ou zG=|ouG=|ouG=|ouG)DYmXl+VkXl+VkXl+VkXl+VkXl+VkXl+VkXl+VkXl+Vk#J`OA zml6Lm;$KGm%ZPs&@h>C(WyHUX_?Hp?GU8uG{L6@c8SyV8{$<2JPWCy_O!;WZ7L5>ogzzJTA0hk*@r@AQh|G_zB^tpd z8jWBRjYdeGQj(_>n`X2WyGE3jgwaysM@zvdEd`^r1dP%WFiK0nC@le_wDgOT1&Y!V zFG@?hD4CcjnV2Y#qD4B{VnTjZxgeaMW zD47KKU{y52D4BvNnF9EzlG3T1$rMD%6hz4sM9CCH$rMD%6hz4sM9CCHsq07M#4k?u z8AsC-jZ?i*FOO0$kH)Fq;#6;Os<&}yTB75q{>D-LjU)Qwi2gXDKaTVnNBWGTb~BFJ z%{byej@r#Q;yaG`jw8P1#J8OImJ^?H;!{q1%85@o(Jd#s<I+Fl0+{_@+65KEkL5Q0Es3^-z1eMS+HoD^iPxiG@*`?{fUzOiIV+^l^}kM>`tr% z^^B4IiIt#Su@aOkR)TWHN>HvC*{2xUrx@9%SPAMID?vG9B`9Z%>{cvHe8X72u`ux^ zI~EHQU$SR0vS%@}XR$EWPb^Gy!`Pl;m6X?kM#~P@yB`xnD}G815Es}-T@~4vEBhD{juHwCjCpX-cvaK zg;O~GK}h^@`~#Ty<2VQ~@yBrxVB(MCA;82R$3uXLKe|K#CjRIW4X4m03L&<;6uLwK z#&(xNmngv4?ov2z0*vi0h2ti`RQ@<_0!-zP<0imV{y1)iQ#dt;kjfv&QGiK*98Uoz z{c&mzFzJs|bAU;I9B%<8{c&mzFzJs|bAU;I9EZax9ETw!{c$`7nDoc-7+}&Lr{(~Y z{x}{3O#0(^3^3`B<1xUbKaR)Y6i&?{B>i!G2AK56@fl#!A7|qLlm2nyA1D2B{D$YG zKaSr3lm0k<15Enk_#IB+_zfZHkK;GMq(6?|0F(YWegjPWar_3D^vCfVVA3DQaezsG z9LK{c9LFIf{n6hEFzJuuI>4kqj_UxE{^i8Kob<`a?&5i{csA$ zeF#Z^9QOex{c+p}nDobSA7J8-<37NoKe{#nCjBdje+BWcApT_cQ)Ks3WcO2K_fur| zQ)Ks3WcO2K_fur|Q)Ks3WcO2K_fur|Q)Ks3((a>YGn_)tCWKV}=-C9A${*($08{zn zJOf}Vf1GCkOy!UB41lTplcYb+J0P9(M>l6Ug>Fs=i9fnI0Ve+F<^-7dqni_8;*awe zfQf&K%0ET?Q^Y?-<)0${DJp-Q_k>e8?}3o&KTZ769SZ5hAKjq<6MuAv0!;kT9SSh< zM|UW|#2;Uw0ZjbS5gJaRBNRg7kB(4)sr}))5Ma^==V1VoKDZ7Hr)fM&(|DAo@h45= zPnyP`G>t!L>L+RHC+YH(J1N_;2;7d~VdG1WaGJVG8mENeG)@U2L{ZWtN}5DTlPHzc z=~}C)?(O!&AKIgD8UxNV02cimLgJ6}41kG0&NBc``zx~WQjNd6$~93f;2<9NAe+Zr4y6|$1XT>5$fLz71fB&eM}i3QpmlJNlw7h zLb0noXhgY2s9cbq!Nv#h2ik4*;Natq!TC+Fe}RWWi)yZb#I;F*YMW%Q7GlfC7QFzl zrSsn=VXO0UO59MPoYmM4aJ~>J!TAD&)Twd405ElGoG$=Oof_v008^*N`2xUHk7x%0 zPWsvQmL_-s%UkvGP)!p!)g@%tw6w3t=31JJhFnX=SOo7F%*G5vSu>oO0Dj08DD$ko zw5gFSKv`*-kp^-oJI(snQ10+OqulGeC!Fa33`v=8+(Ju%<5o47$h!3uDFp?qy{9TOjj^>Cc?`jkS zVu%Ob4e{P1xh{GX&8$7Ay?o< z2_canM+=l3S1pk+8MH92S^!3C8pc%%z-UdwxM~3yt!WrnEdZl64dbc>U@>cg5L;{* zS1kZziw)zdB@)I}3xsIR!?62# zopumn{io4s2Qb!u8l83klm6(m1DNzjryangKRWFICjHTA2QcZ6PCI}}e{|YK(&)5< zQ0QM#hUpLrxgeD3LN0(sxj-o7f>4wTghDO|MY%vI_~RrSVB(JhE5O7b2UdWIKiWlri9gy!fQdiaMSzJv+Qo`8w2Kgu z{%98gCjHSa0!;d&T?Cl)N4p3x>5q01VA3D$BEY0S+Qo`8w2Kgu{%98gCjHSa0!;d& zT?Cl)N4p3x>5q01VA3D$BEY0S+C_j#e;gJo%5YeOko3o45n$3Ehed!%e;gJ8CjHTw z2QcZ6&OCrge{|*nO!}iUuc8bm=Ma+qI5`KH^vB6Lz@$G;&H*O5r3hfJuLJ zQUFZ)qi3Qbj`k2jm>kp$V11lf@U z*^va2KIp4e9m zE+O#x=<^Sq4}T3NgoozDZv*4|Pg~ox);4a&D$_VDK`5FFgra#uD262nMe~GE3`-D- z<_Vz~mLL?(6GG9fAQa6LLZJ?XqIp6n)PeT^LXFBwY;F*y!1tUBSBA?fWF+B$uRWJ( zT9(TUhG!*_bnH+J;}njwzzZ`5oTnDJWq4?FhbWIY+^;gGbx}>TE1WGD6)qSNwZSbT zA`W-^Pz<~`h$s9)h$k2T8SZ#OG-Lru>;RF#=3=lv2K!+!2ZPxd%)($M1~V|&7lY{- zOv7L*2K!(z1%tgY*b9Ry3?^eR34=-zc(qNil^V4OaR)s(_3OkFdwI4|L|n0{6Qg2h zCq~8gP7(ERkEe)R`{yv6j-e~Ie2OF+_I*;Ml^#yxAN`0lpVt%Yb1Vd+g+O^BP*DgZ z3IWs#sfEJ@Pr`-N!exa|;)TGtLZFaOxR6hzv;ZwqNG(!GEmBA=Qm8_tkaVOQ>0}}4 zR3VTq1PV1RFD$q6LUGCq`IHxut|%#>R#7NuqVP$gkaVK35EI?<6!J+F@<|np7BGYlKEMqwsr45*3&nSY9fp^@HI8T>7~{sA$Ao?75f__? zh+-QNh@z43m^n$*H&ktFNf`rU$^)ox9)L;a0oZ8>!~oC)W30`G1Rim*h4LthfH5wD zc#NER6jcj~!K;Cjy&K>}Yj}rjO~W>c0~SYSQ^!ED)UeqYRA7+CU>_0mlLUD2@VAA2 zVFJn3CgkGMV2pPn9uxBO2o;TZDhg9PQf+z5%H5>r&em_qDHs(b0Pr{MPJP?iLnZkapNd8G_ zWoZELoj(Ck5&;`1rH+96S^$0X8!3eKp$SKM(S)PS7ySfYtnu$*BZVAS#8=LeBz_qwBk?8u;&drbae8lbq`t-YQl3B#;xFkJr%QQ?(==~A!a^kVu_zhe3_y_mjCkM-r|zF-z#1j~Fw-tB`ib9SHd!sa!Oc+l72B?NvAlyH~A%eX9%F&&97%Tu;%nJ(olPM7qH(|e;|tXDvv+LtWnvLZNM1j}|T z@g-de%TVeg;ea3Y`{MLsIuc*%Q;aY5FHVqUd zZ*jV$FZ+k2Bg>~aUGgnX?~RV+Ta4eEoRWUAoa4xD$nur)NqJ?uY`+qg_C~_8JxN%~ zA>n{t(fm)QOTNYUz0r~S7vszFl62*BslS9}xfQ3&=TaWoo+Q4Mr#M~8Q=Bg4DNdJs zi_;}vDTfRtUkS@l(wDFdW&KFGWj#w+@|Wc#Ls^d!Pu9DH;~{ZC1X{aPIwTJ42_SLk zkc9fup+pjPnq+KX!Er@0ZOcs#7m`n$jEvy|n=i_F3jY>ulawSm5ekt8$t5{o56PUO z-NUgN5*}D^XaJ?9HUnP+-?ok`DVf?2QpLFuGC`bLQG_&D9>7tYUUa;Jq%lY=yo->z zjG(2cQ~sqL!4)zS!J_W?b96U|bv@=xk69XagJ>ilEv0BOj{s4L0MSxfcyot6t41F~ z4F96VgJujCV<0f&44l!eC2=M&XZW#1^LBxP{b)&H_|H;6QRRg|S zNXApzc&rb2iR)m$FkB+WeF(z2!R6^6oC-w+k?w;h%Sq6KGo@tQ=ckPY06$tz0)6QW zCkgj~59nccDC}Stt7~tHI08e&tqOF2xU=|4CHQz+{;|VPv=gaznq1j^3msQu8fvh9 zWkckxlP5|za410{9MJ;qHS%%+%$r-@r3MMq!UGQ%ByA=Mbwdou7ot+`LXHE9JYJmO zrmi?h#okt!HDD3=1OaWJ*a99L`-h$yfxtW9Ku~^BCR5F~^Yg0ZWf79DA<;Yd%bt>w ziWE!Xd4cs%y2dR2(*{3oMc^uRGeOj#mY;BnP#iNLBN$zfZwkh}^d&td6Q zk&HPE-Ks!*t!ghVx4_#gPuN?KkNbNUNaAykDpahH5zgS?XCcAi89eZ(6g=`93IH|* ziv!Myg90_C0Ar&8e%MsZ--!9A7G&|c>p9>v74tV@aW)nl4>gTchTY0S$4Z?|8n%;$vr)r#5ty@i-!R+x zZ)kR*exde4$eK-+-;SZTUtU^oX+eB$L40{W&JIAmSBStpAZR-7iXaN+Q#`dYSu7p2 zofvNtb`;~-;mMM=)r*M3dp?@n3Sr17E^>%?zlyv#^iUL#i1(?Gwo!m+Jw&u`-jE15 zl%!Lh1mvX)n4$P0yKk z`b_IiZ*~EV*#$Ia7t!c5yL-O5d0d~2Di#P@R1x>dp5lXKK1aU-!-|;PmV-qlOwo)n zj;0ln$gtuAw5lTBH*Zr35gREX=Bf27Fr@hK|Ksdk;G`(7J>Y)KbocbUXZ96BkRad+ zh|4-WL;{FRiv$soAVEZS7IArmAR-bjBBCNO1eFj(M6z-T5<&W32x=<9U5}LdazaTMro+uKSWF(6vr5~3sA^oex@=;yAP_~drMW5?S zMgN{+nW(N_xF@2dpMSv@eWX(t2|ltSw-i^ZFZ_5RSuFben0%4v)4sqXohTA_WJGVf zXe`}!A!o@!MIzLX$d_@Xxn%?S=U?!}oqs_IJ9XhgJGhi{WT!3?bLzIDvL%W{8?Ppc zWEvTeA&iJ@e7iT=i#9j%H&=zW~FGDDw6eWbn?W>Vy{)Kl7()PU00#dFr#HU z@=v}~6xgL^0{KaK7AfR}oFG#O^ujk`wn`x|6elK92uS3?Z;Rn4Sp`k-&wh2kZ-)~}VA1N+W{F2)v>K;Z@uye{2mj<;+GQy0nlMBTSD*fO{Wgi@=?1Lkf zeQ@+A*^HW8Gs+N5Mj3#~D8nxqW%woI>(*9fl);ybGVGF3Hi$FI2609iddVn5FB!E; z%lJx3RT*C?2}hMLUk9x!+5#nJZs&~DZqNbKip0+Z9Tw4==MU(g&rtS)_47YKkyIoK_!-O_rap& zc>HGvEj#0Bn61#EU~f~I1IYTN>7c$S=%BtR!BLgU*QiFTP_+uVD3hrfWimD6e=df= zRBy&nC8`=Pql~3yl(E!|GM1WA#!@rNc3egoN6jeXs2%-A?T}TrF0#tjMON9m$SPYG zS!L@Yt886lm92}cvUQPFwl1>D)~HRkkj&%GO0z z*}BLoTNhbn>msXcU1XK5i>$JBkyW-XvdY#)R@u79Dq9!XLg!Y72eQiWKvo$Z$ST7F zS!H-2t886lm92}cvUQPFwl1>D)~HRR#^R%Ai43 z88pZ$TNhbn>msXcU1XK5i>$JBkyW-XvdY#)R@u79Dq9y>W$PlVY+YoPt&6O(b&*wu z6SB&1LRJ}0$SPYGS!L@Yt886lm92}cvUQPFwl1>D)4LCOgVgV5 z5EhE>*MC(;*-p$TW5F4}A5>+OHRp`qU#c>`S~pAzP%jn2sc=+-+gG;+UyVb*{~{hG z^n5jDq~~Ans6L0Q>d=w;F!Jt8>CFh~>+|@OXwV2nl_G}o^{KHND0ahjC^~Wv#;rssKJO1H* z9{fo{2}>xwsB>SK!)T8>_k|@?U(~sJ{_TB3@b1Eg*HtiWqOSV2f;q)M&vtOGR}T(O zP2rWyjeaif@ekkj4`cm2sdcXXeSb};V)5I)gOZg{?)CXfCU)?y!GA&UU52lor>?3J zD2WKW9pqQ5>8{%UV5q8EEmB}MLS6S41=VV?0(~OI_v3@P0Cn!y2ow#~xt~jXTCdLi zI;mDx?5fpb2KtNsb+xFeR*M>ZwXXOoePzuKA3`g>icc+Ss(sDrs%l?z8b_5sUvnDg zDu2G_G|pB2e9dW`tN8p6daA0`BBFOHQIM*oCgB{gMu%mj-(owDKI{GU+^ebix zN5+ez5-0u&6X!~t_>G2hRtk=6G#pteII_`jWToK9M#GVnf};{&exr3zU%ld}()Sw; z=PG?=s!o0N3ceC&%2b{D>J@yIzF$6^tMm)yQ>NcnyUzw^?>h$0% zaq4#poU8QxPJnY&UcZsC24pA1QN^cB&#A9i!B_Dq({t)8R`6AQ`YU9dtMd3?Ti{%k zN12|>`YY!S>Z?^8ReH+QTvnNy%la#6TvzRC(>^N8H`zv;wtMd9^SKwTg*Z;Z#=c>H^8X4!R z|M*{5bjT`Gb6I6-F6&P_;9n!-0!Nj;GBuY~rslHB)Ld4Xn#(Fvb6I6-E~`wA0*i9hX(6s(!Yg^8Ovict zPstTenZWdvT=A4#@swQglw9$Y9PyMK@su3#lpOJt$xBbk5l_hxPstI_pIkb4%EXwb zw3!MrKjYO=Rd)B@RS_#lpOMu9P*SL@|4{1l-%)@-0_s$@s!;0 zl-%)@obi;L@synLl$`ODobi;L@synLl$`ODeDRcA@svFAlpOJt9PyMK@su3#lpOJt z9PyMK@su3#lpOJt9PyMK@su3#lpOJt9PyMK@su3#lpOJt9PyMK@su3#lpOJt9PyMK z@su3#lpOJt9PyMK@su3#lpOJt9PyMK@sx>5PnoFnl!;1DnW*%XiAqnIsPvSa@synL zl$`ODobi;L@synLl$`ODobi;L@synLl$`ODobi;L@synLl$`ODobi;L@synLl$`OD zobd{BMw#C8l<7TBnW*%XiAqn|-SgCAyQd!8J@we`smFFtJ+^!5vE5UT?VfsU_tay% zrykoq_1NyI$97LWwtMQa-BXY4o_cKe)MLA+9@{M`6?kKvwr{Pxu2x2GPzJ@xqQsmE_mJ$`%Y@!M06*`9jL z_S9pxryjFC^;qqx$7)YKR(tBP+Eb6!o_ehI)MK-!9-HyKA?IsPJvMvlvDs6P&7OK} z_S9puryiR<_1NsG$7WAGHhb!^*;A$;J!SgQQ;)}n6+`&_J89il} zQGNc1XBx^|Jj-zXha=~2968^?L>;eR-?@tyzwknwFUym~Z^g-p;>9&z>hdfmkD(V% zlf^H|{Rq^X>I-4w7hab$g#QknWJq3XtISHd)TnIl=h(|sjCZiBRBcpIB?$bq?DBf0=i$&Y&avCXY5&Jh@ zNUeB_*bi8gdf3;t;rD1^wZYB^)JCe%$%{5p{i}Rm2kX5x+DeOzODuoqMiWp2*x-=fFFMN5{0WXF55txRbt{Ix68RySyanPC;5hi|y z_JY=0>j9pp-36QqXKFLybnQ_%U3(l(*PejWwK>`|z~$PDz+b`X+8XUQz;)V7z}Mh( z?M-bP@E!c;ysrIGI{-Wer)wwSbou9ZApR>})A6eT&e7*+rv9YU!yOupK&wr zR^wLSZN_cD(Z*=t1Y;8L0pkJSzZw4qe9(9Z_^|Ob@F&JkfIl@*UgKHgS>O`mXTVj) z8sJ)EEpWZD0l3lFjQqS|Yz4j3co+D-@jh^m@jKvtA!Y2h!!feGttz^+zT;7$wWwf=7XUDNGK8?o6} z+K9ofvO9t9ZTAK}&>n@WW9%`&`|SIG_uKaar`gkiGwg?fkJyg@AGgud>>t`c0zPe{ z73>A}Lf|5M5%3rGFSLNY(q0MrS2p^d{cHQzz_m8|p1t1QhO67{9l)LTd%#2XA>hY0 zYTN$I323@wIhJNPj`K8dfwKUdXC1Vxv&i|mW;)BA7r|fQtN^{z`89BxvkjcLoE-@B zj`I%acb(m!KMwWN^w2G#)4($>#*>?LzX80=tpv7lv%s6&n}FYT2LcDVgMr_5?*!iE zB0u=g_P+s-yXXV*pY6a??&q40e`ZI!;~&`(5B^hq1h6)8i>Bj$uzw6(5JB7F-=Wc4 zB40+)H}DV6=o|PCXN&?(#<-@r+Q){W1$uk^25C*8A~{ZL2P3ItweI)_(DB*;Er2i@ zq<|bgQIQa~smq+BdW-w948E_f61F{@-xy-4n-aXZ~*}=3uq_{}IQ2Z>(PSz5f%U zoupj_qd*r&0}?u*1vN{vA=5+Pi5w53tzud;eN)qORimyue&eb>v+i11*P?EG-L}!z zge8+^F&-Uv=(r<@TJHmD8}4r{^{QQ|wb9yY?X;`3_S)52mDWM4)~?YqT1Tyu)>-SK zU8{A~uG6|{-_p8cz8Iy|Y4eRb$kH*!SYw>=J!8D_eaO^%jfuv6#w6o@V=`px6ypcR zR7lrpkgd~=hm7rzknb70jNOoq`;8Bc1I8gp#gB|njT3<#fe!-z8Te!1P~f=pvGd=~ zU!1=>pE!SWjyZpKjys<^C!BvcC!K#fr<~86)6VD48RrYHFR6( zJE7Y{-woXnx-)cFC>I(T8WpMwjR}nljSo!-O$<#AO$kj6O$$vA%?Qm5JsO%7njLx~ zG$%AS^knGi&`;b7_iJtw_v>y`_Zx0ANP*_=H{BNQ)IH3wCaPLoi)zRO*k)SI4^hP zyxfWNa%axVT{tiI=Dh4-US6Z!$a%RB=jFbfm%q(<`DV_`w{Tv*mGkmI&daxPUj7c} z<=Z*KZZNuHhJA%I>_*P8uVRMnrETI2`xXV|TrVc$e=n5b<-Uznu5 z&3SeQ=h=5S&+gv z*LS>`GI0hy=tDKa1F(_}uW8>eLs zGW*LMWZr@~Xs9_pd}sJh^ZD@T@ITEZn1h}PTp4*jvLw(pvMTbcz*Uj8k>3Wcj;xQo z9Ow|)9CbHhNXy zvFO#&&Vkv{YolEQPX~Td+cKA_*;{j{wyL(O=44I2W_?ZHTp*XoHO*DlcFSeLgL6G| z{c=Nc!*gSDlXKH@vvYH6+tlo;`LO0lZKs-JHD_{*YG#Bgb8fCI*CN-xwpXresC~}M z4b0W&M&>5umgi>XR^{f`tgIPOv!-Tn&5=;1c4%Zdq(jeKWv)FWL%-0#(2&q@$bm7T z2{p57=7c8K%&S?Ln;V)|b22ouW)jB#+|c~cqELOVXJ~n7Rc@8LIC3kf{~XX3>_>Hqol+;%KL6 zw`i|upJ@N+py<%(h)#>6qoU)Y6Qfh2({t^kkLJAS6Va!l3!;m2^P@|11EVXVtE20p z8>3s|%X6!u+oSK*_KNO_?vEafeq7tEwps3QZAJ8x+`im_+EmT>=!x9E+CjOsx!pB& zxy`j!ZbR;PO)h#mw=;S!X5^0M&em$R&0H<9uptMr>AXFZA~#xskCsv3WIh zwUO9Dq+_Y*TD& zY-@T?Y)86TY!}?#*oUc^u|u&#=_g`GQU_wkkiv(lRjF0!b*X-_lW=EJLt^=OARb8X zkGtuX@dR91ylHB7x_^1|c#HBT>8EO2*0w3#P`Wu@8E+rY#Jk3O#=UsInj`Un@gb%A zN)MDCjt|GE?i3#x9}}MtpB$eSpBbMWpNmm`CO$vDC|)05Ufw0Xs=PYB7NdSfd_#P5 zd>h8T8{Zk<4GB_ITNF^!~ z%@QpkEe48Dv`JJ+?TAlwg722-mFSb`pBR)Fni!E7l^Bp#A<#kD; zd|vsyq?3##OOj3CnkQSAFH5#dKT+E!S)J^X?4InM?3)~r9Go0h+dr90)+NU$C)MPV zQ z1b-xXEWJN@GI=JqFPTpT$_JM(OS!2;Dw8TpHBGfhRi@gfGG(b$*Hq8iHnp8nUao7+ zhq;-leyM@Efi*KyLsG+2BU58i6X++Wrln@4W~b(+=BE}(%c2i0M~_&W+K}3u+Lqdx z+AVFK+Lt;YZJRorI+{A3I+Z$`*3wowl1`;7(#_H>Yo?~#q^r`M(%sU%N|&emr2D4_ zrH7_Rq(`O4r6)?f=_%>yaxC`gM^R6g@Yuu9d(sQii_=TfE2JkC`}FGcy7b2M7U?;) z-D-QKx2NBeW3f-~N$*b|On*%ON%}xJv;U!byXTi^qW6iSKmL+rG=anohSyHmB zWM#>klJ$}Y>7^w*N_I)#Ejd(jq~uu1$&xcA`O-kCTbd{>D{WfZqO`KKeQBn&YiZ9? zue4w3z|tY5!%Ih&jwzi`I=OUO=}eZlTj}iDZl!Ze=a(+RvA%pj>GGOQHCt=qT z%gf#?+f%l`>|oi)WuKItC_9~-o10I+sO((1QSQ{XDUX$xls74FUf#OAU3qnRm-6oA zz03QS52)=^+aGdsL+v0)^gfWm{UO(Jp4*(;om&f;D$*U2zPt`&X;^u#HkI2@URP_C zk1wAj;d47{`$}RKB%*NBOSuy>K6vA1Xgmeysdt`I+*3MWDj1fEcSNt7ux$qN1{* zeFau4wRo$e{g>4f?~A?`#1o#@M>>u0B>ba2^o8`87$)9A)AClAaS zLE}co@He6L-w_9q?-F|wFC$(rDQbTr{VD0c$-Vm3#N*^Q25aoU{(eyTn_A8nBtr<}CrdSu&b3v5hw+8WM!hG0j0t^U8{}0|f&H~@C1BZY&1qa}2h&U-Is?cGhzFNI@%mzgm1hp z`8P9^oKdD#Z~qMZdV31-Ai|f~za(N$#MnpvR{I6eTkWa9=NSGu`#I9Xfmv%bQRHye zT7f*Av+f~EAMQr^+=F3yknT(xmJx8Zvn4f_wPf$(MSBylg#6`9D{Wm){%^@21{`58 z0gkm%E~6vw$}r4W@;h1*=4Xtj9pm{K`R%M5!C%5S+cNwP8>Q1v+1N)wc^_qN(Z`T; z>>2>^##=A7kwfz~c~`H8WbV*yp|vmU^N4MqJrCH8DQ>kN0^NhDY_%UFeKW8|>Pmlx z<*kwMpe2XnS)bM9kGG!(r<%DPZ_g(!VQLxwbFBZ)theW6-vKoyb@iP6AZaPrb2eHH zeAGPXM@S=eb1~cKOU67}XvlV{^E$Sm!PM`uzYC7eI;mrdhUCy(DSg1aL;97m)BY>4 z!m0yit;dOyO4b@d^oZMm6}(zu;i`5@XrP3gz>vd94_CC@)q~-Ckl(}dfZmPJoMi_2L4 zQd$7}T@0=2k}o_F(v}9J4(b+h&mohR1r_Y-1-2 zw4}IMb_PL9oX;^QoymWR_1v2^{1Wrf+xjiSyu>{8wypv_TEe5G63=Ln0H7tFdXCVn zj8I6eCCuAU@V8QK^q_2cj$t~J?qXxF&FD3Y?*RVKY{^4A z-Zjjg1G*K+CL>N@zhBCket%TY9xNp!heqGb|(KhrrVkP z9a1xp?aw33&nVw|Q@-_N*?ZWM|2J$&u{-%aq&85)B3GNTFHNyWf_{s|<=NEHI4Qk*E z8-2|Df#lZwF?02M=KMO9Lz#bj_#=g>-^pGyiED#l%-em~#g_RIG3X0;SB=aUs4K}y z2hIx}?8%_N$Fg@|m>k1*vJQjanX^)7*3y&G8}&0*7v$$T=Hx8H&tZwqGOZu8L|<}6 zTE}?4WSBaRHA6;+-r4#KV*XO*Ce$#{xR*6{FLPCIKL}1e^VwRY3gjW=19}DI3=r)O z*$6&RXiB`s6koF&NIyng3_NJRLwt)UBkLgB<)Hl{=s&Tx4{{x{&z5-hG3I^t zZ$R%My@xUH;nh7{GwiV?<~>YnkG=H5{@HD-W&do@WbA_*+HDu@pNalThWlhaxlh)M z`(!t8pR70c$vp0p-N=2io48N*ZA~;+ZsrbIf9{YCz#_3m8%XVw-*Q*%CF5pjnXJd| z=e^p?*#Epw+h9CqJcgaK*~V;bqwz8{MPB9J)oa-8{8)RPdslC8?`kXeuHNLX)i&;0 zz0F;#9n|Fb12sARNKKAI)Z{ouO^z?9$?+w3t(u4ihTe>OSC@&7g`Tk=v>((vLc3yy z-idouo$Y6^JJp5zQeC+(bshJmx^Z9XTilnr-u}!!t9$l2`<&j-&fDkpn;qTJ^;@`8 zHGn%+w{oXyAa|+;bEj$;cB*dC-@-oXe0_W9S?pT5*pa&0$Z6Hr+3~O~(7!ZE9xh3V$tmf_6Jv{G zD`Hz>R%AzPe{5pBdAv)!Eyh`GCc z)IM&XidEQW9Szzhk?4l#W+xRrfO`fy6;3m!rPIc#aymtJINjnioL)|!#B8U(d)67` z40Sg+Bb-sOZqB$Ew(*^biRFpq&J<_5^C;XC&Qs2U@P22pvlJRCtDSYu#?WAAi?iK% z&)MVbcMduquzv2yW8BIVa?s`?sE^|%3=2?cE`^`FXn7mgW{1D zt_nxOsc=QOS-54m4bD1+yM=p&`-J<42Ze`*M}$X($Au?`r-Y}69}Pbdek!&)ydb;=21kZPa*?{o_{gNl)X1F3jL4D5EciK*d69*&ZjmLCWs#N9 z;YlsB2C*Ma)T76Cifu=~oEA%6ly@&0H(9M47vMu$g-zzE{ItZr=*&VYa!jU@ z6-aFn-Q3ue_|)j!gcn~Ki$F@vZ1gA_Bw1@|vFQBhqC(3RAHRxE-nnS~zqDkbb>$jb zFSs4_8M12Mjb z$C@R^#9GGMVAM#D?-lD4>mM5w8yXuC8xL=9Z-#0n+o=#WpgX z_hNe_&#{kVpTthYPRGv04e@a&9*dX6n@GFG+r_Kd^Lxkp#s|a)$A`spm*y)zDL$26 z$}%TDkA7i%3CF~m`1-=whzpIe5Z@Z#5#JTx8~-qVD1Ibau4$r0 zqLO_mljw>$&!d-dBBNw@Vq{`WVnSjv#??&l=AzGUlU|L{Pyso&0a9%n`la;qwTYe5 zrxUvq`$SSAOa-nj=Lk59tA5y>iK7g29HZ_);xO(!j&P?EXOkM@jG(WJt4KCWwoJB3 zRg9swE20-?{)XCdpY!Slp^aKW zeIRt(1m)f=5fcpPrRSk*X>`he12~SH2L#Qp@viIh9dXz7(5Hi*FJrrkIDj~qcq6ec zk9RHl3DQ>(TM6nv5WdllIFbA+(my8MiP(|%4iZ{BBB+0dIGUJY_z8mMoL};4A3^PJJ_=v|okBbZL(fa%AH=&vS5P;45-$^uwx9S1Uj441-iJ6z!st(u ze=oydPR?=C4W$1_x{UN^yz3)|zl!u<$Unz0-N}E0^qs^O40D3~ENSKw`esNs_yeLV z{9sPdd|EjA?}ZM0OX$E%@;eJ{e8H>l5&uliLb+-zXZyBfU+ODp3@08WKggcYj`U*k zyAyi}nmf*)Mff+*=YjvuIEPC}J!C#Vy(Ka<~y*rVZ3;NMS~FoEqj zT4-p|qc)5VK-^1mR*6*T%l z=UUfKGlr`e=B<2Fq@Ive&?^v|@gh_FL&Im_l;wwjqmlEI{OzC<`QE^>c?T(e;Un+* zKxm_ym?Qr)(v$Kxk>3+IsX_X1wZv@PEwsLkoVz8AzKtPo%4b3UlX3PxFHi9O8@@zZ zr}BIskPm?7zKgfR@*q!AqVJUu<$Ga>2GeM&>>9YI^ z&~K8{hq#X6J%(RLj+g%_!aU3H=ln1nv+ME;z^^7}6!Go6j@%w+{wW{yGHG8tw@cmr ziZKsn%!3){;QW5@U(ELamJmlWWF13}%>M%XI>tOQe~h%e^%=>!tz+$t%ugnN9B?e} z^<`1#JURIh;CIdsmaF;ONK1=Qdgy%_{tqlm8}`qgOtBwvC$IJ+?qnYN5r3lLeU*`b zHZZ^_XN)Ju{CviEIKLRxtbs`^ zT{YYEZpL}HaL_ZQx7?ja&4ZTuzgy}SwAApk`FdbA@mbb$HSt-7Ip>FAY}LdkB|o~~ zAD)!_=w}4YMZz~1F^75LV+?to`5)!yhjf&9H^W4Uce5-}Vn0EB8^h-r^6m3Ea6Thv z8aXAzv3Ogh8PC=VrdWDRvTNsjS8>50_szJi*r+8Ny@3Zh8RQURr?8*0#f7AJM z$p3WV=sN^W%64NiW2=u&tr_`BAFUr6;d%klM7FTCrzO1ptyBzcy6dDMsB7WL&fL3i;x zpb`AuXC%Mw8O^VI#^8O`TiRH>uXacfNIlhS|WLRm}yF?4n-^tsUa|Azh~G_fz! ze)h+~74Em(to{qU1G`0E>)z_# zs=q8>1n3*^Wzb#vt8UI6qi@2yk_Ys++!^i+{cXH6nW^u9rthQrJMItNwfauz_r9s0 za(B7^VFcU*?nj0r-xC=L_oRE$NaBsjXGY5X-2L1r!F#ocQ3_q(uNf8LujB1nOT1m{ zVpNB_hPxU)@c!g>qi6Vz@Eyjm@bK_m#+~8X@IQ^a@J_@uPDI>D+&B}dh+Jmmp}TvX zX+?TRZZw-jMnr1NW_YJ@zu7!8J@S}Y32pEf%&N$Xkzbk}p{4th*%dmxTg~ox_J7B` zLA1fmES~;9F~7|>dBgZ7uZC~(YWXH_WaP_8-n<8zyM{TI@AJmviNA~aefG2zdRqm& ztXf*4pJ{;~68}j2g7^z_NxPiA!`@|o=cpoUTs$R*RkX_NF%V_EvjgQ~a(UJ@8 zC8+uN&c2J>a_4|@)6hcV>d^)Tw;U}cZnL^aog<#YT?f#nbhwZAx0#RQDrb?%Dsj@D z7vi+nh(qaoHy`<#uhQ`2-hi|=qc_2!MRuaDJZIpQE2YolY!uHc3q2iue2Y_wUa$o{ zs0Dg$FSJsEt$-1s?op${-qp4f!ftbROMkcbN)Jb?u9cC18k3qUc&S0n&P$JwR+rv@ zbkN&`BVKA$u1MQS|7o?ja#Z2k|MqxkfBKhmq=&o}^^i;H#vZ^HmsW4J*j|rPW>_oj z?L(3m#DX{)!xr_NLQbVL(vvZ+PZrXUbLpprFpasBT&X-2dTV3c7y(RC)l4BA(?{N% zfV~$|XIEPdQfjBvHrmu}^fK-~gcP{MH>O-P1|j#3s2paH%SJCHDEJE_&mV!(qVz>0 zP)b;wOQD^9s0sMS(y4KI>3Ed$3u97^&7#^Z)QZy-JrlmryU+*Qi|061BUDDJlY#W@ zidsSsyV#)y8lA|@V#mIYHYvD*bd#eq5U!{1AQ9+elpec`&K>t8LzUx)`a zB(d*vhB(8Wk2QY3$4lHtJ<0v!0e9jBgfCn(Iv*EX#9xykYMm7!N6ES^Ibt_ zHR&^g!8-*5vq)1%*mzuM<9*^=ygHjWgSbl2Tt~Vu(NA}((AFE|d?^_0NVzo2cB9z%SP_&Sjq#KCf*%|n900O`+pwNB9Dy}|858&?SiseKuw4s3v$ zyur(OwWVOdFV{V!FBjC0k{%#v8Kj31TN4KnKO#;OG(RBST+k{d-Ih3%IEVNx;$wp5 z5z?;;2B}9Gd|Vl`wMQ6qup2}2uk(JFk7HV3P4S#5XZYP&O6+g-TY z?#k8nbzE(C<7&G*SKB?f+U~{Gc5kk>J(<0)HI&}qGdYwn3$bI;_O`w_0WALE*P7T4U5 zbItuDuDR!M%{`ZE?kBnCp2s!!Law=&aLrxMHTMf#bFbu@``27^ui={ew^(zJ(%0d8 z$T9jWTzS9BmG^60d2i;*`*p6ow{YeCiLAW!v$FEm^IUmbTzT7Ec{^NryIgsPx$=%- z<=xC^!rkp=Tz|Lb`uiHLzcXBa58(RyR<6HqeQ*Wb5u{XK;1?>o5uzKiSc z9M|8qcrw+n8Lh!ifQIk%vGzWXcP7AH#Ifc)=M+{h>+P)}P1fF_PR<0ZoqJ*)I*Bi? zBhEB<7b{AU_U*BP?g>fg;j8TcX4f4d3v1ATGtfSUK7}oU06Dtbo?{OPHM3_S%-T=|a@h&riC5W!k@8ujG7s^~9lPxuTpe;g1YevN zIeR_6G^c}fr<0Nv9Lnjt$tY!!vp0n--$@w@4kh&6NR;su!Yhv&-E@)PYL7>n)}7e+%UOt9)0q z*<_SQTDo7sW1TQ!jvooGcmX-B6qPWe9DUWiip&VLpvq#{Q=b;0>7?o+61^k!!!R`uxP=#w7Ldlq^& z>R~ea^8{xydQQgahg#H7U+57s0{nXlVeRz@-P1XX7DWHHH%X5US?HnY`Jko73LZ6Q zg0!~u0;D7TO!W=+0kjwHgTp$psNei==u7+1rROZcu|kjds$*lT`>iZ3JGoD&&4=CR7gY4rC%0GQOKF(s1T;HcdGc%CIcmWp;ijv zn6~6AkCC(zwRjR7O==m>S%F3`<7_KN{UyGTa`BN*zs#XCP*N>;DM7)jQ6?kJ_h?CJ z&BADFJgV_l$Su}?0s7+cC-kM`PtF_1pc;$CHCv#uZkDxkW3TFoktkkTtY}oC?);Ui zNS=!vN`AqKj4X2Ojg)*oRgh+K#AiU6G4jQ4L*OI$2%ITXDNL-;Mu>IGXs7VDKfuz*s@^ zT|s9x=`(`CI|T!?NPkVxc%1k?@h!o?4B{$5a~XvTnAw8C8+6-+Lax~tU3w)Azn7B|77FB?H2|4qz zbD*0Gu;;Ct`^gzXyodMz@fkCYyUrR9A{U<-GjQ)5;p-CSl9CyDQ+9@QeLJ2>rd{+* z@;~nkJcDwGow*D4Y<{Ep_H1sVp6<7)r`wNuy8WrAJAn3V22xx1Hfrk*qPFf}YU>W6 zJ)2i(&t@a-*}O{4-A%L@_ZsclyiR*ITWHVb4cfDLllE-3(Vop)v}d!O_H5oU#BSV9 z+Ov6=_H5pxJ(~|`&*l*A+5DOIY(AzvoByUgo4?SW%`w`u`8(~|oS?~SC%M+R&5WQh^VOWgQZLxWyJEA?TheUfAc5YxXPLGPk zI9R)Z#W+1F`owyQR&XvCePX>8t>Cnw6`W4Af^!3{;AE*!JW`(e^g4O!)5lP&cpNR_ zJWPu?Gied$5n9BVMTVuQ_WA~x8}%jC(;{Ds(HGgpcYHglEO zU^7?K{>@t2zxgem)<&3n#0Fcy78`7VQ(}WH*hZeIg2TnaO%Qt*Q6u=MShxw!k!Ppi zT(QR%lu_)D&J}?+!8g*|pwQVQ)jGX9Rh{ad4x|&wk5he9gHyTG`1FBvWy(m+NXvwzi$P4A@QDDpx5*yibd$t}>5eJaup+OaJnT}1me(zj99722*%2Pu8m z8If7YDV*rLrg}#fioR=VR%B&l4e~t;7K7GDHbpk2=0&#pTB4#M3O!Nig~mP>9Z+a` zD$UQRr<~IMjAo$C*%Q2e(6kgi%aNj28J!%RMt#b;qUo4iojQ{`naZc#XnndY-89)U z-9C9Dx;(lH+Jt@5f#j&<(&RX3Izpc@xj(rsx;fd4x@Q&8@avU42rab5QD`?4?BU)?FRut{SRICEwyTzKtS|a9yqD7e66zh{*3SGh}vFYSN zr!d(`xR>Y^PEQVsO-b#IO;0pUulg6=LZw#-4MHQ;JO=H;U^)Y!R1 z)BkrZ!_=-+??z2S(Jm}#7Ambm^f2@$(I-U6UGYu-lSbj!*NRSII*{5cI)&*W>CNex z=?&?D>G|nt>9t8KeJVW}er|d~Qp1(y>EY?Q(E4kU9+RG*s7%jJFG|-#>#}{KpoIu6 zU4%bQ9mL~^Wn}tjiG@$n&X#CJI*D_sl?YvBXeQ2w9%4Q94c8)#Rf!EuZ!R6y?Kr2}7_iqY55POnvN8uBwu zrQyfD0cmYU-xRkEbq{%)&LksgSL85c@DAaiW((4HAry41Yn6v*S# z$qH~#zXdP#j5f}t>ez!Mr?^*X#6lNV`Vx9(0y3)?QdT`p+Cq91bY{1};hN~pLdp!H zM}MGvI!7Fu9FZJ_R0@7+as}kxM9eEi{q}!Fe?>pOs9&@HqTd$v$4hCMbuRUVJ}7h1 zrC#c%;FAkd3zLf>wO1rpqek}%CO6{hCd_>&F|X!X3dsd>P>8konA}n*r_>r}f-ePd3FS^(wkA~|O2B0ZF+ybzgjk=#)71N}_;TBGbxGNduLh1^%B zrlL1bZ7gX)s$42flLxfOEjdFz zn2y9JcOu-yB}$#SxI~a9|6h6ks#9`I&KRC?Zcy3=LwnqZD zh?OMcN~<0m?sQ`BvzNS`5nCVl>h=!^^B-dIBlx)R13URGxo9g>vJ!a6T88k~k@E_}KS`SQl!9x7 zHWrcd8j<#70=r1xAs9@PuJci1Gge4E#;=8A{E4YQ!2CSR)IVg%Ul3OcS}zlSN{;L) z;tdY*lyHo$)zWE|ke4gQdE*Ok6{2?O41Znrid`GSZ?jrvk;v>XS zg25SLdneG2oX-VK?&$_-Z^wL;eAZ{+YKE*5w4anX?OH)+1XF*GsoWsBvf2x6{aol^ zFY-SVG#SR+A#^an)JvG+`_kf8AF<&O94S}rD;TDNZM|1)90lJOdqjcPB#bqMxP6&Naf z>zvrZ2-3Eac?Qb}vZclx;4j{Pa*cOj=MQ~VY=KNmE+2*>n$GDl+2<<&rZXt&jpMqBk4QmXN z3P+9ENn!}>WtbJb`Xgxz;~^sZ30fa58yG8B&3DDVkQEjT{zEYEZ{$oS{W$qsNsl4! zB&Q$SH!Zyvni+Q?ev7t@^0arfU1(#Y&}M%@{Y&x#!nfK=xFSuRAvb$_%%W3dDH{nC%zSY2mBe<6Nq7UFa(^z9Ig`7|0-y%Bd4!m(3g|DVF7*jO_)MaX-_g^|Qct$-hs+n0JuALc#<(2gAq-!&q+!9r%%q z-@rVEd7d~%ByC_hamka7g zNe>V-e=TSkYL-Kxm6L)&k8*m-1>`!60QtU?@v8hxw<( zK+8&bpCbP;32D+^S%8+xps#~b34NV0z;#0FXE6o?csodI@6e$y0rdnlb70w@|L0+7 zY5WJuTbd?sIki%}YLY0Q;3mBv$IHNkjVtR@&Q;N83yoh9D*_h6r3 z|KOnDP`n2o6&x3w7@QKE9(*+TMDVHLg5cud(%_2V>fpNI#^4rfm^I8Uv-a9e?fPP8 z4X{`F4yz*HEweWiob{pa?0~(z*ja<|PS|(*U{wLmT4`@CIO~w_frX^32TNm%bH`&yT~uIR$2wW5+&JO@YW`4 zYop&`?XoWQs2!;x`VUctukt7D7O0I%c-Kx8yw%!j7kn?cC%8Yj-yULbyU0zz|D5

    lJsD%9vgYl*a1@N}Wo{@2H^XvyN1^;<7^uFx7^b%aiy^Dn;8 z3Nbu1T;f$rNNZGDO(dt1ip2btZP&a|7HKc`F!Y~7K6s9PQwUSYd!g4ghAZ@m;`+rZIE#i4syC3@WZuHwB;0#BIMY!gnPs<2E-^bq8HoGS{JMGB`JBDE=AoOs1 zB+e!v{A7fgY0t)$x%PazMHjV`q`g~Ym!>yN34Tx0^F0L{UJBj^JSP9A)UY9V5AbE{ zVZk8Q```=(=7M(szX-xAQp0b9u*QeG#t~uL2_Z4xHmoB)6U6>9I4=M<1g`}CcL3jc zHEbYm5;Sk)liyBRml+i@8|(j+*~#c5sBz9T9HI3P*NMBt)|5v5TVpeyl0BKz@I;6? z+33xAyB9g_Wd&re6ux%!dEm zP|NUq{xdrBt|uieoljKeVqX0*IbSj!Y7QIJ5H<`6ub*OmI?H;;_>vqx_ZSmcpT50P zi#41ea_wL=kmkC>NRe(u$D#qZt*tnnkH1VLINxQIyzc%sjE#X!FStrX-a%FZ_^wdHe>3U%tF-?_uNnh1 zplc98o|@)&qb|Oa$A4^HvW95}{bGshm(FS&O%2wz^ybB}HN4B%9H5D>7-O>XF~JaD7ikOlziH3#f74#z|D!F12GbJl7sy%Y|8LHC=M~y&O*1yaZ-L(q59|HrMzB9#$&MLpX<>0Rvy58?ee?q8?30d1(2X(DF5#z&D3)4c`tP zU(T9c;PEB5*&DtuJS<_DgW+K*z|6td!H7ro|G_!;n6Ne1TQI0wg>ie8BGd7x+G z2s&^az8>@vb$uD=mGEoe*TZju-|B}C>;t_6v>bQA?^Pc53#44;5%^>3`bp4dl+T0b z!3W^crvnN2v+#3K7Z$t*x(vQ4d<*zW`1bG_c%&8Rdf^!Cr0AaNDDh5)@2Adp;wXNg zI!;6Ua?cR>;qW8j$G}S%)D7d8G{g&U0=&eB`^b}eLtQZr>}jwJO{Ob(FLuT)}S!y$v3G#1@Y?*uLlj_dU@_|cqyv4%r_T) zzB@6&Fb7FrMf%?b(IOJY#5j=LN_iI$-!gZCewFkl;%nx+;Or*-3*rvq7sL&W;W#Mg*_7R)ac)a%GOBWMK4XBp4^oc#5~qpH72i|1I|8-eYhaW?}0 zZZ}Sws7=wPYmef|{3&gLwpd%Lt%u--bL@O_lE1M56}lAl~1kFz;}u3#MWlRU##xH*TqI}!^`&Nz?Wn{ z0V_kyWeqv<*IFAu|JHgQINllv+$i>65oS2>CBZyxv4iu0@Pm2s@jjv91xxVvg831$ zg35>NCg5zaUIo5vqlatF#nR{bX9XMH7MsKkKesrV7~3nPH(KMsf6B(&i-zsiL%>5~ zi8h~>cXh}E-cdEo!siJX3)cI4*xDggfAxDrN?!e3r4MT0Ok;Ckl)Zf-2 zdH8>?0sh}>2>1Xt_+GTnk{{Y*<2pG2368``j)+)_b_!rbR=0D7T zXzk4}%rCU7&AfSDtHQsUw$`cxm5}7uI6uQbduI5bp4TCzxHc6|<3G;oOx0kj7E=9B zEv$d2f2_s-H)%sluASCK-a}~Zn8sE3Uqt+gC(4I;N5?#(_XLf3NS|Efab1Uu*T=xi zb<9l?UXBb`h9hLY4*AdHrL>%j$6UsvgcmPqNZfKQ0bhuxDd-mPUEwjW>6q7a%xgTC zu*ids`9=5OF^9oYmZlGZ9}bVXit$QbCC`$E_(I-hqxB83-O=y@N&tC6#Owt6723A?ADgDl)MjgQ@osIARQl6RSXCa@j>3A( zDebI22+K@MkLW4ALT{$G)Z6G)dMCY`-b?SJ_s7+th-Z{Oj(6(X?`cD)gV@MucqKm# zxRm%la*nYfm^bp5k><>i9|Zd4{58N=@~;5b^G)JL;46&(<$O8l_ryj|!-fWYyVdYAQ^_}c1p563c}D%S zVBTqvF}$`xTJ$Y^5)4a8d5m^_0Vw4}ix?Iyn>uY1DVyE;C`gZ+2{5R+vV=4X#^t9Q((%+3QJxZKTu!%a}nc>WW{Zsj}>v*Usbn!Ex?(sSJif~|P1pb|K z8vc{>DOe~yf&bDgSR$Q(@AziB^I%`}I4p(^3@?kP!n-0ZBcmdZLJM&P{y*+e0}(M`s`u8!lst=>9jLddb;>e=Ti}e<}6c8{NM6 zH`0gQN8G=-$5Ev>K||}@pm{K{PoV|qJeXj|G6_xG-8kB z2F`>27tSO8XU;6q(3$P8cINnVob~>zPLY4TQ|v$Nl;T$Af9D+ZE1aMFiOw(n>bN-n z+PI)U+r7bG;oj(f=icOh>E7%gb_e>a+#&u^_fF!ECxtsm-ELC$h4XL0k(T~WT?8+!iRBQnloQJ#IFtOF#QE&IWPJ4U zx8Z9aKK6;5;U47g#n)~(%g^J@ISI*`m@Z}dT35e}9G?`+ahWrn`&s0E4pJ%NSuwN} z`jcNu4ogUD8LKVZ`^AL6Q{3S1A&wn{zlZQQk*ZIS=V!<<)0ygji`;Su=?&*qKjK9E z4bDf%cfG%WP(MR%WzNt3TIW}PEizjZ7w;c*2m8B_(=Oz+)6GCaMsphx+cMFyvYb*~ zjohxMRPQ6*4Tv?LSRW?VH<3j*Wbp#B7=XkJk$7i9J4|R5gm#$Fj&K%tBa?e%QdI(b z#ihultG`inBmIy2+nx6Q5$9t6kaM}e*BQtEm!Xa(SZ{0NM1T~bs@H{&|dx=Qt>in z@**jDnR3q(1O3@Z={2O3jg(f2A%2D!=C2TAE3?I;?8x=F-%mX0KPR5{U!&YUaJu@B zIX(OlPEY?yQhq1p_=?lVALaD*$2eL);)p}^L{|z!LKxU>c?P8b7 zu=9%uA*TW>r@_QrRhMikkjEj;%GTu6NFu>sM_yh>qZW{|(wIgyppGpdl?~CV2H1x% zn$?iJvgXHMOv;yGhxVdjo5;a3^1KNRTZv`eOKvukoBe3m9yF}PNKuUSKS0aAM9UVS zWuKsBdF1wMr!%#ytH09eMvA+WvmWH4r@tGG%SGe%Ab~YVpb!bH3Xb-_p~MeT;zua) zBgl3QxmrWXtULfA(L z<k(vq7+pMqF783LX17t#qo0k)6 zoRL0~Ue15@$#D{O>Qd@gS9JAqw8GTKFNu@B8XVX;a#?QjPcHV8^4|Vdw98o10yOG7 zVkkyacA_Qw(2_mQ7093`x_%X{Q6K*+v?L!b*!T3 zK4|A4@-vt^)16Wym2m8bBasvolD^)Ajz7{j+viOaq(6&*)`xmO0ErJqc1C{4 z&b>i|-J7@uQulA6m8jCNax`o|8g>93T}BGaZD^a2$`I~{*?cb~)?#9%mBOV$7>)Tp z_TLdSq$9HHjE%Y!S7eB6*2A$Aj)U}1Oxu)C3co-%cAy)j=*9|K?P6N%e4``IFa9pt zt_|qQ7p5=ae~j;g`1%50+woP7uYLI1jwRU>)2brMr6X~4Mq-zermj%5o%nX+b0ZF0vzvH!5l6Y{!4SvSw!Jnj)NVp3B7`D*Z^w$Ah!v|JOK}l>j7C_q`dG3eo7!S% z8L2)<8h(QR7k?WeZZ}qpR5t-TQf8fzL6sa1(FPqQEr;Q*fV%?j3ULy(KOvU#o!G8= z#Jw}to2y6MJ8bG7^}i?nhQyyhPIi%o-Q;E$>DYlqH96Wzh&y9B+C`~%tUOBGpA+{X zLVO=td|~5%5MPh@pAyQtm^^kN#qET!o)GpE!UANnp1NuKT38|5*YZp7y@R^87mhN< z9Xe7kE~PCUhXlq$r*qB11~@!nuR&^qq}&e@?;&exW}~;Sl8Y)^aF93-(tE@vk&^Fu za+K5<>tK4C#=e+3Je@Sp@;6bJ4+xk1p9pU`c`tY7lAiU@VoEWHUb?mx(LTYu&mGK# zJwlWBpvn8#>#i44=#4e2vLOYw9`y3xb*{v=_whFp(?VkU5Z@)vN7$90sUyD%{eP?@ zi-fePGumNn@EopWLRk+jwmNnQY4kSsN_3(O8^cu!*MHzBfuoc>lu}m*V>{??^E8*I zn~};o)?7BO+SP^uYkKR^|7kX8R;G*?MB{7{w#9(6*>J(TrU!lzmelqTD#}5 zs9HG3@*{%uJP*)19i``K`Xm+BWryvd+=S!?VsQ-5URtsJF&@eQmrQsz;VEi^ zAnij)`yipm2V{=eL2LUlDrnmGc>INvkNIb^QM=S1~+gwC_LoQF?dV z>D@IDmlJbWVvf=iSx@XPy%3jPNIa=9t=qYz;tEo6A*r~6p2rwc(h^(Vhtj*B_I)K>G+>O0&=JcxEfz9vb^r)@~!M!7Y^1 z99ryK(Mus(i%2lSD8&zq_*~3L?WjNsV-(*r%GklVJ-(2)>4>v6&WURkH!AME_;ztC zF1BnUFs#csti{t~J-v`;qrmLatmS zLY0exw{s2Wx|8cJt`U`syk1;aa$OCN2|10B(+D|@kkbe`jgZsK6SSdG<$G`Zz5BrVrmqgr+iMuiWlklH}|0Mh;k(S1!rLpC12KN@Yo58&W?k#XPgS#2r z&EReZ_ZGOfSnio{uYh|d+$-Q-0ryO}XTm)b?wN3}fP00_{RX%;z_kI_4Y+QwVYDHP zwS>`zr)zn-mN427MjOItLl|w~UJLhHTMAK1AxbGkDTOGd5Tz8NltPqJh*AnsN+D`F zOW`bqvlPx!I7{Iyg|igSQaDTDEEQ~@gk=f&6>wI-RRLE8TorItz*Pa4X{TM<%8;lZ zyq#+}*PUEJ^e2%11Y|sq z5E95k0(nRvgn7t&9`c??2npmTf&3&`t($?*8Tgn1*9^F3z%|1@Y08tPJZZ|4raWoN zlcrYsh2*Y~+!Z4ILULG04hzX)Avr80hlS*@&~h$-a{-(S;9LOb0yr1Ixd6@ua4vvz zfh~a$ISG-25IG2u`VgrPk@^rR4w2#zDK>d^8A%CY!6*3Tgu9w>R}=1P!d*?ctGz_O zoUm6D_G-dj?M>nTXkx(C-i zxbCrXs?pNn|BI%EkwO?bg^^PjIfao^7&(QJQy4jgk(1d^+w~`t&dH>4GHIMl8Yh#+ z$)s^I>6=XYCX>F&ma_oP0yqocEP%5B&H^|K;4FZ%0L}u-c@)m0aGLkRqi`OD^C+B0 z;XDfGQ8>;1C~iClTJ-KHR;r(QY3(-jWuP3{nJgg1iwk-?GN6Q+n`OgE9wL5%7OhjHDj!6|4)fbrdF;B>UZ z%=1A%_%TY)&-4u%;MR~|z{cdzx-sH!V~VpeIX0#^8*?ebS zAQtkC`D5{Uj60r^I#v9QZwejxZg4&C0(XjWVzQVnp5-jV*TlObODtm-6ccuU-bGcK zPC$oF?+fZzI=cBI(P9!>><8qKPOVe>wE?Ki0{~|WBe}Vf4XQP&Zn1hnYfBC z6GOxZai4ft%oKA(s(6Fb5*Le4#F`ktL{pm3!)Z&OraOI^0qjyUk}u|uh$r}3o+jSp zOL?|f&R6oP^eD9I4EjSChzsch^$|DnW#DcxUOXzE6fcN>inqjvB1e2Gz7$^#Z#RB; zu%M1Oe9XPUt;6etcZ|M!L~wr{aqPG;!HPTTgm>M4+wkCzbwiSTTfAe@8$EX7nE3j2 z#Bsw%$DcgfU=xeYEw&=a`{U268;&#SX$7vCShb;Aqe2jaJG*gk$Je(Q!8#ovM7y5Ys~qwrfdyd?f!{MG>{7`=L+ zZa8u5-NWM_sTt{7>(V@jF{4!hYZBd!jDe3P?&n3N`v^44Kq_U(RTGnsbzvaCxpKF=dvZB>l ztva-Ny46Uok6Jfr-L~}=t#5Drct!i}f((-w$zb<@@y@_ZGfn-^}R>H@XAd z8<@#I(!HBgP`_fg;dN$jD7S=p`uDh_-7)T1cN{bJ?{n|xOxy|VUG;$bpgYN(>^|f^ z%pCql-N!f|cUt%aX7f*XXSg%jqx4DU^*`l4?LOmv;Xcd!{yFaRVZn}8FEY=69(!7G zhOV0`_tpN9C#|oUNB-ct>Wy} zFVt$aMy*v}h9$Ec)~T=6dbL4)!-=dLRe}0eZBm=n7PVDswk{gu_{qzs#NXZ zyw+W6x7wrjs(qZ?TCNVLgX$2gzC6~2<%c$g3PRt8Hib5`K5R>9Yv{Ysw$S!aQK&dn z5-JUqg?6wT)2`6&(4NrV&_4EMDi0k99dxIJ4uuYzJ%mHwvxo2x>>&JO=%>)n>>>PX z=r?w43WdYrdf|kS9}a|_aL`Q)$A{zCr**C>WYyWp;ZwqmnO7pvWYdc~8SQIK59c)c zA65EaHIJe-dUy_fq&DnS^6Jy#XsnC zU*NQJE@VE%MX?zb9T{7>+ZpBD!|aJMcK*b@jI`X(Y0VRyiOfiO(3!-z%R}tx_6R$= zJ?2btRO6LpY{v|ji#e~ilj_2my_cyToZH)r`7WGKqx!KL@OpKFx>4P%Zc&5P5H*aG zeD6?qahmTaHJTHB?`2NR1oeQL#L2#osK+?l_i;5tJ)vf)r#a#GIWA6xKWTY^8@fg)I zbEJCyVV2af`A{<$Lw$l-QL~(3&B^)3+3aj_zH^G5GUu?|DR--&if12Zm-Dri@|0!|-!La_C#d?W z0Xy9`VyF9))XC};cC9{@)3;A&#@ZR|BHK)zss5%~sFtd=I!Cop=c^0Uh3X=8iR!32 zt4mck)m>e#da5hcRjQA=hBLbRs{x$TJx~qetnQ)AbQ`YjR3p^g>K-*ljZ^oj@oJ)a zP)$}3t4GxoHBC)dGu4yoDfNtcmicgV)j!mHPV-ir(yhIaSC1X_>zn=b*$C!DcGN%F zJH?#R?zLsN{C3`joZWqqcd>Vg*)`wm#IE^W*fYPI*WJ6^>*)>hZsjcR(cXA(GCSk% zW#`0$-VvRohlLx28--7*Rg&1_ey#bP&F`G>Ie%h>kF&wyHvG=x zcYgT%zp%vE;qZn0+Vi`J-^Khc;nyMD!P?|%i+s#(BGK1*yFQx{g$rrt>QZxR>J4?q z=a5j?=yh{6`>f-1yDi$?4*hPAhF^@1clbYO`C#;X2%0|ZFLm8$ z`eW$&RJ8qZ^nC^z{{%We>knEV(_A|OR5j!LBl^_M(YmgGF&bi5x5+fK0$=_UJeM*E z%9qUl_U`5CyH~33UgNHfeY&9f?)B=sH>&U6tiF4z`tH^0ySJibspUSh#+~`=HQ`@Z;~qGp zmOJx=kHs6P9e!dhcg8cT(`i--#L}sU)wr|PqT1c-Sj&A=E%(l~-1BPOIYGEOd`A4L z!-@N>mizOy+`p;iKB<l zSrfRDNY(__vQE*gR%~onD<<02if7u@if6G}QL#eNtX6DoS1X=vS1Vo;m&j_xE_StI zce`5ga=Tiwr(Lbs*REE)&aPIR7%UBziU;g!#Ru(b#YuLxVv1d@m}*xmzF}7@zUeM@ zSBQ7rmG0*vhZW9i#Zu1fC=$!;TEskAB#UAruC~7M3VNxS${hJ80*eAA&f9!L-5=MK zx!&)^6$E9ld2lrQOWegi5Tg=Y75`ox8*j9%tI`i?Jo1>Pau zDHIAd` zspjBf&B%NpTb6@4-fS>e9srkU4~*)zV4iLVF87`W^SypxfjSG^EY19dcRVZ+B@kOl ztMBGXw!X*k>M>vm;|w9{d}GEGc)!3UqFR9&>TEDuoeM@Kv&{mVuq0;Q+-V|_R_9!O zpM8Fwd9KHU`Fb+AO{15A68#{tw&Ja=?Qt^xUsgI7!!C&4V$1zfDU zg4y~^Fsd%Ik~mIwss~Rp^i!7WSujWUHoW>7FkjCBw-Zxytae+zMRdV-_r?yJeco%W zI^&#Eqel^W6#jJi0~nG20W;JUU?yjlniQFk7qdf*k;0E)ju!!QbqI`VBa1w3a_cuTF`rF*%SM!Y;r zGrf;3&DHNhGfDGxmS*YqEzQv%LXDoLTN=>=pr+>2mfD*CGbu}#zkm_>E0`gF12fg_ zV3xWAT&(+p*|HMM@v^~Obr-l)*8`*4=vkgNdbZsA2+Y?lz|FJ?tiRV8+(&d4n9XQf ztacyMCN8xeK4v`&CAoK<}LTW0`v8HaHE<9Zq^&Y zZTcIqI95_yxX;wzf%(VVSI?Afx@SrzqKs|N(A&Xm&y-S*#y>u)O1sp!>mA1Z&uWZk z(v+^^z=)a+W_U$lroIWx(!0QH^&FVvZ3AEucql`_8=sjSDGW9=OnVKKf`@no% z4wl&V^si5q_|p{pymuL+} z^#x#_z7SmQJp<-@*MJ4e)(PdIJ@0r}9Cxv90%q%`;8Kq~xu1IE#$Dl&8+WCL9dlQC zso-kwWpIu6Dp=EcQXi1CsWbW7X#DZ5XG>fnsx_FQ&H=Mk8!)O&8h_t)^F3M)|BWfeK)P=Ut0e^t~pl5qNl4c7*X}W49{4=Ol72#rHphItBb&FPPR63 zd<)D~9l@wJ?s@tWaJgsfX}<0UZc@f&vo`v_O&k3$*3?w@V@HutZ^Z>AC#Q zN~r;cTMYuU)j%++Q^0)v3b;+b1ePf36F!f%W_@vs=mlW5ejWVt*34)?y1LcysuRIX z{U(^L{tia7|c|| z!7OzrxLDr+W~)=d9M7}_xoQNsR2z+nYExEu+LYCD&$J);ItkpYuO(I4`bKc6eh-Z5 zMc~Ie0xt8GfS-C%aD}%FTd$PU8prd9x)03Id0@7B5RB^OV7?j;Zc-DCZ@mKiM6U#w z|FI3#tBqfM2+UEBf>BK#2;pIHll~gqroRG9)MT3$({|bZ3$53mow@S|>!?n{ce*+q zj3}%Yr8Nc2RMWsLH62{6&H!_~^M3xu zE௦UzIB8}M^{cv+Mn&IYZG{9Y=H-S-I2qG)nv)BrMrrrkT|GtiRMlaGmQzjAh z449#dz--TyPmVqeF3}|*aoVuT%u~J7JY{xIt$sDFyjcO;*E2atS53i)dKS#^ior~M zGnl1!gW0MXnB#2+b9Du{#QP5XefwqXLqzWdGt^u#o3liXjP`^1`T$7EOsdp8?jzcS znq};mN;dz;p3S$<604sX8xZ0BDfUkE`!ZJbd7iFJX+%^*FhiLfWhzr9S<2Y2#R}Pz z{tPfzhry`s2!RDoL_?Z$8j2cNcT2?5s%U*Ors~c9y;(l&W*UFd*6T&Z!?(beGBG#TfkTe ztLwPW@Jv3b{kF`lrPD)j$-#wHe9X=g%!um7`;O_IpGxhxkvE^48P{i3B)!XgZ$H1z_DrTOHKTg*qsU?XKy#;kTrYOn zX>wd2auj1?4-w1Lyz{3qHnxzrfLyVhIZqpS+b?0?jtbWL+m-(EQhJH=1Ieqp8T`WzL-J!{W`SLPLlVvwcQ)DkoQ)N#}(og{7%dTAC)u zLQ~;mz8*ALjH@+%hN1Pk*os4JymwHG)`SVA?!T0Xd6O^_F>e?~SD5(+O_8Tqnkvm3$b4d+2{jUV($Zx4 zgrzC+DN9r3EKAemT~H&L5tb&)J1tF>j3VNmCU1ZmiHx>1Szd2xiX3QZs=V3KGz1Rx)!e zO_rQf%>OBJjHPLEGSo<9sHMqrh@~m=@0O;@$1P2h#(K?{#$qSS-e8Kn`oENjB4i_x z+bm6%11wFEH(Hu1Z?ZH^n(;y-5n~sOL@u{9MfR{XRZh1wP1b`NiJV|*vg~hZiZtVl zMj}ltO_NigMpvd;nk=VUnj#;wG*y~WwN!NKX{eFRGnOXH*_Nir8J4EXN3291wi0>B zN@Nn4iaie)-Nv4qZw?_w6x;H)K7i4dNwvQ_&_fO|_7aF^=NazI_`PwNflJj~rDHBn zGq?E17!P_15998^oiP_|1TOP&$&a~UJ#cvmm*p`R>;^7;(TF$mf=yg4z(4@|ROKGr zU)!CL-(&8_!b~#!d{I8;zBI<)(z|GpB22x%%ey`w(nbmE$XqAwNxjWb1 z)o{`KtLCBwV=jQtM#NQ>yS6P-?F#sCU>(;ml2sEGi8?iNG^se zWWIGXW2rqom#Uc#eW@X4UPDc6AvRphk-3r`dXVnbqOZ)X_4FcgCf&R!PZA#2i1e}`{X{A#&jd+ce$rY2X_ zEVJ$)mIB=3szR^zv>x_gq5QyxLtc2!9$nSX1EP8Sv|9h2Xm$jSg^c9mALqY-{Zf4_ XwkE6s{u4>Ex2wa?N-FMLe0cu@Z#UM5 literal 0 HcmV?d00001 diff --git a/upload/system/library/sitecreator/elFinder/php/MySQLStorage.sql b/upload/system/library/sitecreator/elFinder/php/MySQLStorage.sql new file mode 100644 index 0000000..205e9c3 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/MySQLStorage.sql @@ -0,0 +1,47 @@ +DROP TABLE IF EXISTS `elfinder_file`; +CREATE TABLE IF NOT EXISTS `elfinder_file` ( + `id` int(7) unsigned NOT NULL auto_increment, + `parent_id` int(7) unsigned NOT NULL, + `name` varchar(255) NOT NULL, + `content` longblob NOT NULL, + `size` int(10) unsigned NOT NULL default '0', + `mtime` int(10) unsigned NOT NULL default '0', + `mime` varchar(256) NOT NULL default 'unknown', + `read` enum('1', '0') NOT NULL default '1', + `write` enum('1', '0') NOT NULL default '1', + `locked` enum('1', '0') NOT NULL default '0', + `hidden` enum('1', '0') NOT NULL default '0', + `width` int(5) NOT NULL default '0', + `height` int(5) NOT NULL default '0', + PRIMARY KEY (`id`), + UNIQUE KEY `parent_name` (`parent_id`, `name`), + KEY `parent_id` (`parent_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; + +INSERT INTO `elfinder_file` +(`id`, `parent_id`, `name`, `content`, `size`, `mtime`, `mime`, `read`, `write`, `locked`, `hidden`, `width`, `height`) VALUES +('1' , '0', 'DATABASE', '', '0', '0','directory', '1', '1', '0', '0', '0', '0'); + +DROP TABLE IF EXISTS `elfinder_trash`; +CREATE TABLE IF NOT EXISTS `elfinder_trash` ( + `id` int(7) unsigned NOT NULL auto_increment, + `parent_id` int(7) unsigned NOT NULL, + `name` varchar(255) NOT NULL, + `content` longblob NOT NULL, + `size` int(10) unsigned NOT NULL default '0', + `mtime` int(10) unsigned NOT NULL default '0', + `mime` varchar(256) NOT NULL default 'unknown', + `read` enum('1', '0') NOT NULL default '1', + `write` enum('1', '0') NOT NULL default '1', + `locked` enum('1', '0') NOT NULL default '0', + `hidden` enum('1', '0') NOT NULL default '0', + `width` int(5) NOT NULL default '0', + `height` int(5) NOT NULL default '0', + PRIMARY KEY (`id`), + UNIQUE KEY `parent_name` (`parent_id`, `name`), + KEY `parent_id` (`parent_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci; + +INSERT INTO `elfinder_trash` +(`id`, `parent_id`, `name`, `content`, `size`, `mtime`, `mime`, `read`, `write`, `locked`, `hidden`, `width`, `height`) VALUES +('1' , '0', 'DB Trash', '', '0', '0','directory', '1', '1', '0', '0', '0', '0'); diff --git a/upload/system/library/sitecreator/elFinder/php/autoload.php b/upload/system/library/sitecreator/elFinder/php/autoload.php new file mode 100644 index 0000000..720a4fd --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/autoload.php @@ -0,0 +1,55 @@ + 'elFinder.class.php', + 'elFinderConnector' => 'elFinderConnector.class.php', + 'elFinderEditor' => 'editors/editor.php', + 'elFinderLibGdBmp' => 'libs/GdBmp.php', + 'elFinderPlugin' => 'elFinderPlugin.php', + 'elFinderPluginAutoResize' => 'plugins/AutoResize/plugin.php', + 'elFinderPluginAutoRotate' => 'plugins/AutoRotate/plugin.php', + 'elFinderPluginNormalizer' => 'plugins/Normalizer/plugin.php', + 'elFinderPluginSanitizer' => 'plugins/Sanitizer/plugin.php', + 'elFinderPluginWatermark' => 'plugins/Watermark/plugin.php', + 'elFinderSession' => 'elFinderSession.php', + 'elFinderSessionInterface' => 'elFinderSessionInterface.php', + 'elFinderVolumeDriver' => 'elFinderVolumeDriver.class.php', + 'elFinderVolumeDropbox2' => 'elFinderVolumeDropbox2.class.php', + 'elFinderVolumeFTP' => 'elFinderVolumeFTP.class.php', + 'elFinderVolumeFlysystemGoogleDriveCache' => 'elFinderFlysystemGoogleDriveNetmount.php', + 'elFinderVolumeFlysystemGoogleDriveNetmount' => 'elFinderFlysystemGoogleDriveNetmount.php', + 'elFinderVolumeGoogleDrive' => 'elFinderVolumeGoogleDrive.class.php', + 'elFinderVolumeGroup' => 'elFinderVolumeGroup.class.php', + 'elFinderVolumeLocalFileSystem' => 'elFinderVolumeLocalFileSystem.class.php', + 'elFinderVolumeMySQL' => 'elFinderVolumeMySQL.class.php', + 'elFinderVolumeTrash' => 'elFinderVolumeTrash.class.php', + ); + if (isset($map[$name])) { + return include_once(ELFINDER_PHP_ROOT_PATH . '/' . $map[$name]); + } + $prefix = substr($name, 0, 14); + if (substr($prefix, 0, 8) === 'elFinder') { + if ($prefix === 'elFinderVolume') { + $file = ELFINDER_PHP_ROOT_PATH . '/' . $name . '.class.php'; + return (is_file($file) && include_once($file)); + } else if ($prefix === 'elFinderPlugin') { + $file = ELFINDER_PHP_ROOT_PATH . '/plugins/' . substr($name, 14) . '/plugin.php'; + return (is_file($file) && include_once($file)); + } else if ($prefix === 'elFinderEditor') { + $file = ELFINDER_PHP_ROOT_PATH . '/editors/' . substr($name, 14) . '/editor.php'; + return (is_file($file) && include_once($file)); + } + } + return false; +} + +if (version_compare(PHP_VERSION, '5.3', '<')) { + spl_autoload_register('elFinderAutoloader'); +} else { + spl_autoload_register('elFinderAutoloader', true, true); +} + diff --git a/upload/system/library/sitecreator/elFinder/php/connector.maximal.php-dist b/upload/system/library/sitecreator/elFinder/php/connector.maximal.php-dist new file mode 100644 index 0000000..ce7592a --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/connector.maximal.php-dist @@ -0,0 +1,433 @@ +file = $path; + $dir = dirname($path); + if (!is_dir($dir)) { + mkdir($dir); + } + } + + /** + * Create log record + * + * @param string $cmd command name + * @param array $result command result + * @param array $args command arguments from client + * @param elFinder $elfinder elFinder instance + * @param elFinderVolumeDriver $volume current volume driver instance + * @return void|true + * @author Dmitry (dio) Levashov + **/ + public function log($cmd, $result, $args, $elfinder, $volume) + { + $log = $cmd.' ['.date('d.m H:s')."]\n"; + + if (!empty($result['error'])) { + $log .= "\tERROR: ".implode(' ', $result['error'])."\n"; + } + + if (!empty($result['warning'])) { + $log .= "\tWARNING: ".implode(' ', $result['warning'])."\n"; + } + + if (!empty($result['removed'])) { + foreach ($result['removed'] as $file) { + // removed file contain additional field "realpath" + $log .= "\tREMOVED: ".$file['realpath']."\n"; + } + } + + if (!empty($result['added'])) { + foreach ($result['added'] as $file) { + $log .= "\tADDED: ".$elfinder->realpath($file['hash'])."\n"; + } + } + + if (!empty($result['changed'])) { + foreach ($result['changed'] as $file) { + $log .= "\tCHANGED: ".$elfinder->realpath($file['hash'])."\n"; + } + } + + $this->write($log); + } + + /** + * Write log into file + * + * @param string $log log record + * @return void + * @author Dmitry (dio) Levashov + **/ + protected function write($log) + { + + if (($fp = @fopen($this->file, 'a'))) { + fwrite($fp, $log."\n"); + fclose($fp); + } + } + +} // END class +// Make logger instance +$logger = new elFinderSimpleLogger('.log.txt'); + +// Documentation for connector options: +// https://github.com/Studio-42/elFinder/wiki/Connector-configuration-options +$opts = array( + 'debug' => true, // enable debug mode + 'roots' => array( + // Items volume + array( + 'driver' => 'LocalFileSystem', // driver for accessing file system (REQUIRED) + 'path' => '../files/', // path to files (REQUIRED) + 'URL' => dirname($_SERVER['PHP_SELF']) . '/../files/', // URL to files (REQUIRED) + 'trashHash' => 't1_Lw', // elFinder's hash of trash folder + 'winHashFix' => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too + 'uploadDeny' => array('all'), // All Mimetypes not allowed to upload + 'uploadAllow' => array('image/x-ms-bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/x-icon', 'text/plain'), // Mimetype `image` and `text/plain` allowed to upload + 'uploadOrder' => array('deny', 'allow'), // allowed Mimetype `image` and `text/plain` only + 'accessControl' => 'access', // disable and hide dot starting files (OPTIONAL) + 'attributes' => array( // additional thumbnail directories + 'pattern' => '~^/\.tmb(?:Cloud|Netmount)$~', + 'read' => false, + 'write' => false, + 'locked' => true, + 'hidden' => true ), + ), + // Trash volume + array( + 'id' => '1', + 'driver' => 'Trash', + 'path' => '../files/.trash/', + 'tmbURL' => dirname($_SERVER['PHP_SELF']) . '/../files/.trash/.tmb/', + 'winHashFix' => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too + 'uploadDeny' => array('all'), // Recomend the same settings as the original volume that uses the trash + 'uploadAllow' => array('image/x-ms-bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/x-icon', 'text/plain'), // Same as above + 'uploadOrder' => array('deny', 'allow'), // Same as above + 'accessControl' => 'access', // Same as above + ), + ), + // some bind functions + 'bind' => array( + // enable logger + // '*' => array($logger, 'log'), + 'mkdir mkfile rename duplicate upload rm paste' => array($logger, 'log'), + // enable plugins + 'archive.pre ls.pre mkdir.pre mkfile.pre rename.pre upload.pre' => array( + 'Plugin.Normalizer.cmdPreprocess', + 'Plugin.Sanitizer.cmdPreprocess' + ), + 'upload.presave' => array( + 'Plugin.AutoRotate.onUpLoadPreSave', + 'Plugin.AutoResize.onUpLoadPreSave', + 'Plugin.Watermark.onUpLoadPreSave', + 'Plugin.Normalizer.onUpLoadPreSave', + 'Plugin.Sanitizer.onUpLoadPreSave', + ), + ), + // volume options of netmount volumes + 'optionsNetVolumes' => array( + '*' => array( // "*" is all of netmount volumes + 'tmbURL' => dirname($_SERVER['PHP_SELF']) . '/../files/.tmbNetmount/', + 'tmbPath' => '../files/.tmbNetmount', + 'tmbGcMaxlifeHour' => 1, // 1 hour + 'tmbGcPercentage' => 10, // 10 execute / 100 tmb querys + 'plugin' => array( + 'AutoResize' => array( + 'enable' => false + ), + 'Watermark' => array( + 'enable' => false + ), + 'Normalizer' => array( + 'enable' => false + ), + 'Sanitizer' => array( + 'enable' => false + ) + ), + ) + ), +); + +// Extended other volume types +// To get an access token or refresh token, see the elFinder wiki. +// https://github.com/Studio-42/elFinder/wiki/How-to-get-OAuth-token + +// Thumbnail settings for cloud volumes +$tmbConfig = array( + 'tmbPath' => '../files/.tmbCloud', + 'tmbURL' => dirname($_SERVER['PHP_SELF']) . '/../files/.tmbCloud/', + 'tmbGcMaxlifeHour' => 2160, // 90 days + 'tmbGcPercentage' => 5, // 5 execute / 100 tmb querys +); + +// MySQL config +$mySqlConfig = array( + 'path' => 1, + 'host' => '127.0.0.1', + 'user' => '', // @String DB user name + 'pass' => '', // @String DB user password + 'db' => '', // @String Database name + 'uploadMaxSize' => '10M', // It should be less than "max_allowed_packet" value of MySQL setting +); +// MySQL volume +$opts['roots'][] = array_merge($tmbConfig, $mySqlConfig, array( + 'driver' => 'MySQL', + 'trashHash' => 'tm1_MQ', // set trash to MySQL trash (tm1_) 1 (MQ) + 'files_table' => 'elfinder_file', +)); +// MySQL trash volume +$opts['roots'][] = array_merge($tmbConfig, $mySqlConfig, array( + 'id' => '1', // volume id became "tm1_" + 'alias' => 'DB Trash', + 'driver' => 'TrashMySQL', + 'files_table' => 'elfinder_trash', +)); + +// Volume group +$opts['roots'][] = array( + 'id' => '1', // volume id became "g1_" + 'alias' => 'CloudVolumes', + 'driver' => 'Group', + 'rootCssClass' => 'elfinder-navbar-root-network' // set volume icon +); + +// FTP volume +$opts['roots'][] = array_merge($tmbConfig, array( + 'phash' => 'g1_Lw', // set parent to Volume group (g1_) root "/" (Lw) + 'driver' => 'FTP', + 'host' => 'ftp.jaist.ac.jp', + 'user' => 'anonymous', + 'path' => '/', + 'owner' => false, +)); + +// To enable the following cloud volumes, first complete the steps +// for enabling each network-mounted volume described earlier in this file. + +// Box volume +// Require constant "ELFINDER_BOX_CLIENTID" and "ELFINDER_BOX_CLIENTSECRET" +$opts['roots'][] = array_merge($tmbConfig, array( + 'phash' => 'g1_Lw', // set parent to Volume group (g1_) root "/" (Lw) + 'driver' => 'Box', + 'path' => '/', // or folder id as root + 'accessToken' => '', // @JSON String access token including refresh token +)); + +// Dropbox volume +// Require constant "ELFINDER_DROPBOX_APPKEY" and "ELFINDER_DROPBOX_APPSECRET" +$opts['roots'][] = array_merge($tmbConfig, array( + 'phash' => 'g1_Lw', // set parent to Volume group (g1_) root "/" (Lw) + 'driver' => 'Dropbox2', + 'path' => '/', // or folder path as root + 'access_token' => '', // @String your access token +)); + +// GoogleDrive volume with refresh token +// Require constant "ELFINDER_GOOGLEDRIVE_CLIENTID" and "ELFINDER_GOOGLEDRIVE_CLIENTSECRET" +$opts['roots'][] = array_merge($tmbConfig, array( + 'phash' => 'g1_Lw', // set parent to Volume group (g1_) root "/" (Lw) + 'driver' => 'GoogleDrive', + 'path' => '/', // or folder id as root + 'refresh_token' => '', // @String your refresh token +)); + +// GoogleDrive volume with service account +// Require constant "ELFINDER_GOOGLEDRIVE_CLIENTID" and "ELFINDER_GOOGLEDRIVE_CLIENTSECRET" +$opts['roots'][] = array_merge($tmbConfig, array( + 'phash' => 'g1_Lw', // set parent to Volume group (g1_) root "/" (Lw) + 'driver' => 'GoogleDrive', + 'path' => '/', // or folder id as root + 'serviceAccountConfigFile' => '', // @String path to config json file +)); + +// OneDrive volume +// Require constant "ELFINDER_ONEDRIVE_CLIENTID" and "ELFINDER_ONEDRIVE_CLIENTSECRET" +$opts['roots'][] = array_merge($tmbConfig, array( + 'phash' => 'g1_Lw', // set parent to Volume group (g1_) root "/" (Lw) + 'driver' => 'OneDrive', + 'path' => '/', // or folder id as root + 'accessToken' => '', // @JSON String access token including refresh token +)); + + +// run elFinder +$connector = new elFinderConnector(new elFinder($opts)); +$connector->run(); + diff --git a/upload/system/library/sitecreator/elFinder/php/connector.minimal.php-dist b/upload/system/library/sitecreator/elFinder/php/connector.minimal.php-dist new file mode 100644 index 0000000..fcafdd3 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/connector.minimal.php-dist @@ -0,0 +1,179 @@ + true, + 'roots' => array( + // Items volume + array( + 'driver' => 'LocalFileSystem', // driver for accessing file system (REQUIRED) + 'path' => '../files/', // path to files (REQUIRED) + 'URL' => dirname($_SERVER['PHP_SELF']) . '/../files/', // URL to files (REQUIRED) + 'trashHash' => 't1_Lw', // elFinder's hash of trash folder + 'winHashFix' => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too + 'uploadDeny' => array('all'), // All Mimetypes not allowed to upload + 'uploadAllow' => array('image/x-ms-bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/x-icon', 'text/plain'), // Mimetype `image` and `text/plain` allowed to upload + 'uploadOrder' => array('deny', 'allow'), // allowed Mimetype `image` and `text/plain` only + 'accessControl' => 'access' // disable and hide dot starting files (OPTIONAL) + ), + // Trash volume + array( + 'id' => '1', + 'driver' => 'Trash', + 'path' => '../files/.trash/', + 'tmbURL' => dirname($_SERVER['PHP_SELF']) . '/../files/.trash/.tmb/', + 'winHashFix' => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too + 'uploadDeny' => array('all'), // Recomend the same settings as the original volume that uses the trash + 'uploadAllow' => array('image/x-ms-bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/x-icon', 'text/plain'), // Same as above + 'uploadOrder' => array('deny', 'allow'), // Same as above + 'accessControl' => 'access', // Same as above + ), + ) +); + +// run elFinder +$connector = new elFinderConnector(new elFinder($opts)); +$connector->run(); + diff --git a/upload/system/library/sitecreator/elFinder/php/elFinder.class.php b/upload/system/library/sitecreator/elFinder/php/elFinder.class.php new file mode 100644 index 0000000..9167ab5 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinder.class.php @@ -0,0 +1,5273 @@ + array('id' => true), + 'archive' => array('targets' => true, 'type' => true, 'mimes' => false, 'name' => false), + 'callback' => array('node' => true, 'json' => false, 'bind' => false, 'done' => false), + 'chmod' => array('targets' => true, 'mode' => true), + 'dim' => array('target' => true, 'substitute' => false), + 'duplicate' => array('targets' => true, 'suffix' => false), + 'editor' => array('name' => true, 'method' => true, 'args' => false), + 'extract' => array('target' => true, 'mimes' => false, 'makedir' => false), + 'file' => array('target' => true, 'download' => false, 'cpath' => false, 'onetime' => false), + 'get' => array('target' => true, 'conv' => false), + 'info' => array('targets' => true, 'compare' => false), + 'ls' => array('target' => true, 'mimes' => false, 'intersect' => false), + 'mkdir' => array('target' => true, 'name' => false, 'dirs' => false), + 'mkfile' => array('target' => true, 'name' => true, 'mimes' => false), + 'netmount' => array('protocol' => true, 'host' => true, 'path' => false, 'port' => false, 'user' => false, 'pass' => false, 'alias' => false, 'options' => false), + 'open' => array('target' => false, 'tree' => false, 'init' => false, 'mimes' => false, 'compare' => false), + 'parents' => array('target' => true, 'until' => false), + 'paste' => array('dst' => true, 'targets' => true, 'cut' => false, 'mimes' => false, 'renames' => false, 'hashes' => false, 'suffix' => false), + 'put' => array('target' => true, 'content' => '', 'mimes' => false, 'encoding' => false), + 'rename' => array('target' => true, 'name' => true, 'mimes' => false, 'targets' => false, 'q' => false), + 'resize' => array('target' => true, 'width' => false, 'height' => false, 'mode' => false, 'x' => false, 'y' => false, 'degree' => false, 'quality' => false, 'bg' => false), + 'rm' => array('targets' => true), + 'search' => array('q' => true, 'mimes' => false, 'target' => false, 'type' => false), + 'size' => array('targets' => true), + 'subdirs' => array('targets' => true), + 'tmb' => array('targets' => true), + 'tree' => array('target' => true), + 'upload' => array('target' => true, 'FILES' => true, 'mimes' => false, 'html' => false, 'upload' => false, 'name' => false, 'upload_path' => false, 'chunk' => false, 'cid' => false, 'node' => false, 'renames' => false, 'hashes' => false, 'suffix' => false, 'mtime' => false, 'overwrite' => false, 'contentSaveId' => false), + 'url' => array('target' => true, 'options' => false), + 'zipdl' => array('targets' => true, 'download' => false) + ); + + /** + * Plugins instance + * + * @var array + **/ + protected $plugins = array(); + + /** + * Commands listeners + * + * @var array + **/ + protected $listeners = array(); + + /** + * script work time for debug + * + * @var string + **/ + protected $time = 0; + /** + * Is elFinder init correctly? + * + * @var bool + **/ + protected $loaded = false; + /** + * Send debug to client? + * + * @var string + **/ + protected $debug = false; + + /** + * Call `session_write_close()` before exec command? + * + * @var bool + */ + protected $sessionCloseEarlier = true; + + /** + * SESSION use commands @see __construct() + * + * @var array + */ + protected $sessionUseCmds = array(); + + /** + * session expires timeout + * + * @var int + **/ + protected $timeout = 0; + + /** + * Temp dir path for Upload + * + * @var string + */ + protected $uploadTempPath = ''; + + /** + * Max allowed archive files size (0 - no limit) + * + * @var integer + */ + protected $maxArcFilesSize = 0; + + /** + * undocumented class variable + * + * @var string + **/ + protected $uploadDebug = ''; + + /** + * Max allowed numbar of targets (0 - no limit) + * + * @var integer + */ + public $maxTargets = 1000; + + /** + * Errors from PHP + * + * @var array + **/ + public static $phpErrors = array(); + + /** + * Errors from not mounted volumes + * + * @var array + **/ + public $mountErrors = array(); + + + /** + * Archivers cache + * + * @var array + */ + public static $archivers = array(); + + /** + * URL for callback output window for CORS + * redirect to this URL when callback output + * + * @var string URL + */ + protected $callbackWindowURL = ''; + + /** + * hash of items to unlock on command completion + * + * @var array hashes + */ + protected $autoUnlocks = array(); + + /** + * Item locking expiration (seconds) + * Default: 3600 secs + * + * @var integer + */ + protected $itemLockExpire = 3600; + + /** + * Additional request querys + * + * @var array|null + */ + protected $customData = null; + + /** + * Ids to remove of session var "urlContentSaveIds" for contents uploading by URL + * + * @var array + */ + protected $removeContentSaveIds = array(); + + /** + * Flag of throw Error on exec() + * + * @var boolean + */ + protected $throwErrorOnExec = false; + + /** + * Default params of toastParams + * + * @var array + */ + protected $toastParamsDefault = array( + 'mode' => 'warning', + 'prefix' => '' + ); + + /** + * Toast params of runtime notification + * + * @var array + */ + private $toastParams = array(); + + /** + * Toast messages of runtime notification + * + * @var array + */ + private $toastMessages = array(); + + /** + * Optional UTF-8 encoder + * + * @var callable || null + */ + private $utf8Encoder = null; + + /** + * Seekable URL file pointer ids - for getStreamByUrl() + * + * @var array + */ + private static $seekableUrlFps = array(); + + // Errors messages + const ERROR_ACCESS_DENIED = 'errAccess'; + const ERROR_ARC_MAXSIZE = 'errArcMaxSize'; + const ERROR_ARC_SYMLINKS = 'errArcSymlinks'; + const ERROR_ARCHIVE = 'errArchive'; + const ERROR_ARCHIVE_EXEC = 'errArchiveExec'; + const ERROR_ARCHIVE_TYPE = 'errArcType'; + const ERROR_CONF = 'errConf'; + const ERROR_CONF_NO_JSON = 'errJSON'; + const ERROR_CONF_NO_VOL = 'errNoVolumes'; + const ERROR_CONV_UTF8 = 'errConvUTF8'; + const ERROR_COPY = 'errCopy'; + const ERROR_COPY_FROM = 'errCopyFrom'; + const ERROR_COPY_ITSELF = 'errCopyInItself'; + const ERROR_COPY_TO = 'errCopyTo'; + const ERROR_CREATING_TEMP_DIR = 'errCreatingTempDir'; + const ERROR_DIR_NOT_FOUND = 'errFolderNotFound'; + const ERROR_EXISTS = 'errExists'; // 'File named "$1" already exists.' + const ERROR_EXTRACT = 'errExtract'; + const ERROR_EXTRACT_EXEC = 'errExtractExec'; + const ERROR_FILE_NOT_FOUND = 'errFileNotFound'; // 'File not found.' + const ERROR_FTP_DOWNLOAD_FILE = 'errFtpDownloadFile'; + const ERROR_FTP_MKDIR = 'errFtpMkdir'; + const ERROR_FTP_UPLOAD_FILE = 'errFtpUploadFile'; + const ERROR_INV_PARAMS = 'errCmdParams'; + const ERROR_INVALID_DIRNAME = 'errInvDirname'; // 'Invalid folder name.' + const ERROR_INVALID_NAME = 'errInvName'; // 'Invalid file name.' + const ERROR_LOCKED = 'errLocked'; // '"$1" is locked and can not be renamed, moved or removed.' + const ERROR_MAX_TARGTES = 'errMaxTargets'; // 'Max number of selectable items is $1.' + const ERROR_MKDIR = 'errMkdir'; + const ERROR_MKFILE = 'errMkfile'; + const ERROR_MKOUTLINK = 'errMkOutLink'; // 'Unable to create a link to outside the volume root.' + const ERROR_MOVE = 'errMove'; + const ERROR_NETMOUNT = 'errNetMount'; + const ERROR_NETMOUNT_FAILED = 'errNetMountFailed'; + const ERROR_NETMOUNT_NO_DRIVER = 'errNetMountNoDriver'; + const ERROR_NETUNMOUNT = 'errNetUnMount'; + const ERROR_NOT_ARCHIVE = 'errNoArchive'; + const ERROR_NOT_DIR = 'errNotFolder'; + const ERROR_NOT_FILE = 'errNotFile'; + const ERROR_NOT_REPLACE = 'errNotReplace'; // Object "$1" already exists at this location and can not be replaced with object of another type. + const ERROR_NOT_UTF8_CONTENT = 'errNotUTF8Content'; + const ERROR_OPEN = 'errOpen'; + const ERROR_PERM_DENIED = 'errPerm'; + const ERROR_REAUTH_REQUIRE = 'errReauthRequire'; // 'Re-authorization is required.' + const ERROR_RENAME = 'errRename'; + const ERROR_REPLACE = 'errReplace'; // 'Unable to replace "$1".' + const ERROR_RESIZE = 'errResize'; + const ERROR_RESIZESIZE = 'errResizeSize'; + const ERROR_RM = 'errRm'; // 'Unable to remove "$1".' + const ERROR_RM_SRC = 'errRmSrc'; // 'Unable remove source file(s)' + const ERROR_SAVE = 'errSave'; + const ERROR_SEARCH_TIMEOUT = 'errSearchTimeout'; // 'Timed out while searching "$1". Search result is partial.' + const ERROR_SESSION_EXPIRES = 'errSessionExpires'; + const ERROR_TRGDIR_NOT_FOUND = 'errTrgFolderNotFound'; // 'Target folder "$1" not found.' + const ERROR_UNKNOWN = 'errUnknown'; + const ERROR_UNKNOWN_CMD = 'errUnknownCmd'; + const ERROR_UNSUPPORT_TYPE = 'errUsupportType'; + const ERROR_UPLOAD = 'errUpload'; // 'Upload error.' + const ERROR_UPLOAD_FILE = 'errUploadFile'; // 'Unable to upload "$1".' + const ERROR_UPLOAD_FILE_MIME = 'errUploadMime'; // 'File type not allowed.' + const ERROR_UPLOAD_FILE_SIZE = 'errUploadFileSize'; // 'File exceeds maximum allowed size.' + const ERROR_UPLOAD_NO_FILES = 'errUploadNoFiles'; // 'No files found for upload.' + const ERROR_UPLOAD_TEMP = 'errUploadTemp'; // 'Unable to make temporary file for upload.' + const ERROR_UPLOAD_TOTAL_SIZE = 'errUploadTotalSize'; // 'Data exceeds the maximum allowed size.' + const ERROR_UPLOAD_TRANSFER = 'errUploadTransfer'; // '"$1" transfer error.' + + /** + * Constructor + * + * @param array elFinder and roots configurations + * + * @author Dmitry (dio) Levashov + */ + public function __construct($opts) + { + // set default_charset + if (version_compare(PHP_VERSION, '5.6', '>=')) { + if (($_val = ini_get('iconv.internal_encoding')) && strtoupper($_val) !== 'UTF-8') { + ini_set('iconv.internal_encoding', ''); + } + if (($_val = ini_get('mbstring.internal_encoding')) && strtoupper($_val) !== 'UTF-8') { + ini_set('mbstring.internal_encoding', ''); + } + if (($_val = ini_get('internal_encoding')) && strtoupper($_val) !== 'UTF-8') { + ini_set('internal_encoding', ''); + } + } else { + if (function_exists('iconv_set_encoding') && strtoupper(iconv_get_encoding('internal_encoding')) !== 'UTF-8') { + iconv_set_encoding('internal_encoding', 'UTF-8'); + } + if (function_exists('mb_internal_encoding') && strtoupper(mb_internal_encoding()) !== 'UTF-8') { + mb_internal_encoding('UTF-8'); + } + } + ini_set('default_charset', 'UTF-8'); + + // define accept constant of server commands path + !defined('ELFINDER_TAR_PATH') && define('ELFINDER_TAR_PATH', 'tar'); + !defined('ELFINDER_GZIP_PATH') && define('ELFINDER_GZIP_PATH', 'gzip'); + !defined('ELFINDER_BZIP2_PATH') && define('ELFINDER_BZIP2_PATH', 'bzip2'); + !defined('ELFINDER_XZ_PATH') && define('ELFINDER_XZ_PATH', 'xz'); + !defined('ELFINDER_ZIP_PATH') && define('ELFINDER_ZIP_PATH', 'zip'); + !defined('ELFINDER_UNZIP_PATH') && define('ELFINDER_UNZIP_PATH', 'unzip'); + !defined('ELFINDER_RAR_PATH') && define('ELFINDER_RAR_PATH', 'rar'); + !defined('ELFINDER_UNRAR_PATH') && define('ELFINDER_UNRAR_PATH', 'unrar'); + !defined('ELFINDER_7Z_PATH') && define('ELFINDER_7Z_PATH', (substr(PHP_OS, 0, 3) === 'WIN') ? '7z' : '7za'); + !defined('ELFINDER_CONVERT_PATH') && define('ELFINDER_CONVERT_PATH', 'convert'); + !defined('ELFINDER_IDENTIFY_PATH') && define('ELFINDER_IDENTIFY_PATH', 'identify'); + !defined('ELFINDER_EXIFTRAN_PATH') && define('ELFINDER_EXIFTRAN_PATH', 'exiftran'); + !defined('ELFINDER_JPEGTRAN_PATH') && define('ELFINDER_JPEGTRAN_PATH', 'jpegtran'); + !defined('ELFINDER_FFMPEG_PATH') && define('ELFINDER_FFMPEG_PATH', 'ffmpeg'); + + !defined('ELFINDER_DISABLE_ZIPEDITOR') && define('ELFINDER_DISABLE_ZIPEDITOR', false); + + // enable(true)/disable(false) handling postscript on ImageMagick + // Should be `false` as long as there is a Ghostscript vulnerability + // see https://artifex.com/news/ghostscript-security-resolved/ + !defined('ELFINDER_IMAGEMAGICK_PS') && define('ELFINDER_IMAGEMAGICK_PS', false); + + // for backward compat + $this->version = (string)self::$ApiVersion; + + // set error handler of WARNING, NOTICE + $errLevel = E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE | E_STRICT | E_RECOVERABLE_ERROR; + if (defined('E_DEPRECATED')) { + $errLevel |= E_DEPRECATED | E_USER_DEPRECATED; + } + set_error_handler('elFinder::phpErrorHandler', $errLevel); + + // Associative array of file pointers to close at the end of script: ['temp file pointer' => true] + $GLOBALS['elFinderTempFps'] = array(); + // Associative array of files to delete at the end of script: ['temp file path' => true] + $GLOBALS['elFinderTempFiles'] = array(); + // regist Shutdown function + register_shutdown_function(array('elFinder', 'onShutdown')); + + // convert PATH_INFO to GET query + if (!empty($_SERVER['PATH_INFO'])) { + $_ps = explode('/', trim($_SERVER['PATH_INFO'], '/')); + if (!isset($_GET['cmd'])) { + $_cmd = $_ps[0]; + if (isset($this->commands[$_cmd])) { + $_GET['cmd'] = $_cmd; + $_i = 1; + foreach (array_keys($this->commands[$_cmd]) as $_k) { + if (isset($_ps[$_i])) { + if (!isset($_GET[$_k])) { + $_GET[$_k] = $_ps[$_i++]; + } + } else { + break; + } + } + } + } + } + + // set elFinder instance + elFinder::$instance = $this; + + // setup debug mode + $this->debug = (isset($opts['debug']) && $opts['debug'] ? true : false); + if ($this->debug) { + error_reporting(defined('ELFINDER_DEBUG_ERRORLEVEL') ? ELFINDER_DEBUG_ERRORLEVEL : -1); + ini_set('display_errors', '1'); + // clear output buffer and stop output filters + while (ob_get_level() && ob_end_clean()) { + } + } + + if (!interface_exists('elFinderSessionInterface')) { + include_once dirname(__FILE__) . '/elFinderSessionInterface.php'; + } + + // session handler + if (!empty($opts['session']) && $opts['session'] instanceof elFinderSessionInterface) { + $this->session = $opts['session']; + } else { + $sessionOpts = array( + 'base64encode' => !empty($opts['base64encodeSessionData']), + 'keys' => array( + 'default' => !empty($opts['sessionCacheKey']) ? $opts['sessionCacheKey'] : 'elFinderCaches', + 'netvolume' => !empty($opts['netVolumesSessionKey']) ? $opts['netVolumesSessionKey'] : 'elFinderNetVolumes' + ) + ); + if (!class_exists('elFinderSession')) { + include_once dirname(__FILE__) . '/elFinderSession.php'; + } + $this->session = new elFinderSession($sessionOpts); + } + // try session start | restart + $this->session->start(); + + // 'netmount' added to handle requests synchronously on unmount + $sessionUseCmds = array('netmount'); + if (isset($opts['sessionUseCmds']) && is_array($opts['sessionUseCmds'])) { + $sessionUseCmds = array_merge($sessionUseCmds, $opts['sessionUseCmds']); + } + + // set self::$volumesCnt by HTTP header "X-elFinder-VolumesCntStart" + if (isset($_SERVER['HTTP_X_ELFINDER_VOLUMESCNTSTART']) && ($volumesCntStart = intval($_SERVER['HTTP_X_ELFINDER_VOLUMESCNTSTART']))) { + self::$volumesCnt = $volumesCntStart; + } + + $this->time = $this->utime(); + $this->sessionCloseEarlier = isset($opts['sessionCloseEarlier']) ? (bool)$opts['sessionCloseEarlier'] : true; + $this->sessionUseCmds = array_flip($sessionUseCmds); + $this->timeout = (isset($opts['timeout']) ? $opts['timeout'] : 0); + $this->uploadTempPath = (isset($opts['uploadTempPath']) ? $opts['uploadTempPath'] : ''); + $this->callbackWindowURL = (isset($opts['callbackWindowURL']) ? $opts['callbackWindowURL'] : ''); + $this->maxTargets = (isset($opts['maxTargets']) ? intval($opts['maxTargets']) : $this->maxTargets); + elFinder::$commonTempPath = (isset($opts['commonTempPath']) ? realpath($opts['commonTempPath']) : dirname(__FILE__) . '/.tmp'); + if (!is_writable(elFinder::$commonTempPath)) { + elFinder::$commonTempPath = sys_get_temp_dir(); + if (!is_writable(elFinder::$commonTempPath)) { + elFinder::$commonTempPath = ''; + } + } + if (isset($opts['connectionFlagsPath']) && is_writable($opts['connectionFlagsPath'] = realpath($opts['connectionFlagsPath']))) { + elFinder::$connectionFlagsPath = $opts['connectionFlagsPath']; + } else { + elFinder::$connectionFlagsPath = elFinder::$commonTempPath; + } + + if (!empty($opts['tmpLinkPath'])) { + elFinder::$tmpLinkPath = realpath($opts['tmpLinkPath']); + } + if (!empty($opts['tmpLinkUrl'])) { + elFinder::$tmpLinkUrl = $opts['tmpLinkUrl']; + } + if (!empty($opts['tmpLinkLifeTime'])) { + elFinder::$tmpLinkLifeTime = $opts['tmpLinkLifeTime']; + } + if (!empty($opts['textMimes']) && is_array($opts['textMimes'])) { + elfinder::$textMimes = $opts['textMimes']; + } + if (!empty($opts['urlUploadFilter'])) { + $this->urlUploadFilter = $opts['urlUploadFilter']; + } + $this->maxArcFilesSize = isset($opts['maxArcFilesSize']) ? intval($opts['maxArcFilesSize']) : 0; + $this->optionsNetVolumes = (isset($opts['optionsNetVolumes']) && is_array($opts['optionsNetVolumes'])) ? $opts['optionsNetVolumes'] : array(); + if (isset($opts['itemLockExpire'])) { + $this->itemLockExpire = intval($opts['itemLockExpire']); + } + + // deprecated settings + $this->netVolumesSessionKey = !empty($opts['netVolumesSessionKey']) ? $opts['netVolumesSessionKey'] : 'elFinderNetVolumes'; + self::$sessionCacheKey = !empty($opts['sessionCacheKey']) ? $opts['sessionCacheKey'] : 'elFinderCaches'; + + // check session cache + $_optsMD5 = md5(json_encode($opts['roots'])); + if ($this->session->get('_optsMD5') !== $_optsMD5) { + $this->session->set('_optsMD5', $_optsMD5); + } + + // setlocale and global locale regists to elFinder::locale + self::$locale = !empty($opts['locale']) ? $opts['locale'] : (substr(PHP_OS, 0, 3) === 'WIN' ? 'C' : 'en_US.UTF-8'); + if (false === setlocale(LC_ALL, self::$locale)) { + self::$locale = setlocale(LC_ALL, '0'); + } + + // set defaultMimefile + elFinder::$defaultMimefile = isset($opts['defaultMimefile']) ? $opts['defaultMimefile'] : ''; + + // set memoryLimitGD + elFinder::$memoryLimitGD = isset($opts['memoryLimitGD']) ? $opts['memoryLimitGD'] : 0; + + // set flag of throwErrorOnExec + // `true` need `try{}` block for `$connector->run();` + $this->throwErrorOnExec = !empty($opts['throwErrorOnExec']); + + // set archivers + elFinder::$archivers = isset($opts['archivers']) && is_array($opts['archivers']) ? $opts['archivers'] : array(); + + // set utf8Encoder + if (isset($opts['utf8Encoder']) && is_callable($opts['utf8Encoder'])) { + $this->utf8Encoder = $opts['utf8Encoder']; + } + + // bind events listeners + if (!empty($opts['bind']) && is_array($opts['bind'])) { + $_req = $_SERVER["REQUEST_METHOD"] == 'POST' ? $_POST : $_GET; + $_reqCmd = isset($_req['cmd']) ? $_req['cmd'] : ''; + foreach ($opts['bind'] as $cmd => $handlers) { + $doRegist = (strpos($cmd, '*') !== false); + if (!$doRegist) { + $doRegist = ($_reqCmd && in_array($_reqCmd, array_map('self::getCmdOfBind', explode(' ', $cmd)))); + } + if ($doRegist) { + // for backward compatibility + if (!is_array($handlers)) { + $handlers = array($handlers); + } else { + if (count($handlers) === 2 && is_callable($handlers)) { + $handlers = array($handlers); + } + } + foreach ($handlers as $handler) { + if ($handler) { + if (is_string($handler) && strpos($handler, '.')) { + list($_domain, $_name, $_method) = array_pad(explode('.', $handler), 3, ''); + if (strcasecmp($_domain, 'plugin') === 0) { + if ($plugin = $this->getPluginInstance($_name, isset($opts['plugin'][$_name]) ? $opts['plugin'][$_name] : array()) + and method_exists($plugin, $_method)) { + $this->bind($cmd, array($plugin, $_method)); + } + } + } else { + $this->bind($cmd, $handler); + } + } + } + } + } + } + + if (!isset($opts['roots']) || !is_array($opts['roots'])) { + $opts['roots'] = array(); + } + + // try to enable elFinderVolumeFlysystemZipArchiveNetmount to zip editing + if (empty(elFinder::$netDrivers['ziparchive'])) { + elFinder::$netDrivers['ziparchive'] = 'FlysystemZipArchiveNetmount'; + } + + // check for net volumes stored in session + $netVolumes = $this->getNetVolumes(); + foreach ($netVolumes as $key => $root) { + if (!isset($root['id'])) { + // given fixed unique id + if (!$root['id'] = $this->getNetVolumeUniqueId($netVolumes)) { + $this->mountErrors[] = 'Netmount Driver "' . $root['driver'] . '" : Could\'t given volume id.'; + continue; + } + } + $root['_isNetVolume'] = true; + $opts['roots'][$key] = $root; + } + + // "mount" volumes + foreach ($opts['roots'] as $i => $o) { + $class = 'elFinderVolume' . (isset($o['driver']) ? $o['driver'] : ''); + + if (class_exists($class)) { + /* @var elFinderVolumeDriver $volume */ + $volume = new $class(); + + try { + if ($this->maxArcFilesSize && (empty($o['maxArcFilesSize']) || $this->maxArcFilesSize < $o['maxArcFilesSize'])) { + $o['maxArcFilesSize'] = $this->maxArcFilesSize; + } + // pass session handler + $volume->setSession($this->session); + if (!$this->default) { + $volume->setNeedOnline(true); + } + if ($volume->mount($o)) { + // unique volume id (ends on "_") - used as prefix to files hash + $id = $volume->id(); + + $this->volumes[$id] = $volume; + if ((!$this->default || $volume->root() !== $volume->defaultPath()) && $volume->isReadable()) { + $this->default = $volume; + } + } else { + if (!empty($o['_isNetVolume'])) { + $this->removeNetVolume($i, $volume); + } + $this->mountErrors[] = 'Driver "' . $class . '" : ' . implode(' ', $volume->error()); + } + } catch (Exception $e) { + if (!empty($o['_isNetVolume'])) { + $this->removeNetVolume($i, $volume); + } + $this->mountErrors[] = 'Driver "' . $class . '" : ' . $e->getMessage(); + } + } else { + if (!empty($o['_isNetVolume'])) { + $this->removeNetVolume($i, $volume); + } + $this->mountErrors[] = 'Driver "' . $class . '" does not exist'; + } + } + + // if at least one readable volume - ii desu >_< + $this->loaded = !empty($this->default); + + // restore error handler for now + restore_error_handler(); + } + + /** + * Return elFinder session wrapper instance + * + * @return elFinderSessionInterface + **/ + public function getSession() + { + return $this->session; + } + + /** + * Return true if fm init correctly + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + public function loaded() + { + return $this->loaded; + } + + /** + * Return version (api) number + * + * @return string + * @author Dmitry (dio) Levashov + **/ + public function version() + { + return self::$ApiVersion; + } + + /** + * Return revision (api) number + * + * @return string + * @author Naoki Sawada + **/ + public function revision() + { + return self::$ApiRevision; + } + + /** + * Add handler to elFinder command + * + * @param string command name + * @param string|array callback name or array(object, method) + * + * @return elFinder + * @author Dmitry (dio) Levashov + **/ + public function bind($cmd, $handler) + { + $allCmds = array_keys($this->commands); + $cmds = array(); + foreach (explode(' ', $cmd) as $_cmd) { + if ($_cmd !== '') { + if ($all = strpos($_cmd, '*') !== false) { + list(, $sub) = array_pad(explode('.', $_cmd), 2, ''); + if ($sub) { + $sub = str_replace('\'', '\\\'', $sub); + $subs = array_fill(0, count($allCmds), $sub); + $cmds = array_merge($cmds, array_map(array('elFinder', 'addSubToBindName'), $allCmds, $subs)); + } else { + $cmds = array_merge($cmds, $allCmds); + } + } else { + $cmds[] = $_cmd; + } + } + } + $cmds = array_unique($cmds); + + foreach ($cmds as $cmd) { + if (!isset($this->listeners[$cmd])) { + $this->listeners[$cmd] = array(); + } + + if (is_callable($handler)) { + $this->listeners[$cmd][] = $handler; + } + } + + return $this; + } + + /** + * Remove event (command exec) handler + * + * @param string command name + * @param string|array callback name or array(object, method) + * + * @return elFinder + * @author Dmitry (dio) Levashov + **/ + public function unbind($cmd, $handler) + { + if (!empty($this->listeners[$cmd])) { + foreach ($this->listeners[$cmd] as $i => $h) { + if ($h === $handler) { + unset($this->listeners[$cmd][$i]); + return $this; + } + } + } + return $this; + } + + /** + * Trigger binded functions + * + * @param string $cmd binded command name + * @param array $vars variables to pass to listeners + * @param array $errors array into which the error is written + */ + public function trigger($cmd, $vars, &$errors) + { + if (!empty($this->listeners[$cmd])) { + foreach ($this->listeners[$cmd] as $handler) { + $_res = call_user_func_array($handler, $vars); + if ($_res && is_array($_res)) { + $_err = !empty($_res['error'])? $_res['error'] : (!empty($_res['warning'])? $_res['warning'] : null); + if ($_err) { + if (is_array($_err)) { + $errors = array_merge($errors, $_err); + } else { + $errors[] = (string)$_err; + } + if ($_res['error']) { + throw elFinderTriggerException(); + } + } + } + } + } + } + + /** + * Return true if command exists + * + * @param string command name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + public function commandExists($cmd) + { + return $this->loaded && isset($this->commands[$cmd]) && method_exists($this, $cmd); + } + + /** + * Return root - file's owner (public func of volume()) + * + * @param string file hash + * + * @return elFinderVolumeDriver + * @author Naoki Sawada + */ + public function getVolume($hash) + { + return $this->volume($hash); + } + + /** + * Return command required arguments info + * + * @param string command name + * + * @return array + * @author Dmitry (dio) Levashov + **/ + public function commandArgsList($cmd) + { + if ($this->commandExists($cmd)) { + $list = $this->commands[$cmd]; + $list['reqid'] = false; + } else { + $list = array(); + } + return $list; + } + + private function session_expires() + { + + if (!$last = $this->session->get(':LAST_ACTIVITY')) { + $this->session->set(':LAST_ACTIVITY', time()); + return false; + } + + if (($this->timeout > 0) && (time() - $last > $this->timeout)) { + return true; + } + + $this->session->set(':LAST_ACTIVITY', time()); + return false; + } + + /** + * Exec command and return result + * + * @param string $cmd command name + * @param array $args command arguments + * + * @return array + * @throws elFinderAbortException|Exception + * @author Dmitry (dio) Levashov + **/ + public function exec($cmd, $args) + { + // set error handler of WARNING, NOTICE + set_error_handler('elFinder::phpErrorHandler', E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE); + + // set current request args + self::$currentArgs = $args; + + if (!$this->loaded) { + return array('error' => $this->error(self::ERROR_CONF, self::ERROR_CONF_NO_VOL)); + } + + if ($this->session_expires()) { + return array('error' => $this->error(self::ERROR_SESSION_EXPIRES)); + } + + if (!$this->commandExists($cmd)) { + return array('error' => $this->error(self::ERROR_UNKNOWN_CMD)); + } + + // check request id + $args['reqid'] = preg_replace('[^0-9a-fA-F]', '', !empty($args['reqid']) ? $args['reqid'] : (!empty($_SERVER['HTTP_X_ELFINDERREQID']) ? $_SERVER['HTTP_X_ELFINDERREQID'] : '')); + + // to abort this request + if ($cmd === 'abort') { + $this->abort($args); + return array('error' => 0); + } + + // make flag file and set self::$abortCheckFile + if ($args['reqid']) { + $this->abort(array('makeFile' => $args['reqid'])); + } + + if (!empty($args['mimes']) && is_array($args['mimes'])) { + foreach ($this->volumes as $id => $v) { + $this->volumes[$id]->setMimesFilter($args['mimes']); + } + } + + // regist shutdown function as fallback + register_shutdown_function(array($this, 'itemAutoUnlock')); + + // detect destination dirHash and volume + $dstVolume = false; + $dst = !empty($args['target']) ? $args['target'] : (!empty($args['dst']) ? $args['dst'] : ''); + if ($dst) { + $dstVolume = $this->volume($dst); + } else if (isset($args['targets']) && is_array($args['targets']) && isset($args['targets'][0])) { + $dst = $args['targets'][0]; + $dstVolume = $this->volume($dst); + if ($dstVolume && ($_stat = $dstVolume->file($dst)) && !empty($_stat['phash'])) { + $dst = $_stat['phash']; + } else { + $dst = ''; + } + } else if ($cmd === 'open') { + // for initial open without args `target` + $dstVolume = $this->default; + $dst = $dstVolume->defaultPath(); + } + + $result = null; + + // call pre handlers for this command + $args['sessionCloseEarlier'] = isset($this->sessionUseCmds[$cmd]) ? false : $this->sessionCloseEarlier; + if (!empty($this->listeners[$cmd . '.pre'])) { + foreach ($this->listeners[$cmd . '.pre'] as $handler) { + $_res = call_user_func_array($handler, array($cmd, &$args, $this, $dstVolume)); + if (is_array($_res)) { + if (!empty($_res['preventexec'])) { + $result = array('error' => true); + if ($cmd === 'upload' && !empty($args['node'])) { + $result['callback'] = array( + 'node' => $args['node'], + 'bind' => $cmd + ); + } + if (!empty($_res['results']) && is_array($_res['results'])) { + $result = array_merge($result, $_res['results']); + } + break; + } + } + } + } + + // unlock session data for multiple access + if ($this->sessionCloseEarlier && $args['sessionCloseEarlier']) { + $this->session->close(); + // deprecated property + elFinder::$sessionClosed = true; + } + + if (substr(PHP_OS, 0, 3) === 'WIN') { + // set time out + elFinder::extendTimeLimit(300); + } + + if (!is_array($result)) { + try { + $result = $this->$cmd($args); + } catch (elFinderAbortException $e) { + throw $e; + } catch (Exception $e) { + $result = array( + 'error' => htmlspecialchars($e->getMessage()), + 'sync' => true + ); + if ($this->throwErrorOnExec) { + throw $e; + } + } + } + + // check change dstDir + $changeDst = false; + if ($dst && $dstVolume && (!empty($result['added']) || !empty($result['removed']))) { + $changeDst = true; + } + + foreach ($this->volumes as $volume) { + $removed = $volume->removed(); + if (!empty($removed)) { + if (!isset($result['removed'])) { + $result['removed'] = array(); + } + $result['removed'] = array_merge($result['removed'], $removed); + if (!$changeDst && $dst && $dstVolume && $volume === $dstVolume) { + $changeDst = true; + } + } + $added = $volume->added(); + if (!empty($added)) { + if (!isset($result['added'])) { + $result['added'] = array(); + } + $result['added'] = array_merge($result['added'], $added); + if (!$changeDst && $dst && $dstVolume && $volume === $dstVolume) { + $changeDst = true; + } + } + $volume->resetResultStat(); + } + + // dstDir is changed + if ($changeDst) { + if ($dstDir = $dstVolume->dir($dst)) { + if (!isset($result['changed'])) { + $result['changed'] = array(); + } + $result['changed'][] = $dstDir; + } + } + + // call handlers for this command + if (!empty($this->listeners[$cmd])) { + foreach ($this->listeners[$cmd] as $handler) { + if (call_user_func_array($handler, array($cmd, &$result, $args, $this, $dstVolume))) { + // handler return true to force sync client after command completed + $result['sync'] = true; + } + } + } + + // replace removed files info with removed files hashes + if (!empty($result['removed'])) { + $removed = array(); + foreach ($result['removed'] as $file) { + $removed[] = $file['hash']; + } + $result['removed'] = array_unique($removed); + } + // remove hidden files and filter files by mimetypes + if (!empty($result['added'])) { + $result['added'] = $this->filter($result['added']); + } + // remove hidden files and filter files by mimetypes + if (!empty($result['changed'])) { + $result['changed'] = $this->filter($result['changed']); + } + // add toasts + if ($this->toastMessages) { + $result['toasts'] = array_merge(((isset($result['toasts']) && is_array($result['toasts']))? $result['toasts'] : array()), $this->toastMessages); + } + + if ($this->debug || !empty($args['debug'])) { + $result['debug'] = array( + 'connector' => 'php', + 'phpver' => PHP_VERSION, + 'time' => $this->utime() - $this->time, + 'memory' => (function_exists('memory_get_peak_usage') ? ceil(memory_get_peak_usage() / 1024) . 'Kb / ' : '') . ceil(memory_get_usage() / 1024) . 'Kb / ' . ini_get('memory_limit'), + 'upload' => $this->uploadDebug, + 'volumes' => array(), + 'mountErrors' => $this->mountErrors + ); + + foreach ($this->volumes as $id => $volume) { + $result['debug']['volumes'][] = $volume->debug(); + } + } + + // remove sesstion var 'urlContentSaveIds' + if ($this->removeContentSaveIds) { + $urlContentSaveIds = $this->session->get('urlContentSaveIds', array()); + foreach (array_keys($this->removeContentSaveIds) as $contentSaveId) { + if (isset($urlContentSaveIds[$contentSaveId])) { + unset($urlContentSaveIds[$contentSaveId]); + } + } + if ($urlContentSaveIds) { + $this->session->set('urlContentSaveIds', $urlContentSaveIds); + } else { + $this->session->remove('urlContentSaveIds'); + } + } + + foreach ($this->volumes as $volume) { + $volume->saveSessionCache(); + $volume->umount(); + } + + // unlock locked items + $this->itemAutoUnlock(); + + // custom data + if ($this->customData !== null) { + $result['customData'] = $this->customData ? json_encode($this->customData) : ''; + } + + if (!empty($result['debug'])) { + $result['debug']['backendErrors'] = elFinder::$phpErrors; + } + elFinder::$phpErrors = array(); + restore_error_handler(); + + if (!empty($result['callback'])) { + $result['callback']['json'] = json_encode($result); + $this->callback($result['callback']); + return array(); + } else { + return $result; + } + } + + /** + * Return file real path + * + * @param string $hash file hash + * + * @return string + * @author Dmitry (dio) Levashov + **/ + public function realpath($hash) + { + if (($volume = $this->volume($hash)) == false) { + return false; + } + return $volume->realpath($hash); + } + + /** + * Sets custom data(s). + * + * @param string|array $key The key or data array + * @param mixed $val The value + * + * @return self ( elFinder instance ) + */ + public function setCustomData($key, $val = null) + { + if (is_array($key)) { + foreach ($key as $k => $v) { + $this->customData[$k] = $v; + } + } else { + $this->customData[$key] = $val; + } + return $this; + } + + /** + * Removes a custom data. + * + * @param string $key The key + * + * @return self ( elFinder instance ) + */ + public function removeCustomData($key) + { + $this->customData[$key] = null; + return $this; + } + + /** + * Update sesstion value of a NetVolume option + * + * @param string $netKey + * @param string $optionKey + * @param mixed $val + * + * @return bool + */ + public function updateNetVolumeOption($netKey, $optionKey, $val) + { + $netVolumes = $this->getNetVolumes(); + if (is_string($netKey) && isset($netVolumes[$netKey]) && is_string($optionKey)) { + $netVolumes[$netKey][$optionKey] = $val; + $this->saveNetVolumes($netVolumes); + return true; + } + return false; + } + + /** + * remove of session var "urlContentSaveIds" + * + * @param string $id + */ + public function removeUrlContentSaveId($id) + { + $this->removeContentSaveIds[$id] = true; + } + + /** + * Return network volumes config. + * + * @return array + * @author Dmitry (dio) Levashov + */ + protected function getNetVolumes() + { + if ($data = $this->session->get('netvolume', array())) { + return $data; + } + return array(); + } + + /** + * Save network volumes config. + * + * @param array $volumes volumes config + * + * @return void + * @author Dmitry (dio) Levashov + */ + protected function saveNetVolumes($volumes) + { + $this->session->set('netvolume', $volumes); + } + + /** + * Remove netmount volume + * + * @param string $key netvolume key + * @param object $volume volume driver instance + * + * @return bool + */ + protected function removeNetVolume($key, $volume) + { + $netVolumes = $this->getNetVolumes(); + $res = true; + if (is_object($volume) && method_exists($volume, 'netunmount')) { + $res = $volume->netunmount($netVolumes, $key); + $volume->clearSessionCache(); + } + if ($res) { + if (is_string($key) && isset($netVolumes[$key])) { + unset($netVolumes[$key]); + $this->saveNetVolumes($netVolumes); + return true; + } + } + return false; + } + + /** + * Get plugin instance & set to $this->plugins + * + * @param string $name Plugin name (dirctory name) + * @param array $opts Plugin options (optional) + * + * @return object | bool Plugin object instance Or false + * @author Naoki Sawada + */ + protected function getPluginInstance($name, $opts = array()) + { + $key = strtolower($name); + if (!isset($this->plugins[$key])) { + $class = 'elFinderPlugin' . $name; + // to try auto load + if (!class_exists($class)) { + $p_file = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'plugins' . DIRECTORY_SEPARATOR . $name . DIRECTORY_SEPARATOR . 'plugin.php'; + if (is_file($p_file)) { + include_once $p_file; + } + } + if (class_exists($class, false)) { + $this->plugins[$key] = new $class($opts); + } else { + $this->plugins[$key] = false; + } + } + return $this->plugins[$key]; + } + + /***************************************************************************/ + /* commands */ + /***************************************************************************/ + + /** + * Normalize error messages + * + * @return array + * @author Dmitry (dio) Levashov + **/ + public function error() + { + $errors = array(); + + foreach (func_get_args() as $msg) { + if (is_array($msg)) { + $errors = array_merge($errors, $msg); + } else { + $errors[] = $msg; + } + } + + return count($errors) ? $errors : array(self::ERROR_UNKNOWN); + } + + /** + * @param $args + * + * @return array + * @throws elFinderAbortException + */ + protected function netmount($args) + { + $options = array(); + $protocol = $args['protocol']; + $toast = ''; + + if ($protocol === 'netunmount') { + if (!empty($args['user']) && $volume = $this->volume($args['user'])) { + if ($this->removeNetVolume($args['host'], $volume)) { + return array('removed' => array(array('hash' => $volume->root()))); + } + } + return array('sync' => true, 'error' => $this->error(self::ERROR_NETUNMOUNT)); + } + + $driver = isset(self::$netDrivers[$protocol]) ? self::$netDrivers[$protocol] : ''; + $class = 'elFinderVolume' . $driver; + + if (!class_exists($class)) { + return array('error' => $this->error(self::ERROR_NETMOUNT, $args['host'], self::ERROR_NETMOUNT_NO_DRIVER)); + } + + if (!$args['path']) { + $args['path'] = '/'; + } + + foreach ($args as $k => $v) { + if ($k != 'options' && $k != 'protocol' && $v) { + $options[$k] = $v; + } + } + + if (is_array($args['options'])) { + foreach ($args['options'] as $key => $value) { + $options[$key] = $value; + } + } + + /* @var elFinderVolumeDriver $volume */ + $volume = new $class(); + + // pass session handler + $volume->setSession($this->session); + + $volume->setNeedOnline(true); + + if (is_callable(array($volume, 'netmountPrepare'))) { + $options = $volume->netmountPrepare($options); + if (isset($options['exit'])) { + if ($options['exit'] === 'callback') { + $this->callback($options['out']); + } + return $options; + } + if (!empty($options['toast'])) { + $toast = $options['toast']; + unset($options['toast']); + } + } + + $netVolumes = $this->getNetVolumes(); + + if (!isset($options['id'])) { + // given fixed unique id + if (!$options['id'] = $this->getNetVolumeUniqueId($netVolumes)) { + return array('error' => $this->error(self::ERROR_NETMOUNT, $args['host'], 'Could\'t given volume id.')); + } + } + + // load additional volume root options + if (!empty($this->optionsNetVolumes['*'])) { + $options = array_merge($this->optionsNetVolumes['*'], $options); + } + if (!empty($this->optionsNetVolumes[$protocol])) { + $options = array_merge($this->optionsNetVolumes[$protocol], $options); + } + + if (!$key = $volume->netMountKey) { + $key = md5($protocol . '-' . serialize($options)); + } + $options['netkey'] = $key; + + if (!isset($netVolumes[$key]) && $volume->mount($options)) { + // call post-process function of netmount + if (is_callable(array($volume, 'postNetmount'))) { + $volume->postNetmount($options); + } + $options['driver'] = $driver; + $netVolumes[$key] = $options; + $this->saveNetVolumes($netVolumes); + $rootstat = $volume->file($volume->root()); + $res = array('added' => array($rootstat)); + if ($toast) { + $res['toast'] = $toast; + } + return $res; + } else { + $this->removeNetVolume(null, $volume); + return array('error' => $this->error(self::ERROR_NETMOUNT, $args['host'], implode(' ', $volume->error()))); + } + } + + /** + * "Open" directory + * Return array with following elements + * - cwd - opened dir info + * - files - opened dir content [and dirs tree if $args[tree]] + * - api - api version (if $args[init]) + * - uplMaxSize - if $args[init] + * - error - on failed + * + * @param array command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function open($args) + { + $target = $args['target']; + $init = !empty($args['init']); + $tree = !empty($args['tree']); + $volume = $this->volume($target); + $cwd = $volume ? $volume->dir($target) : false; + $hash = $init ? 'default folder' : '#' . $target; + $compare = ''; + + // on init request we can get invalid dir hash - + // dir which can not be opened now, but remembered by client, + // so open default dir + if ((!$cwd || !$cwd['read']) && $init) { + $volume = $this->default; + $target = $volume->defaultPath(); + $cwd = $volume->dir($target); + } + + if (!$cwd) { + return array('error' => $this->error(self::ERROR_OPEN, $hash, self::ERROR_DIR_NOT_FOUND)); + } + if (!$cwd['read']) { + return array('error' => $this->error(self::ERROR_OPEN, $hash, self::ERROR_PERM_DENIED)); + } + + $files = array(); + + // get current working directory files list + if (($ls = $volume->scandir($cwd['hash'])) === false) { + return array('error' => $this->error(self::ERROR_OPEN, $cwd['name'], $volume->error())); + } + + if (isset($cwd['dirs']) && $cwd['dirs'] != 1) { + $cwd = $volume->dir($target); + } + + // get other volume root + if ($tree) { + foreach ($this->volumes as $id => $v) { + $files[] = $v->file($v->root()); + } + } + + // long polling mode + if ($args['compare']) { + $sleep = max(1, (int)$volume->getOption('lsPlSleep')); + $standby = (int)$volume->getOption('plStandby'); + if ($standby > 0 && $sleep > $standby) { + $standby = $sleep; + } + $limit = max(0, floor($standby / $sleep)) + 1; + do { + elFinder::extendTimeLimit(30 + $sleep); + $_mtime = 0; + foreach ($ls as $_f) { + if (isset($_f['ts'])) { + $_mtime = max($_mtime, $_f['ts']); + } + } + $compare = strval(count($ls)) . ':' . strval($_mtime); + if ($compare !== $args['compare']) { + break; + } + if (--$limit) { + sleep($sleep); + $volume->clearstatcache(); + if (($ls = $volume->scandir($cwd['hash'])) === false) { + break; + } + } + } while ($limit); + if ($ls === false) { + return array('error' => $this->error(self::ERROR_OPEN, $cwd['name'], $volume->error())); + } + } + + if ($ls) { + if ($files) { + $files = array_merge($files, $ls); + } else { + $files = $ls; + } + } + + $result = array( + 'cwd' => $cwd, + 'options' => $volume->options($cwd['hash']), + 'files' => $files + ); + + if ($compare) { + $result['cwd']['compare'] = $compare; + } + + if (!empty($args['init'])) { + $result['api'] = sprintf('%.1F%03d', self::$ApiVersion, self::$ApiRevision); + $result['uplMaxSize'] = ini_get('upload_max_filesize'); + $result['uplMaxFile'] = ini_get('max_file_uploads'); + $result['netDrivers'] = array_keys(self::$netDrivers); + $result['maxTargets'] = $this->maxTargets; + if ($volume) { + $result['cwd']['root'] = $volume->root(); + } + if (elfinder::$textMimes) { + $result['textMimes'] = elfinder::$textMimes; + } + } + + return $result; + } + + /** + * Return dir files names list + * + * @param array command arguments + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function ls($args) + { + $target = $args['target']; + $intersect = isset($args['intersect']) ? $args['intersect'] : array(); + + if (($volume = $this->volume($target)) == false + || ($list = $volume->ls($target, $intersect)) === false) { + return array('error' => $this->error(self::ERROR_OPEN, '#' . $target)); + } + return array('list' => $list); + } + + /** + * Return subdirs for required directory + * + * @param array command arguments + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function tree($args) + { + $target = $args['target']; + + if (($volume = $this->volume($target)) == false + || ($tree = $volume->tree($target)) == false) { + return array('error' => $this->error(self::ERROR_OPEN, '#' . $target)); + } + + return array('tree' => $tree); + } + + /** + * Return parents dir for required directory + * + * @param array command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function parents($args) + { + $target = $args['target']; + $until = $args['until']; + + if (($volume = $this->volume($target)) == false + || ($tree = $volume->parents($target, false, $until)) == false) { + return array('error' => $this->error(self::ERROR_OPEN, '#' . $target)); + } + + return array('tree' => $tree); + } + + /** + * Return new created thumbnails list + * + * @param array command arguments + * + * @return array + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function tmb($args) + { + + $result = array('images' => array()); + $targets = $args['targets']; + + foreach ($targets as $target) { + elFinder::checkAborted(); + + if (($volume = $this->volume($target)) != false + && (($tmb = $volume->tmb($target)) != false)) { + $result['images'][$target] = $tmb; + } + } + return $result; + } + + /** + * Download files/folders as an archive file + * 1st: Return srrsy contains download archive file info + * 2nd: Return array contains opened file pointer, root itself and required headers + * + * @param array command arguments + * + * @return array + * @throws Exception + * @author Naoki Sawada + */ + protected function zipdl($args) + { + $targets = $args['targets']; + $download = !empty($args['download']); + $h404 = 'HTTP/1.x 404 Not Found'; + $CriOS = isset($_SERVER['HTTP_USER_AGENT'])? (strpos($_SERVER['HTTP_USER_AGENT'], 'CriOS') !== false) : false; + + if (!$download) { + //1st: Return array contains download archive file info + $error = array(self::ERROR_ARCHIVE); + if (($volume = $this->volume($targets[0])) !== false) { + if ($dlres = $volume->zipdl($targets)) { + $path = $dlres['path']; + register_shutdown_function(array('elFinder', 'rmFileInDisconnected'), $path); + if (count($targets) === 1) { + $name = basename($volume->path($targets[0])); + } else { + $name = $dlres['prefix'] . '_Files'; + } + $name .= '.' . $dlres['ext']; + $uniqid = uniqid(); + $this->session->set('zipdl' . $uniqid, basename($path)); + $result = array( + 'zipdl' => array( + 'file' => $CriOS? basename($path) : $uniqid, + 'name' => $name, + 'mime' => $dlres['mime'] + ) + ); + return $result; + } + $error = array_merge($error, $volume->error()); + } + return array('error' => $error); + } else { + // 2nd: Return array contains opened file session key, root itself and required headers + + // Detect Chrome on iOS + // It has access twice on downloading + $CriOSinit = false; + if ($CriOS) { + $accept = isset($_SERVER['HTTP_ACCEPT'])? $_SERVER['HTTP_ACCEPT'] : ''; + if ($accept && $accept !== '*' && $accept !== '*/*') { + $CriOSinit = true; + } + } + // data check + if (count($targets) !== 4 || ($volume = $this->volume($targets[0])) == false || !($file = $CriOS? $targets[1] : $this->session->get('zipdl' . $targets[1]))) { + return array('error' => 'File not found', 'header' => $h404, 'raw' => true); + } + $path = $volume->getTempPath() . DIRECTORY_SEPARATOR . basename($file); + // remove session data of "zipdl..." + $this->session->remove('zipdl' . $targets[1]); + if (!$CriOSinit) { + // register auto delete on shutdown + $GLOBALS['elFinderTempFiles'][$path] = true; + } + if ($volume->commandDisabled('zipdl')) { + return array('error' => 'File not found', 'header' => $h404, 'raw' => true); + } + if (!is_readable($path) || !is_writable($path)) { + return array('error' => 'File not found', 'header' => $h404, 'raw' => true); + } + // for HTTP headers + $name = $targets[2]; + $mime = $targets[3]; + + $filenameEncoded = rawurlencode($name); + if (strpos($filenameEncoded, '%') === false) { // ASCII only + $filename = 'filename="' . $name . '"'; + } else { + $ua = $_SERVER['HTTP_USER_AGENT']; + if (preg_match('/MSIE [4-8]/', $ua)) { // IE < 9 do not support RFC 6266 (RFC 2231/RFC 5987) + $filename = 'filename="' . $filenameEncoded . '"'; + } elseif (strpos($ua, 'Chrome') === false && strpos($ua, 'Safari') !== false && preg_match('#Version/[3-5]#', $ua)) { // Safari < 6 + $filename = 'filename="' . str_replace('"', '', $name) . '"'; + } else { // RFC 6266 (RFC 2231/RFC 5987) + $filename = 'filename*=UTF-8\'\'' . $filenameEncoded; + } + } + + $fp = fopen($path, 'rb'); + $file = fstat($fp); + $result = array( + 'pointer' => $fp, + 'header' => array( + 'Content-Type: ' . $mime, + 'Content-Disposition: attachment; ' . $filename, + 'Content-Transfer-Encoding: binary', + 'Content-Length: ' . $file['size'], + 'Accept-Ranges: none', + 'Connection: close' + ) + ); + // add cache control headers + if ($cacheHeaders = $volume->getOption('cacheHeaders')) { + $result['header'] = array_merge($result['header'], $cacheHeaders); + } + return $result; + } + } + + /** + * Required to output file in browser when volume URL is not set + * Return array contains opened file pointer, root itself and required headers + * + * @param array command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function file($args) + { + $target = $args['target']; + $download = !empty($args['download']); + $onetime = !empty($args['onetime']); + //$h304 = 'HTTP/1.1 304 Not Modified'; + $h403 = 'HTTP/1.0 403 Access Denied'; + $a403 = array('error' => 'Access Denied', 'header' => $h403, 'raw' => true); + $h404 = 'HTTP/1.0 404 Not Found'; + $a404 = array('error' => 'File not found', 'header' => $h404, 'raw' => true); + + if ($onetime) { + $volume = null; + $tmpdir = elFinder::$commonTempPath; + if (!$tmpdir || !is_file($tmpf = $tmpdir . DIRECTORY_SEPARATOR . 'ELF' . $target)) { + return $a404; + } + $GLOBALS['elFinderTempFiles'][$tmpf] = true; + if ($file = json_decode(file_get_contents($tmpf), true)) { + $src = base64_decode($file['file']); + if (!is_file($src) || !($fp = fopen($src, 'rb'))) { + return $a404; + } + if (strpos($src, $tmpdir) === 0) { + $GLOBALS['elFinderTempFiles'][$src] = true; + } + unset($file['file']); + $file['read'] = true; + $file['size'] = filesize($src); + } else { + return $a404; + } + } else { + if (($volume = $this->volume($target)) == false) { + return $a404; + } + + if ($volume->commandDisabled('file')) { + return $a403; + } + + if (($file = $volume->file($target)) == false) { + return $a404; + } + + if (!$file['read']) { + return $a404; + } + + $opts = array(); + if (!empty($_SERVER['HTTP_RANGE'])) { + $opts['httpheaders'] = array('Range: ' . $_SERVER['HTTP_RANGE']); + } + if (($fp = $volume->open($target, $opts)) == false) { + return $a404; + } + } + + // check aborted by user + elFinder::checkAborted(); + + // allow change MIME type by 'file.pre' callback functions + $mime = isset($args['mime']) ? $args['mime'] : $file['mime']; + if ($download || $onetime) { + $disp = 'attachment'; + } else { + $dispInlineRegex = $volume->getOption('dispInlineRegex'); + $inlineRegex = false; + if ($dispInlineRegex) { + $inlineRegex = '#' . str_replace('#', '\\#', $dispInlineRegex) . '#'; + try { + preg_match($inlineRegex, ''); + } catch (Exception $e) { + $inlineRegex = false; + } + } + if (!$inlineRegex) { + $inlineRegex = '#^(?:(?:image|text)|application/x-shockwave-flash$)#'; + } + $disp = preg_match($inlineRegex, $mime) ? 'inline' : 'attachment'; + } + + $filenameEncoded = rawurlencode($file['name']); + if (strpos($filenameEncoded, '%') === false) { // ASCII only + $filename = 'filename="' . $file['name'] . '"'; + } else { + $ua = isset($_SERVER['HTTP_USER_AGENT'])? $_SERVER['HTTP_USER_AGENT'] : ''; + if (preg_match('/MSIE [4-8]/', $ua)) { // IE < 9 do not support RFC 6266 (RFC 2231/RFC 5987) + $filename = 'filename="' . $filenameEncoded . '"'; + } elseif (strpos($ua, 'Chrome') === false && strpos($ua, 'Safari') !== false && preg_match('#Version/[3-5]#', $ua)) { // Safari < 6 + $filename = 'filename="' . str_replace('"', '', $file['name']) . '"'; + } else { // RFC 6266 (RFC 2231/RFC 5987) + $filename = 'filename*=UTF-8\'\'' . $filenameEncoded; + } + } + + if ($args['cpath'] && $args['reqid']) { + setcookie('elfdl' . $args['reqid'], '1', 0, $args['cpath']); + } + + $result = array( + 'volume' => $volume, + 'pointer' => $fp, + 'info' => $file, + 'header' => array( + 'Content-Type: ' . $mime, + 'Content-Disposition: ' . $disp . '; ' . $filename, + 'Content-Transfer-Encoding: binary', + 'Content-Length: ' . $file['size'], + 'Last-Modified: ' . gmdate('D, d M Y H:i:s T', $file['ts']), + 'Connection: close' + ) + ); + + if (!$onetime) { + // add cache control headers + if ($cacheHeaders = $volume->getOption('cacheHeaders')) { + $result['header'] = array_merge($result['header'], $cacheHeaders); + } + + // check 'xsendfile' + $xsendfile = $volume->getOption('xsendfile'); + $path = null; + if ($xsendfile) { + $info = stream_get_meta_data($fp); + if ($path = empty($info['uri']) ? null : $info['uri']) { + $basePath = rtrim($volume->getOption('xsendfilePath'), DIRECTORY_SEPARATOR); + if ($basePath) { + $root = rtrim($volume->getRootPath(), DIRECTORY_SEPARATOR); + if (strpos($path, $root) === 0) { + $path = $basePath . substr($path, strlen($root)); + } else { + $path = null; + } + } + } + } + if ($path) { + $result['header'][] = $xsendfile . ': ' . $path; + $result['info']['xsendfile'] = $xsendfile; + } + } + + // add "Content-Location" if file has url data + if (isset($file['url']) && $file['url'] && $file['url'] != 1) { + $result['header'][] = 'Content-Location: ' . $file['url']; + } + return $result; + } + + /** + * Count total files size + * + * @param array command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function size($args) + { + $size = 0; + $files = 0; + $dirs = 0; + $itemCount = true; + $sizes = array(); + + foreach ($args['targets'] as $target) { + elFinder::checkAborted(); + if (($volume = $this->volume($target)) == false + || ($file = $volume->file($target)) == false + || !$file['read']) { + return array('error' => $this->error(self::ERROR_OPEN, '#' . $target)); + } + + $volRes = $volume->size($target); + if (is_array($volRes)) { + $sizeInfo = array('size' => 0, 'fileCnt' => 0, 'dirCnt' => 0); + if (!empty($volRes['size'])) { + $sizeInfo['size'] = $volRes['size']; + $size += $volRes['size']; + } + if (!empty($volRes['files'])) { + $sizeInfo['fileCnt'] = $volRes['files']; + } + if (!empty($volRes['dirs'])) { + $sizeInfo['dirCnt'] = $volRes['dirs']; + } + if ($itemCount) { + $files += $sizeInfo['fileCnt']; + $dirs += $sizeInfo['dirCnt']; + } + $sizes[$target] = $sizeInfo; + } else if (is_numeric($volRes)) { + $size += $volRes; + $files = $dirs = 'unknown'; + $itemCount = false; + } + } + return array('size' => $size, 'fileCnt' => $files, 'dirCnt' => $dirs, 'sizes' => $sizes); + } + + /** + * Create directory + * + * @param array command arguments + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function mkdir($args) + { + $target = $args['target']; + $name = $args['name']; + $dirs = $args['dirs']; + if ($name === '' && !$dirs) { + return array('error' => $this->error(self::ERROR_INV_PARAMS, 'mkdir')); + } + + if (($volume = $this->volume($target)) == false) { + return array('error' => $this->error(self::ERROR_MKDIR, $name, self::ERROR_TRGDIR_NOT_FOUND, '#' . $target)); + } + if ($dirs) { + sort($dirs); + $reset = null; + $mkdirs = array(); + foreach ($dirs as $dir) { + $tgt =& $mkdirs; + $_names = explode('/', trim($dir, '/')); + foreach ($_names as $_key => $_name) { + if (!isset($tgt[$_name])) { + $tgt[$_name] = array(); + } + $tgt =& $tgt[$_name]; + } + $tgt =& $reset; + } + $res = $this->ensureDirsRecursively($volume, $target, $mkdirs); + $ret = array( + 'added' => $res['stats'], + 'hashes' => $res['hashes'] + ); + if ($res['error']) { + $ret['warning'] = $this->error(self::ERROR_MKDIR, $res['error'][0], $volume->error()); + } + return $ret; + } else { + return ($dir = $volume->mkdir($target, $name)) == false + ? array('error' => $this->error(self::ERROR_MKDIR, $name, $volume->error())) + : array('added' => array($dir)); + } + } + + /** + * Create empty file + * + * @param array command arguments + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function mkfile($args) + { + $target = $args['target']; + $name = $args['name']; + + if (($volume = $this->volume($target)) == false) { + return array('error' => $this->error(self::ERROR_MKFILE, $name, self::ERROR_TRGDIR_NOT_FOUND, '#' . $target)); + } + + return ($file = $volume->mkfile($target, $args['name'])) == false + ? array('error' => $this->error(self::ERROR_MKFILE, $name, $volume->error())) + : array('added' => array($file)); + } + + /** + * Rename file, Accept multiple items >= API 2.1031 + * + * @param array $args + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function rename($args) + { + $target = $args['target']; + $name = $args['name']; + $query = (!empty($args['q']) && strpos($args['q'], '*') !== false) ? $args['q'] : ''; + $targets = !empty($args['targets'])? $args['targets'] : false; + $rms = array(); + $notfounds = array(); + $locked = array(); + $errs = array(); + $files = array(); + $removed = array(); + $res = array(); + $type = 'normal'; + + if (!($volume = $this->volume($target))) { + return array('error' => $this->error(self::ERROR_RENAME, '#' . $target, self::ERROR_FILE_NOT_FOUND)); + } + + if ($targets) { + array_unshift($targets, $target); + foreach ($targets as $h) { + if ($rm = $volume->file($h)) { + if ($this->itemLocked($h)) { + $locked[] = $rm['name']; + } else { + $rm['realpath'] = $volume->realpath($h); + $rms[] = $rm; + } + } else { + $notfounds[] = '#' . $h; + } + } + if (!$rms) { + $res['error'] = array(); + if ($notfounds) { + $res['error'] = array(self::ERROR_RENAME, join(', ', $notfounds), self::ERROR_FILE_NOT_FOUND); + } + if ($locked) { + array_push($res['error'], self::ERROR_LOCKED, join(', ', $locked)); + } + return $res; + } + + $res['warning'] = array(); + if ($notfounds) { + array_push($res['warning'], self::ERROR_RENAME, join(', ', $notfounds), self::ERROR_FILE_NOT_FOUND); + } + if ($locked) { + array_push($res['warning'], self::ERROR_LOCKED, join(', ', $locked)); + } + + if ($query) { + // batch rename + $splits = elFinder::splitFileExtention($query); + if ($splits[1] && $splits[0] === '*') { + $type = 'extention'; + $name = $splits[1]; + } else if (strlen($splits[0]) > 1) { + if (substr($splits[0], -1) === '*') { + $type = 'prefix'; + $name = substr($splits[0], 0, strlen($splits[0]) - 1); + } else if (substr($splits[0], 0, 1) === '*') { + $type = 'suffix'; + $name = substr($splits[0], 1); + } + } + if ($type !== 'normal') { + if (!empty($this->listeners['rename.pre'])) { + $_args = array('name' => $name); + foreach ($this->listeners['rename.pre'] as $handler) { + $_res = call_user_func_array($handler, array('rename', &$_args, $this, $volume)); + if (!empty($_res['preventexec'])) { + break; + } + } + $name = $_args['name']; + } + } + } + foreach ($rms as $rm) { + if ($type === 'normal') { + $rname = $volume->uniqueName($volume->realpath($rm['phash']), $name, '', false); + } else { + $rname = $name; + if ($type === 'extention') { + $splits = elFinder::splitFileExtention($rm['name']); + $rname = $splits[0] . '.' . $name; + } else if ($type === 'prefix') { + $rname = $name . $rm['name']; + } else if ($type === 'suffix') { + $splits = elFinder::splitFileExtention($rm['name']); + $rname = $splits[0] . $name . ($splits[1] ? ('.' . $splits[1]) : ''); + } + $rname = $volume->uniqueName($volume->realpath($rm['phash']), $rname, '', true); + } + if ($file = $volume->rename($rm['hash'], $rname)) { + $files[] = $file; + $removed[] = $rm; + } else { + $errs[] = $rm['name']; + } + } + + if (!$files) { + $res['error'] = $this->error(self::ERROR_RENAME, join(', ', $errs), $volume->error()); + if (!$res['warning']) { + unset($res['warning']); + } + return $res; + } + if ($errs) { + array_push($res['warning'], self::ERROR_RENAME, join(', ', $errs), $volume->error()); + } + if (!$res['warning']) { + unset($res['warning']); + } + $res['added'] = $files; + $res['removed'] = $removed; + return $res; + } else { + if (!($rm = $volume->file($target))) { + return array('error' => $this->error(self::ERROR_RENAME, '#' . $target, self::ERROR_FILE_NOT_FOUND)); + } + if ($this->itemLocked($target)) { + return array('error' => $this->error(self::ERROR_LOCKED, $rm['name'])); + } + $rm['realpath'] = $volume->realpath($target); + + $file = $volume->rename($target, $name); + if ($file === false) { + return array('error' => $this->error(self::ERROR_RENAME, $rm['name'], $volume->error())); + } else { + if ($file['hash'] !== $rm['hash']) { + return array('added' => array($file), 'removed' => array($rm)); + } else { + return array('changed' => array($file)); + } + } + } + } + + /** + * Duplicate file - create copy with "copy %d" suffix + * + * @param array $args command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function duplicate($args) + { + $targets = is_array($args['targets']) ? $args['targets'] : array(); + $result = array(); + $suffix = empty($args['suffix']) ? 'copy' : $args['suffix']; + + $this->itemLock($targets); + + foreach ($targets as $target) { + elFinder::checkAborted(); + + if (($volume = $this->volume($target)) == false + || ($src = $volume->file($target)) == false) { + $result['warning'] = $this->error(self::ERROR_COPY, '#' . $target, self::ERROR_FILE_NOT_FOUND); + break; + } + + if (($file = $volume->duplicate($target, $suffix)) == false) { + $result['warning'] = $this->error($volume->error()); + break; + } + } + + return $result; + } + + /** + * Remove dirs/files + * + * @param array command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function rm($args) + { + $targets = is_array($args['targets']) ? $args['targets'] : array(); + $result = array('removed' => array()); + + foreach ($targets as $target) { + elFinder::checkAborted(); + + if (($volume = $this->volume($target)) == false) { + $result['warning'] = $this->error(self::ERROR_RM, '#' . $target, self::ERROR_FILE_NOT_FOUND); + break; + } + + if ($this->itemLocked($target)) { + $rm = $volume->file($target); + $result['warning'] = $this->error(self::ERROR_LOCKED, $rm['name']); + break; + } + + if (!$volume->rm($target)) { + $result['warning'] = $this->error($volume->error()); + break; + } + } + + return $result; + } + + /** + * Return has subdirs + * + * @param array command arguments + * + * @return array + * @author Dmitry Naoki Sawada + **/ + protected function subdirs($args) + { + + $result = array('subdirs' => array()); + $targets = $args['targets']; + + foreach ($targets as $target) { + if (($volume = $this->volume($target)) !== false) { + $result['subdirs'][$target] = $volume->subdirs($target) ? 1 : 0; + } + } + return $result; + } + + /** + * Gateway for custom contents editor + * + * @param array $args command arguments + * + * @return array + * @author Naoki Sawada + */ + protected function editor($args = array()) + { + /* @var elFinderEditor $editor */ + $name = $args['name']; + if (is_array($name)) { + $res = array(); + foreach ($name as $c) { + $class = 'elFinderEditor' . $c; + if (class_exists($class)) { + $editor = new $class($this, $args['args']); + $res[$c] = $editor->enabled(); + } else { + $res[$c] = 0; + } + } + return $res; + } else { + $class = 'elFinderEditor' . $name; + $method = ''; + if (class_exists($class)) { + $editor = new $class($this, $args['args']); + $method = $args['method']; + if ($editor->isAllowedMethod($method) && method_exists($editor, $method)) { + return $editor->$method(); + } + } + return array('error', $this->error(self::ERROR_UNKNOWN_CMD, 'editor.' . $name . '.' . $method)); + } + } + + /** + * Abort current request and make flag file to running check + * + * @param array $args + * + * @return void + */ + protected function abort($args = array()) + { + if (!elFinder::$connectionFlagsPath || $_SERVER['REQUEST_METHOD'] === 'HEAD') { + return; + } + $flagFile = elFinder::$connectionFlagsPath . DIRECTORY_SEPARATOR . 'elfreq%s'; + if (!empty($args['makeFile'])) { + self::$abortCheckFile = sprintf($flagFile, $args['makeFile']); + touch(self::$abortCheckFile); + $GLOBALS['elFinderTempFiles'][self::$abortCheckFile] = true; + return; + } + + $file = !empty($args['id']) ? sprintf($flagFile, $args['id']) : self::$abortCheckFile; + $file && is_file($file) && unlink($file); + } + + /** + * Get remote contents + * + * @param string $url target url + * @param int $timeout timeout (sec) + * @param int $redirect_max redirect max count + * @param string $ua + * @param resource $fp + * + * @return string, resource or bool(false) + * @retval string contents + * @retval resource conttents + * @rettval false error + * @author Naoki Sawada + **/ + protected function get_remote_contents(&$url, $timeout = 30, $redirect_max = 5, $ua = 'Mozilla/5.0', $fp = null) + { + if (preg_match('~^(?:ht|f)tps?://[-_.!\~*\'()a-z0-9;/?:\@&=+\$,%#\*\[\]]+~i', $url)) { + $info = parse_url($url); + $host = trim(strtolower($info['host']), '.'); + // do not support IPv6 address + if (preg_match('/^\[.*\]$/', $host)) { + return false; + } + // do not support non dot host + if (strpos($host, '.') === false) { + return false; + } + // do not support URL-encoded host + if (strpos($host, '%') !== false) { + return false; + } + // disallow including "localhost" and "localdomain" + if (preg_match('/\b(?:localhost|localdomain)\b/', $host)) { + return false; + } + // wildcard DNS (e.g xip.io) + if (preg_match('/0x[0-9a-f]+|[0-9]+(?:\.(?:0x[0-9a-f]+|[0-9]+)){1,3}/', $host)) { + $host = gethostbyname($host); + } + // check IPv4 local loopback, private network and link local + if (preg_match('/^0x[0-9a-f]+|[0-9]+(?:\.(?:0x[0-9a-f]+|[0-9]+)){1,3}$/', $host, $m)) { + $long = (int)sprintf('%u', ip2long($host)); + if (!$long) { + return false; + } + $local = (int)sprintf('%u', ip2long('127.255.255.255')) >> 24; + $prv1 = (int)sprintf('%u', ip2long('10.255.255.255')) >> 24; + $prv2 = (int)sprintf('%u', ip2long('172.31.255.255')) >> 20; + $prv3 = (int)sprintf('%u', ip2long('192.168.255.255')) >> 16; + $link = (int)sprintf('%u', ip2long('169.254.255.255')) >> 16; + + if ($long >> 24 === $local || $long >> 24 === $prv1 || $long >> 20 === $prv2 || $long >> 16 === $prv3 || $long >> 16 === $link) { + return false; + } + } + // dose not support 'user' and 'pass' for security reasons + $url = $info['scheme'].'://'.$host.(!empty($info['port'])? (':'.$info['port']) : '').$info['path'].(!empty($info['query'])? ('?'.$info['query']) : '').(!empty($info['fragment'])? ('#'.$info['fragment']) : ''); + // check by URL upload filter + if ($this->urlUploadFilter && is_callable($this->urlUploadFilter)) { + if (!call_user_func_array($this->urlUploadFilter, array($url, $this))) { + return false; + } + } + $method = (function_exists('curl_exec') && !ini_get('safe_mode') && !ini_get('open_basedir')) ? 'curl_get_contents' : 'fsock_get_contents'; + return $this->$method($url, $timeout, $redirect_max, $ua, $fp); + } + return false; + } + + /** + * Get remote contents with cURL + * + * @param string $url target url + * @param int $timeout timeout (sec) + * @param int $redirect_max redirect max count + * @param string $ua + * @param resource $outfp + * + * @return string, resource or bool(false) + * @retval string contents + * @retval resource conttents + * @retval false error + * @author Naoki Sawada + **/ + protected function curl_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, false); + if ($outfp) { + curl_setopt($ch, CURLOPT_FILE, $outfp); + } else { + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); + } + curl_setopt($ch, CURLOPT_LOW_SPEED_LIMIT, 1); + curl_setopt($ch, CURLOPT_LOW_SPEED_TIME, $timeout); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_MAXREDIRS, $redirect_max); + curl_setopt($ch, CURLOPT_USERAGENT, $ua); + $result = curl_exec($ch); + $url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); + curl_close($ch); + return $outfp ? $outfp : $result; + } + + /** + * Get remote contents with fsockopen() + * + * @param string $url url + * @param int $timeout timeout (sec) + * @param int $redirect_max redirect max count + * @param string $ua + * @param resource $outfp + * + * @return string, resource or bool(false) + * @retval string contents + * @retval resource conttents + * @retval false error + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function fsock_get_contents(&$url, $timeout, $redirect_max, $ua, $outfp) + { + $connect_timeout = 3; + $connect_try = 3; + $method = 'GET'; + $readsize = 4096; + $ssl = ''; + + $getSize = null; + $headers = ''; + + $arr = parse_url($url); + if (!$arr) { + // Bad request + return false; + } + if ($arr['scheme'] === 'https') { + $ssl = 'ssl://'; + } + + // query + $arr['query'] = isset($arr['query']) ? '?' . $arr['query'] : ''; + // port + $port = isset($arr['port']) ? $arr['port'] : ''; + $arr['port'] = $port ? $port : ($ssl ? 443 : 80); + + $url_base = $arr['scheme'] . '://' . $arr['host'] . ($port ? (':' . $port) : ''); + $url_path = isset($arr['path']) ? $arr['path'] : '/'; + $uri = $url_path . $arr['query']; + + $query = $method . ' ' . $uri . " HTTP/1.0\r\n"; + $query .= "Host: " . $arr['host'] . "\r\n"; + $query .= "Accept: */*\r\n"; + $query .= "Connection: close\r\n"; + if (!empty($ua)) $query .= "User-Agent: " . $ua . "\r\n"; + if (!is_null($getSize)) $query .= 'Range: bytes=0-' . ($getSize - 1) . "\r\n"; + + $query .= $headers; + + $query .= "\r\n"; + + $fp = $connect_try_count = 0; + while (!$fp && $connect_try_count < $connect_try) { + + $errno = 0; + $errstr = ""; + $fp = fsockopen( + $ssl . $arr['host'], + $arr['port'], + $errno, $errstr, $connect_timeout); + if ($fp) break; + $connect_try_count++; + if (connection_aborted()) { + throw new elFinderAbortException(); + } + sleep(1); // wait 1sec + } + + if (!$fp) { + return false; + } + + $fwrite = 0; + for ($written = 0; $written < strlen($query); $written += $fwrite) { + $fwrite = fwrite($fp, substr($query, $written)); + if (!$fwrite) { + break; + } + } + + if ($timeout) { + socket_set_timeout($fp, $timeout); + } + + $_response = ''; + $header = ''; + while ($_response !== "\r\n") { + $_response = fgets($fp, $readsize); + $header .= $_response; + }; + + $rccd = array_pad(explode(' ', $header, 2), 2, ''); // array('HTTP/1.1','200') + $rc = (int)$rccd[1]; + + $ret = false; + // Redirect + switch ($rc) { + case 307: // Temporary Redirect + case 303: // See Other + case 302: // Moved Temporarily + case 301: // Moved Permanently + $matches = array(); + if (preg_match('/^Location: (.+?)(#.+)?$/im', $header, $matches) && --$redirect_max > 0) { + $_url = $url; + $url = trim($matches[1]); + if (!preg_match('/^https?:\//', $url)) { // no scheme + if ($url[0] != '/') { // Relative path + // to Absolute path + $url = substr($url_path, 0, strrpos($url_path, '/')) . '/' . $url; + } + // add sheme,host + $url = $url_base . $url; + } + if ($_url === $url) { + sleep(1); + } + fclose($fp); + return $this->fsock_get_contents($url, $timeout, $redirect_max, $ua, $outfp); + } + break; + case 200: + $ret = true; + } + if (!$ret) { + fclose($fp); + return false; + } + + $body = ''; + if (!$outfp) { + $outfp = fopen('php://temp', 'rwb'); + $body = true; + } + while (fwrite($outfp, fread($fp, $readsize))) { + if ($timeout) { + $_status = socket_get_status($fp); + if ($_status['timed_out']) { + fclose($outfp); + fclose($fp); + return false; // Request Time-out + } + } + } + if ($body) { + rewind($outfp); + $body = stream_get_contents($outfp); + fclose($outfp); + $outfp = null; + } + + fclose($fp); + + return $outfp ? $outfp : $body; // Data + } + + /** + * Parse Data URI scheme + * + * @param string $str + * @param array $extTable + * @param array $args + * + * @return array + * @author Naoki Sawada + */ + protected function parse_data_scheme($str, $extTable, $args = null) + { + $data = $name = $mime = ''; + // Scheme 'data://' require `allow_url_fopen` and `allow_url_include` + if ($fp = fopen('data://' . substr($str, 5), 'rb')) { + if ($data = stream_get_contents($fp)) { + $meta = stream_get_meta_data($fp); + $mime = $meta['mediatype']; + } + fclose($fp); + } else if (preg_match('~^data:(.+?/.+?)?(?:;charset=.+?)?;base64,~', substr($str, 0, 128), $m)) { + $data = base64_decode(substr($str, strlen($m[0]))); + if ($m[1]) { + $mime = $m[1]; + } + } + if ($data) { + $ext = ($mime && isset($extTable[$mime])) ? '.' . $extTable[$mime] : ''; + // Set name if name eq 'image.png' and $args has 'name' array, e.g. clipboard data + if (is_array($args['name']) && isset($args['name'][0])) { + $name = $args['name'][0]; + if ($ext) { + $name = preg_replace('/\.[^.]*$/', '', $name); + } + } else { + $name = substr(md5($data), 0, 8); + } + $name .= $ext; + } else { + $data = $name = ''; + } + return array($data, $name); + } + + /** + * Detect file MIME Type by local path + * + * @param string $path Local path + * + * @return string file MIME Type + * @author Naoki Sawada + */ + protected function detectMimeType($path) + { + static $type, $finfo; + if (!$type) { + if (class_exists('finfo', false)) { + $tmpFileInfo = explode(';', finfo_file(finfo_open(FILEINFO_MIME), __FILE__)); + } else { + $tmpFileInfo = false; + } + $regexp = '/text\/x\-(php|c\+\+)/'; + if ($tmpFileInfo && preg_match($regexp, array_shift($tmpFileInfo))) { + $type = 'finfo'; + $finfo = finfo_open(FILEINFO_MIME); + } elseif (function_exists('mime_content_type') + && preg_match($regexp, array_shift(explode(';', mime_content_type(__FILE__))))) { + $type = 'mime_content_type'; + } elseif (function_exists('getimagesize')) { + $type = 'getimagesize'; + } else { + $type = 'none'; + } + } + + $mime = ''; + if ($type === 'finfo') { + $mime = finfo_file($finfo, $path); + } elseif ($type === 'mime_content_type') { + $mime = mime_content_type($path); + } elseif ($type === 'getimagesize') { + if ($img = getimagesize($path)) { + $mime = $img['mime']; + } + } + + if ($mime) { + $mime = explode(';', $mime); + $mime = trim($mime[0]); + + if (in_array($mime, array('application/x-empty', 'inode/x-empty'))) { + // finfo return this mime for empty files + $mime = 'text/plain'; + } elseif ($mime == 'application/x-zip') { + // http://elrte.org/redmine/issues/163 + $mime = 'application/zip'; + } + } + + return $mime ? $mime : 'unknown'; + } + + /** + * Detect file type extension by local path + * + * @param object $volume elFinderVolumeDriver instance + * @param string $path Local path + * @param string $name Filename to save + * + * @return string file type extension with dot + * @author Naoki Sawada + */ + protected function detectFileExtension($volume, $path, $name) + { + $mime = $this->detectMimeType($path); + if ($mime === 'unknown') { + $mime = 'application/octet-stream'; + } + $ext = $volume->getExtentionByMime($volume->mimeTypeNormalize($mime, $name)); + return $ext ? ('.' . $ext) : ''; + } + + /** + * Get temporary directory path + * + * @param string $volumeTempPath + * + * @return string + * @author Naoki Sawada + */ + private function getTempDir($volumeTempPath = null) + { + $testDirs = array(); + if ($this->uploadTempPath) { + $testDirs[] = rtrim(realpath($this->uploadTempPath), DIRECTORY_SEPARATOR); + } + if ($volumeTempPath) { + $testDirs[] = rtrim(realpath($volumeTempPath), DIRECTORY_SEPARATOR); + } + if (elFinder::$commonTempPath) { + $testDirs[] = elFinder::$commonTempPath; + } + $tempDir = ''; + foreach ($testDirs as $testDir) { + if (!$testDir || !is_dir($testDir)) continue; + if (is_writable($testDir)) { + $tempDir = $testDir; + $gc = time() - 3600; + foreach (glob($tempDir . DIRECTORY_SEPARATOR . 'ELF*') as $cf) { + if (filemtime($cf) < $gc) { + unlink($cf); + } + } + break; + } + } + return $tempDir; + } + + /** + * chmod + * + * @param array command arguments + * + * @return array + * @throws elFinderAbortException + * @author David Bartle + */ + protected function chmod($args) + { + $targets = $args['targets']; + $mode = intval((string)$args['mode'], 8); + + if (!is_array($targets)) { + $targets = array($targets); + } + + $result = array(); + + if (($volume = $this->volume($targets[0])) == false) { + $result['error'] = $this->error(self::ERROR_CONF_NO_VOL); + return $result; + } + + $this->itemLock($targets); + + $files = array(); + $errors = array(); + foreach ($targets as $target) { + elFinder::checkAborted(); + + $file = $volume->chmod($target, $mode); + if ($file) { + $files = array_merge($files, is_array($file) ? $file : array($file)); + } else { + $errors = array_merge($errors, $volume->error()); + } + } + + if ($files) { + $result['changed'] = $files; + if ($errors) { + $result['warning'] = $this->error($errors); + } + } else { + $result['error'] = $this->error($errors); + } + + return $result; + } + + /** + * Check chunked upload files + * + * @param string $tmpname uploaded temporary file path + * @param string $chunk uploaded chunk file name + * @param string $cid uploaded chunked file id + * @param string $tempDir temporary dirctroy path + * @param null $volume + * + * @return array|null + * @throws elFinderAbortException + * @author Naoki Sawada + */ + private function checkChunkedFile($tmpname, $chunk, $cid, $tempDir, $volume = null) + { + /* @var elFinderVolumeDriver $volume */ + if (preg_match('/^(.+)(\.\d+_(\d+))\.part$/s', $chunk, $m)) { + $fname = $m[1]; + $encname = md5($cid . '_' . $fname); + $base = $tempDir . DIRECTORY_SEPARATOR . 'ELF' . $encname; + $clast = intval($m[3]); + if (is_null($tmpname)) { + ignore_user_abort(true); + // chunked file upload fail + foreach (glob($base . '*') as $cf) { + unlink($cf); + } + ignore_user_abort(false); + return null; + } + + $range = isset($_POST['range']) ? trim($_POST['range']) : ''; + if ($range && preg_match('/^(\d+),(\d+),(\d+)$/', $range, $ranges)) { + $start = $ranges[1]; + $len = $ranges[2]; + $size = $ranges[3]; + $tmp = $base . '.part'; + $csize = filesize($tmpname); + + $tmpExists = is_file($tmp); + if (!$tmpExists) { + // check upload max size + $uploadMaxSize = $volume ? $volume->getUploadMaxSize() : 0; + if ($uploadMaxSize > 0 && $size > $uploadMaxSize) { + return array(self::ERROR_UPLOAD_FILE_SIZE, false); + } + // make temp file + $ok = false; + if ($fp = fopen($tmp, 'wb')) { + flock($fp, LOCK_EX); + $ok = ftruncate($fp, $size); + flock($fp, LOCK_UN); + fclose($fp); + touch($base); + } + if (!$ok) { + unlink($tmp); + return array(self::ERROR_UPLOAD_TEMP, false); + } + } else { + // wait until makeing temp file (for anothor session) + $cnt = 1200; // Time limit 120 sec + while (!is_file($base) && --$cnt) { + usleep(100000); // wait 100ms + } + if (!$cnt) { + return array(self::ERROR_UPLOAD_TEMP, false); + } + } + + // check size info + if ($len != $csize || $start + $len > $size || ($tmpExists && $size != filesize($tmp))) { + return array(self::ERROR_UPLOAD_TEMP, false); + } + + // write chunk data + $src = fopen($tmpname, 'rb'); + $fp = fopen($tmp, 'cb'); + fseek($fp, $start); + $writelen = stream_copy_to_stream($src, $fp, $len); + fclose($fp); + fclose($src); + + try { + // to check connection is aborted + elFinder::checkAborted(); + } catch (elFinderAbortException $e) { + unlink($tmpname); + is_file($tmp) && unlink($tmp); + is_file($base) && unlink($base); + throw $e; + } + + if ($writelen != $len) { + return array(self::ERROR_UPLOAD_TEMP, false); + } + + // write counts + file_put_contents($base, "\0", FILE_APPEND | LOCK_EX); + + if (filesize($base) >= $clast + 1) { + // Completion + unlink($base); + return array($tmp, $fname); + } + } else { + // old way + $part = $base . $m[2]; + if (move_uploaded_file($tmpname, $part)) { + chmod($part, 0600); + if ($clast < count(glob($base . '*'))) { + $parts = array(); + for ($i = 0; $i <= $clast; $i++) { + $name = $base . '.' . $i . '_' . $clast; + if (is_readable($name)) { + $parts[] = $name; + } else { + $parts = null; + break; + } + } + if ($parts) { + if (!is_file($base)) { + touch($base); + if ($resfile = tempnam($tempDir, 'ELF')) { + $target = fopen($resfile, 'wb'); + foreach ($parts as $f) { + $fp = fopen($f, 'rb'); + while (!feof($fp)) { + fwrite($target, fread($fp, 8192)); + } + fclose($fp); + unlink($f); + } + fclose($target); + unlink($base); + return array($resfile, $fname); + } + unlink($base); + } + } + } + } + } + } + return array('', ''); + } + + /** + * Save uploaded files + * + * @param array + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function upload($args) + { + $ngReg = '/[\/\\?*:|"<>]/'; + $target = $args['target']; + $volume = $this->volume($target); + $files = isset($args['FILES']['upload']) && is_array($args['FILES']['upload']) ? $args['FILES']['upload'] : array(); + $header = empty($args['html']) ? array() : array('header' => 'Content-Type: text/html; charset=utf-8'); + $result = array_merge(array('added' => array()), $header); + $paths = $args['upload_path'] ? $args['upload_path'] : array(); + $chunk = $args['chunk'] ? $args['chunk'] : ''; + $cid = $args['cid'] ? (int)$args['cid'] : ''; + $mtimes = $args['mtime'] ? $args['mtime'] : array(); + $tmpfname = ''; + + if (!$volume) { + return array_merge(array('error' => $this->error(self::ERROR_UPLOAD, self::ERROR_TRGDIR_NOT_FOUND, '#' . $target)), $header); + } + + // check $chunk + if (strpos($chunk, '/') !== false || strpos($chunk, '\\') !== false) { + return array('error' => $this->error(self::ERROR_UPLOAD)); + } + + if ($args['overwrite'] !== '') { + $volume->setUploadOverwrite($args['overwrite']); + } + + $renames = $hashes = array(); + $suffix = '~'; + if ($args['renames'] && is_array($args['renames'])) { + $renames = array_flip($args['renames']); + if (is_string($args['suffix']) && !preg_match($ngReg, $args['suffix'])) { + $suffix = $args['suffix']; + } + } + if ($args['hashes'] && is_array($args['hashes'])) { + $hashes = array_flip($args['hashes']); + } + + $this->itemLock($target); + + // file extentions table by MIME + $extTable = array_flip(array_unique($volume->getMimeTable())); + + if (empty($files)) { + if (isset($args['upload']) && is_array($args['upload']) && ($tempDir = $this->getTempDir($volume->getTempPath()))) { + $names = array(); + foreach ($args['upload'] as $i => $url) { + // check chunked file upload commit + if ($chunk) { + if ($url === 'chunkfail' && $args['mimes'] === 'chunkfail') { + $this->checkChunkedFile(null, $chunk, $cid, $tempDir); + if (preg_match('/^(.+)(\.\d+_(\d+))\.part$/s', $chunk, $m)) { + $result['warning'] = $this->error(self::ERROR_UPLOAD_FILE, $m[1], self::ERROR_UPLOAD_TEMP); + } + return $result; + } else { + $tmpfname = $tempDir . '/' . $chunk; + $files['tmp_name'][$i] = $tmpfname; + $files['name'][$i] = $url; + $files['error'][$i] = 0; + $GLOBALS['elFinderTempFiles'][$tmpfname] = true; + break; + } + } + + $tmpfname = $tempDir . DIRECTORY_SEPARATOR . 'ELF_FATCH_' . md5($url . microtime(true)); + $GLOBALS['elFinderTempFiles'][$tmpfname] = true; + + $_name = ''; + // check is data: + if (substr($url, 0, 5) === 'data:') { + list($data, $args['name'][$i]) = $this->parse_data_scheme($url, $extTable, $args); + } else { + $fp = fopen($tmpfname, 'wb'); + if ($data = $this->get_remote_contents($url, 30, 5, 'Mozilla/5.0', $fp)) { + // to check connection is aborted + try { + elFinder::checkAborted(); + } catch(elFinderAbortException $e) { + fclose($fp); + throw $e; + } + $_name = preg_replace('~^.*?([^/#?]+)(?:\?.*)?(?:#.*)?$~', '$1', rawurldecode($url)); + // Check `Content-Disposition` response header + if (($headers = get_headers($url, true)) && !empty($headers['Content-Disposition'])) { + if (preg_match('/filename\*=(?:([a-zA-Z0-9_-]+?)\'\')"?([a-z0-9_.~%-]+)"?/i', $headers['Content-Disposition'], $m)) { + $_name = rawurldecode($m[2]); + if ($m[1] && strtoupper($m[1]) !== 'UTF-8' && function_exists('mb_convert_encoding')) { + $_name = mb_convert_encoding($_name, 'UTF-8', $m[1]); + } + } else if (preg_match('/filename="?([ a-z0-9_.~%-]+)"?/i', $headers['Content-Disposition'], $m)) { + $_name = rawurldecode($m[1]); + } + } + } else { + fclose($fp); + } + } + if ($data) { + if (isset($args['name'][$i])) { + $_name = $args['name'][$i]; + } + if ($_name) { + $_ext = ''; + if (preg_match('/(\.[a-z0-9]{1,7})$/', $_name, $_match)) { + $_ext = $_match[1]; + } + if ((is_resource($data) && fclose($data)) || file_put_contents($tmpfname, $data)) { + $GLOBALS['elFinderTempFiles'][$tmpfname] = true; + $_name = preg_replace($ngReg, '_', $_name); + list($_a, $_b) = array_pad(explode('.', $_name, 2), 2, ''); + if ($_b === '') { + if ($_ext) { + rename($tmpfname, $tmpfname . $_ext); + $tmpfname = $tmpfname . $_ext; + } + $_b = $this->detectFileExtension($volume, $tmpfname, $_name); + $_name = $_a . $_b; + } else { + $_b = '.' . $_b; + } + if (isset($names[$_name])) { + $_name = $_a . '_' . $names[$_name]++ . $_b; + } else { + $names[$_name] = 1; + } + $files['tmp_name'][$i] = $tmpfname; + $files['name'][$i] = $_name; + $files['error'][$i] = 0; + // set to auto rename + $volume->setUploadOverwrite(false); + } else { + unlink($tmpfname); + } + } + } + } + } + if (empty($files)) { + return array_merge(array('error' => $this->error(self::ERROR_UPLOAD, self::ERROR_UPLOAD_NO_FILES)), $header); + } + } + + $addedDirs = array(); + $errors = array(); + foreach ($files['name'] as $i => $name) { + if (($error = $files['error'][$i]) > 0) { + $result['warning'] = $this->error(self::ERROR_UPLOAD_FILE, $name, $error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE ? self::ERROR_UPLOAD_FILE_SIZE : self::ERROR_UPLOAD_TRANSFER, $error); + $this->uploadDebug = 'Upload error code: ' . $error; + break; + } + + $tmpname = $files['tmp_name'][$i]; + $thash = ($paths && isset($paths[$i])) ? $paths[$i] : $target; + $mtime = isset($mtimes[$i]) ? $mtimes[$i] : 0; + if ($name === 'blob') { + if ($chunk) { + if ($tempDir = $this->getTempDir($volume->getTempPath())) { + list($tmpname, $name) = $this->checkChunkedFile($tmpname, $chunk, $cid, $tempDir, $volume); + if ($tmpname) { + if ($name === false) { + preg_match('/^(.+)(\.\d+_(\d+))\.part$/s', $chunk, $m); + $result['error'] = $this->error(self::ERROR_UPLOAD_FILE, $m[1], $tmpname); + $result['_chunkfailure'] = true; + $this->uploadDebug = 'Upload error: ' . $tmpname; + } else if ($name) { + $result['_chunkmerged'] = basename($tmpname); + $result['_name'] = $name; + $result['_mtime'] = $mtime; + } + } + } else { + $result['error'] = $this->error(self::ERROR_UPLOAD_FILE, $chunk, self::ERROR_UPLOAD_TEMP); + $this->uploadDebug = 'Upload error: unable open tmp file'; + } + return $result; + } else { + // for form clipboard with Google Chrome or Opera + $name = 'image.png'; + } + } + + // Set name if name eq 'image.png' and $args has 'name' array, e.g. clipboard data + if (strtolower(substr($name, 0, 5)) === 'image' && is_array($args['name']) && isset($args['name'][$i])) { + $type = $files['type'][$i]; + $name = $args['name'][$i]; + $ext = isset($extTable[$type]) ? '.' . $extTable[$type] : ''; + if ($ext) { + $name = preg_replace('/\.[^.]*$/', '', $name); + } + $name .= $ext; + } + + // do hook function 'upload.presave' + try { + $this->trigger('upload.presave', array(&$thash, &$name, $tmpname, $this, $volume), $errors); + } catch (elFinderTriggerException $e) { + if (!is_uploaded_file($tmpname) && unlink($tmpname) && $tmpfname) { + unset($GLOBALS['elFinderTempFiles'][$tmpfname]); + } + continue; + } + + clearstatcache(); + if ($mtime && is_file($tmpname)) { + // for keep timestamp option in the LocalFileSystem volume + touch($tmpname, $mtime); + } + + $fp = null; + if (!is_file($tmpname) || ($fp = fopen($tmpname, 'rb')) === false) { + $errors = array_merge($errors, array(self::ERROR_UPLOAD_FILE, $name, ($fp === false? self::ERROR_UPLOAD_TEMP : self::ERROR_UPLOAD_TRANSFER))); + $this->uploadDebug = 'Upload error: unable open tmp file'; + if (!is_uploaded_file($tmpname)) { + if (unlink($tmpname) && $tmpfname) unset($GLOBALS['elFinderTempFiles'][$tmpfname]); + continue; + } + break; + } + $rnres = array(); + if ($thash !== '' && $thash !== $target) { + if ($dir = $volume->dir($thash)) { + $_target = $thash; + if (!isset($addedDirs[$thash])) { + $addedDirs[$thash] = true; + $result['added'][] = $dir; + // to support multi-level directory creation + $_phash = isset($dir['phash']) ? $dir['phash'] : null; + while ($_phash && !isset($addedDirs[$_phash]) && $_phash !== $target) { + if ($_dir = $volume->dir($_phash)) { + $addedDirs[$_phash] = true; + $result['added'][] = $_dir; + $_phash = isset($_dir['phash']) ? $_dir['phash'] : null; + } else { + break; + } + } + } + } else { + $result['error'] = $this->error(self::ERROR_UPLOAD, self::ERROR_TRGDIR_NOT_FOUND, 'hash@' . $thash); + break; + } + } else { + $_target = $target; + // file rename for backup + if (isset($renames[$name])) { + $dir = $volume->realpath($_target); + if (isset($hashes[$name])) { + $hash = $hashes[$name]; + } else { + $hash = $volume->getHash($dir, $name); + } + $rnres = $this->rename(array('target' => $hash, 'name' => $volume->uniqueName($dir, $name, $suffix, true, 0))); + if (!empty($rnres['error'])) { + $result['warning'] = $rnres['error']; + if (!is_array($rnres['error'])) { + $errors = array_push($errors, $rnres['error']); + } else { + $errors = array_merge($errors, $rnres['error']); + } + continue; + } + } + } + if (!$_target || ($file = $volume->upload($fp, $_target, $name, $tmpname, ($_target === $target) ? $hashes : array())) === false) { + $errors = array_merge($errors, $this->error(self::ERROR_UPLOAD_FILE, $name, $volume->error())); + fclose($fp); + if (!is_uploaded_file($tmpname) && unlink($tmpname)) { + unset($GLOBALS['elFinderTempFiles'][$tmpname]); + } + continue; + } + + is_resource($fp) && fclose($fp); + if (!is_uploaded_file($tmpname)) { + clearstatcache(); + if (!is_file($tmpname) || unlink($tmpname)) { + unset($GLOBALS['elFinderTempFiles'][$tmpname]); + } + } + $result['added'][] = $file; + if ($rnres) { + $result = array_merge_recursive($result, $rnres); + } + } + + if ($errors) { + $result['warning'] = $errors; + } + + if ($GLOBALS['elFinderTempFiles']) { + foreach (array_keys($GLOBALS['elFinderTempFiles']) as $_temp) { + is_file($_temp) && is_writable($_temp) && unlink($_temp); + } + } + $result['removed'] = $volume->removed(); + + if (!empty($args['node'])) { + $result['callback'] = array( + 'node' => $args['node'], + 'bind' => 'upload' + ); + } + return $result; + } + + /** + * Copy/move files into new destination + * + * @param array command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function paste($args) + { + $dst = $args['dst']; + $targets = is_array($args['targets']) ? $args['targets'] : array(); + $cut = !empty($args['cut']); + $error = $cut ? self::ERROR_MOVE : self::ERROR_COPY; + $result = array('changed' => array(), 'added' => array(), 'removed' => array(), 'warning' => array()); + + if (($dstVolume = $this->volume($dst)) == false) { + return array('error' => $this->error($error, '#' . $targets[0], self::ERROR_TRGDIR_NOT_FOUND, '#' . $dst)); + } + + $this->itemLock($dst); + + $hashes = $renames = array(); + $suffix = '~'; + if (!empty($args['renames'])) { + $renames = array_flip($args['renames']); + if (is_string($args['suffix']) && !preg_match('/[\/\\?*:|"<>]/', $args['suffix'])) { + $suffix = $args['suffix']; + } + } + if (!empty($args['hashes'])) { + $hashes = array_flip($args['hashes']); + } + + foreach ($targets as $target) { + elFinder::checkAborted(); + + if (($srcVolume = $this->volume($target)) == false) { + $result['warning'] = array_merge($result['warning'], $this->error($error, '#' . $target, self::ERROR_FILE_NOT_FOUND)); + continue; + } + + $rnres = array(); + if ($renames) { + $file = $srcVolume->file($target); + if (isset($renames[$file['name']])) { + $dir = $dstVolume->realpath($dst); + $dstName = $file['name']; + if ($srcVolume !== $dstVolume) { + $errors = array(); + try { + $this->trigger('paste.copyfrom', array(&$dst, &$dstName, '', $this, $dstVolume), $errors); + } catch (elFinderTriggerException $e) { + $result['warning'] = array_merge($result['warning'], $errors); + continue; + } + } + if (isset($hashes[$file['name']])) { + $hash = $hashes[$file['name']]; + } else { + $hash = $dstVolume->getHash($dir, $dstName); + } + $rnres = $this->rename(array('target' => $hash, 'name' => $dstVolume->uniqueName($dir, $dstName, $suffix, true, 0))); + if (!empty($rnres['error'])) { + $result['warning'] = array_merge($result['warning'], $rnres['error']); + continue; + } + } + } + + if ($cut && $this->itemLocked($target)) { + $rm = $srcVolume->file($target); + $result['warning'] = array_merge($result['warning'], $this->error(self::ERROR_LOCKED, $rm['name'])); + continue; + } + + if (($file = $dstVolume->paste($srcVolume, $target, $dst, $cut, $hashes)) == false) { + $result['warning'] = array_merge($result['warning'], $this->error($dstVolume->error())); + continue; + } + + if ($error = $dstVolume->error()) { + $result['warning'] = array_merge($result['warning'], $this->error($error)); + } + + if ($rnres) { + $result = array_merge_recursive($result, $rnres); + } + } + if (count($result['warning']) < 1) { + unset($result['warning']); + } else { + $result['sync'] = true; + } + + return $result; + } + + /** + * Return file content + * + * @param array $args command arguments + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function get($args) + { + $target = $args['target']; + $volume = $this->volume($target); + $enc = false; + + if (!$volume || ($file = $volume->file($target)) == false) { + return array('error' => $this->error(self::ERROR_OPEN, '#' . $target, self::ERROR_FILE_NOT_FOUND)); + } + + if ($volume->commandDisabled('get')) { + return array('error' => $this->error(self::ERROR_OPEN, '#' . $target, self::ERROR_ACCESS_DENIED)); + } + + if (($content = $volume->getContents($target)) === false) { + return array('error' => $this->error(self::ERROR_OPEN, $volume->path($target), $volume->error())); + } + + $mime = isset($file['mime']) ? $file['mime'] : ''; + if ($mime && (strtolower(substr($mime, 0, 4)) === 'text' || in_array(strtolower($mime), self::$textMimes))) { + $enc = ''; + if ($content !== '') { + if (!$args['conv'] || $args['conv'] == '1') { + // detect encoding + if (function_exists('mb_detect_encoding')) { + if ($enc = mb_detect_encoding($content, mb_detect_order(), true)) { + $encu = strtoupper($enc); + if ($encu === 'UTF-8' || $encu === 'ASCII') { + $enc = ''; + } + } else { + $enc = 'unknown'; + } + } else if (!preg_match('//u', $content)) { + $enc = 'unknown'; + } + if ($enc === 'unknown') { + $enc = $volume->getOption('encoding'); + if (!$enc || strtoupper($enc) === 'UTF-8') { + $enc = 'unknown'; + } + } + // call callbacks 'get.detectencoding' + if (!empty($this->listeners['get.detectencoding'])) { + foreach ($this->listeners['get.detectencoding'] as $handler) { + call_user_func_array($handler, array('get', &$enc, array_merge($args, array('content' => $content)), $this, $volume)); + } + } + if ($enc && $enc !== 'unknown') { + $errlev = error_reporting(); + error_reporting($errlev ^ E_NOTICE); + $utf8 = iconv($enc, 'UTF-8', $content); + if ($utf8 === false && function_exists('mb_convert_encoding')) { + error_reporting($errlev ^ E_WARNING); + $utf8 = mb_convert_encoding($content, 'UTF-8', $enc); + if (mb_convert_encoding($utf8, $enc, 'UTF-8') !== $content) { + $enc = 'unknown'; + } + } else { + if ($utf8 === false || iconv('UTF-8', $enc, $utf8) !== $content) { + $enc = 'unknown'; + } + } + error_reporting($errlev); + if ($enc !== 'unknown') { + $content = $utf8; + } + } + if ($enc) { + if ($args['conv'] == '1') { + $args['conv'] = ''; + if ($enc === 'unknown') { + $content = false; + } + } else if ($enc === 'unknown') { + return array('doconv' => $enc); + } + } + if ($args['conv'] == '1') { + $args['conv'] = ''; + } + } + if ($args['conv']) { + $enc = $args['conv']; + if (strtoupper($enc) !== 'UTF-8') { + $_content = $content; + $errlev = error_reporting(); + $this->setToastErrorHandler(array( + 'prefix' => 'Notice: ' + )); + error_reporting($errlev | E_NOTICE | E_WARNING); + $content = iconv($enc, 'UTF-8//TRANSLIT', $content); + if ($content === false && function_exists('mb_convert_encoding')) { + $content = mb_convert_encoding($_content, 'UTF-8', $enc); + } + error_reporting($errlev); + $this->setToastErrorHandler(false); + } else { + $enc = ''; + } + } + } + } else { + $content = 'data:' . ($mime ? $mime : 'application/octet-stream') . ';base64,' . base64_encode($content); + } + + if ($enc !== false) { + $json = false; + if ($content !== false) { + $json = json_encode($content); + } + if ($content === false || $json === false || strlen($json) < strlen($content)) { + return array('doconv' => 'unknown'); + } + } + + $res = array( + 'header' => array( + 'Content-Type: application/json' + ), + 'content' => $content + ); + + // add cache control headers + if ($cacheHeaders = $volume->getOption('cacheHeaders')) { + $res['header'] = array_merge($res['header'], $cacheHeaders); + } + + if ($enc) { + $res['encoding'] = $enc; + } + return $res; + } + + /** + * Save content into text file + * + * @param $args + * + * @return array + * @author Dmitry (dio) Levashov + */ + protected function put($args) + { + $target = $args['target']; + $encoding = isset($args['encoding']) ? $args['encoding'] : ''; + + if (($volume = $this->volume($target)) == false + || ($file = $volume->file($target)) == false) { + return array('error' => $this->error(self::ERROR_SAVE, '#' . $target, self::ERROR_FILE_NOT_FOUND)); + } + + $this->itemLock($target); + + if ($encoding === 'scheme') { + if (preg_match('~^https?://~i', $args['content'])) { + /** @var resource $fp */ + $fp = $this->get_remote_contents($args['content'], 30, 5, 'Mozilla/5.0', $volume->tmpfile()); + if (!$fp) { + return array('error' => self::ERROR_SAVE, $args['content'], self::ERROR_FILE_NOT_FOUND); + } + $fmeta = stream_get_meta_data($fp); + $mime = $this->detectMimeType($fmeta['uri']); + if ($mime === 'unknown') { + $mime = 'application/octet-stream'; + } + $mime = $volume->mimeTypeNormalize($mime, $file['name']); + $args['content'] = 'data:' . $mime . ';base64,' . base64_encode(file_get_contents($fmeta['uri'])); + } + $encoding = ''; + $args['content'] = "\0" . $args['content']; + } else if ($encoding === 'hash') { + $_hash = $args['content']; + if ($_src = $this->getVolume($_hash)) { + if ($_file = $_src->file($_hash)) { + if ($_data = $_src->getContents($_hash)) { + $args['content'] = 'data:' . $file['mime'] . ';base64,' . base64_encode($_data); + } + } + } + $encoding = ''; + $args['content'] = "\0" . $args['content']; + } + if ($encoding) { + $content = iconv('UTF-8', $encoding, $args['content']); + if ($content === false && function_exists('mb_detect_encoding')) { + $content = mb_convert_encoding($args['content'], $encoding, 'UTF-8'); + } + if ($content !== false) { + $args['content'] = $content; + } + } + if (($file = $volume->putContents($target, $args['content'])) == false) { + return array('error' => $this->error(self::ERROR_SAVE, $volume->path($target), $volume->error())); + } + + return array('changed' => array($file)); + } + + /** + * Extract files from archive + * + * @param array $args command arguments + * + * @return array + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function extract($args) + { + $target = $args['target']; + $makedir = isset($args['makedir']) ? (bool)$args['makedir'] : null; + + if (($volume = $this->volume($target)) == false + || ($file = $volume->file($target)) == false) { + return array('error' => $this->error(self::ERROR_EXTRACT, '#' . $target, self::ERROR_FILE_NOT_FOUND)); + } + + $res = array(); + if ($file = $volume->extract($target, $makedir)) { + $res['added'] = isset($file['read']) ? array($file) : $file; + if ($err = $volume->error()) { + $res['warning'] = $err; + } + } else { + $res['error'] = $this->error(self::ERROR_EXTRACT, $volume->path($target), $volume->error()); + } + return $res; + } + + /** + * Create archive + * + * @param array $args command arguments + * + * @return array + * @throws Exception + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + */ + protected function archive($args) + { + $targets = isset($args['targets']) && is_array($args['targets']) ? $args['targets'] : array(); + $name = isset($args['name']) ? $args['name'] : ''; + + if (($volume = $this->volume($targets[0])) == false) { + return $this->error(self::ERROR_ARCHIVE, self::ERROR_TRGDIR_NOT_FOUND); + } + + foreach ($targets as $target) { + $this->itemLock($target); + } + + return ($file = $volume->archive($targets, $args['type'], $name)) + ? array('added' => array($file)) + : array('error' => $this->error(self::ERROR_ARCHIVE, $volume->error())); + } + + /** + * Search files + * + * @param array $args command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry Levashov + */ + protected function search($args) + { + $q = trim($args['q']); + $mimes = !empty($args['mimes']) && is_array($args['mimes']) ? $args['mimes'] : array(); + $target = !empty($args['target']) ? $args['target'] : null; + $type = !empty($args['type']) ? $args['type'] : null; + $result = array(); + $errors = array(); + + if ($target) { + if ($volume = $this->volume($target)) { + $result = $volume->search($q, $mimes, $target, $type); + $errors = array_merge($errors, $volume->error()); + } + } else { + foreach ($this->volumes as $volume) { + $result = array_merge($result, $volume->search($q, $mimes, null, $type)); + $errors = array_merge($errors, $volume->error()); + } + } + + $result = array('files' => $result); + if ($errors) { + $result['warning'] = $errors; + } + return $result; + } + + /** + * Return file info (used by client "places" ui) + * + * @param array $args command arguments + * + * @return array + * @throws elFinderAbortException + * @author Dmitry Levashov + */ + protected function info($args) + { + $files = array(); + $compare = null; + // long polling mode + if ($args['compare'] && count($args['targets']) === 1) { + $compare = intval($args['compare']); + $hash = $args['targets'][0]; + if ($volume = $this->volume($hash)) { + $standby = (int)$volume->getOption('plStandby'); + $_compare = false; + if (($syncCheckFunc = $volume->getOption('syncCheckFunc')) && is_callable($syncCheckFunc)) { + $_compare = call_user_func_array($syncCheckFunc, array($volume->realpath($hash), $standby, $compare, $volume, $this)); + } + if ($_compare !== false) { + $compare = $_compare; + } else { + $sleep = max(1, (int)$volume->getOption('tsPlSleep')); + $limit = max(1, $standby / $sleep) + 1; + do { + elFinder::extendTimeLimit(30 + $sleep); + $volume->clearstatcache(); + if (($info = $volume->file($hash)) != false) { + if ($info['ts'] != $compare) { + $compare = $info['ts']; + break; + } + } else { + $compare = 0; + break; + } + if (--$limit) { + sleep($sleep); + } + } while ($limit); + } + } + } else { + foreach ($args['targets'] as $hash) { + elFinder::checkAborted(); + if (($volume = $this->volume($hash)) != false + && ($info = $volume->file($hash)) != false) { + $info['path'] = $volume->path($hash); + $files[] = $info; + } + } + } + + $result = array('files' => $files); + if (!is_null($compare)) { + $result['compare'] = strval($compare); + } + return $result; + } + + /** + * Return image dimensions + * + * @param array $args command arguments + * + * @return array + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function dim($args) + { + $res = array(); + $target = $args['target']; + + if (($volume = $this->volume($target)) != false) { + if ($dim = $volume->dimensions($target, $args)) { + if (is_array($dim) && isset($dim['dim'])) { + $res = $dim; + } else { + $res = array('dim' => $dim); + if ($subImgLink = $volume->getSubstituteImgLink($target, explode('x', $dim))) { + $res['url'] = $subImgLink; + } + } + } + } + + return $res; + } + + /** + * Resize image + * + * @param array command arguments + * + * @return array + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + */ + protected function resize($args) + { + $target = $args['target']; + $width = (int)$args['width']; + $height = (int)$args['height']; + $x = (int)$args['x']; + $y = (int)$args['y']; + $mode = $args['mode']; + $bg = $args['bg']; + $degree = (int)$args['degree']; + $quality = (int)$args['quality']; + + if (($volume = $this->volume($target)) == false + || ($file = $volume->file($target)) == false) { + return array('error' => $this->error(self::ERROR_RESIZE, '#' . $target, self::ERROR_FILE_NOT_FOUND)); + } + + if ($mode !== 'rotate' && ($width < 1 || $height < 1)) { + return array('error' => $this->error(self::ERROR_RESIZESIZE)); + } + return ($file = $volume->resize($target, $width, $height, $x, $y, $mode, $bg, $degree, $quality)) + ? (!empty($file['losslessRotate']) ? $file : array('changed' => array($file))) + : array('error' => $this->error(self::ERROR_RESIZE, $volume->path($target), $volume->error())); + } + + /** + * Return content URL + * + * @param array $args command arguments + * + * @return array + * @author Naoki Sawada + **/ + protected function url($args) + { + $target = $args['target']; + $options = isset($args['options']) ? $args['options'] : array(); + if (($volume = $this->volume($target)) != false) { + if (!$volume->commandDisabled('url')) { + $url = $volume->getContentUrl($target, $options); + return $url ? array('url' => $url) : array(); + } + } + return array(); + } + + /** + * Output callback result with JavaScript that control elFinder + * or HTTP redirect to callbackWindowURL + * + * @param array command arguments + * + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function callback($args) + { + $checkReg = '/[^a-zA-Z0-9;._-]/'; + $node = (isset($args['node']) && !preg_match($checkReg, $args['node'])) ? $args['node'] : ''; + $json = (isset($args['json']) && json_decode($args['json'])) ? $args['json'] : '{}'; + $bind = (isset($args['bind']) && !preg_match($checkReg, $args['bind'])) ? $args['bind'] : ''; + $done = (!empty($args['done'])); + + while (ob_get_level()) { + if (!ob_end_clean()) { + break; + } + } + + if ($done || !$this->callbackWindowURL) { + $script = ''; + if ($node) { + if ($bind) { + $trigger = 'elf.trigger(\'' . $bind . '\', data);'; + $triggerdone = 'elf.trigger(\'' . $bind . 'done\');'; + $triggerfail = 'elf.trigger(\'' . $bind . 'fail\', data);'; + } else { + $trigger = $triggerdone = $triggerfail = ''; + } + $origin = isset($_SERVER['HTTP_ORIGIN'])? str_replace('\'', '\\\'', $_SERVER['HTTP_ORIGIN']) : '*'; + $script .= ' +var go = function() { + var w = window.opener || window.parent || window, + close = function(){ + window.open("about:blank","_self").close(); + return false; + }; + try { + var elf = w.document.getElementById(\'' . $node . '\').elfinder; + if (elf) { + var data = ' . $json . '; + if (data.error) { + ' . $triggerfail . ' + elf.error(data.error); + } else { + data.warning && elf.error(data.warning); + data.removed && data.removed.length && elf.remove(data); + data.added && data.added.length && elf.add(data); + data.changed && data.changed.length && elf.change(data); + ' . $trigger . ' + ' . $triggerdone . ' + data.sync && elf.sync(); + } + } + } catch(e) { + // for CORS + w.postMessage && w.postMessage(JSON.stringify({bind:\'' . $bind . '\',data:' . $json . '}), \'' . $origin . '\'); + } + close(); + setTimeout(function() { + var msg = document.getElementById(\'msg\'); + msg.style.display = \'inline\'; + msg.onclick = close; + }, 100); +}; +'; + } + + $out = '

    '; + + header('Content-Type: text/html; charset=utf-8'); + header('Content-Length: ' . strlen($out)); + header('Cache-Control: private'); + header('Pragma: no-cache'); + + echo $out; + + } else { + $url = $this->callbackWindowURL; + $url .= ((strpos($url, '?') === false) ? '?' : '&') + . '&node=' . rawurlencode($node) + . (($json !== '{}') ? ('&json=' . rawurlencode($json)) : '') + . ($bind ? ('&bind=' . rawurlencode($bind)) : '') + . '&done=1'; + + header('Location: ' . $url); + + } + throw new elFinderAbortException(); + } + + /** + * Error handler for send toast message to client side + * + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + * + * @return boolean + */ + protected function toastErrorHandler($errno, $errstr, $errfile, $errline) + { + $proc = false; + if (!(error_reporting() & $errno)) { + return $proc; + } + $toast = array(); + $toast['mode'] = $this->toastParams['mode']; + $toast['msg'] = $this->toastParams['prefix'] . $errstr; + $this->toastMessages[] = $toast; + return true; + } + + /** + * PHP error handler, catch error types only E_WARNING | E_NOTICE | E_USER_WARNING | E_USER_NOTICE + * + * @param int $errno + * @param string $errstr + * @param string $errfile + * @param int $errline + * + * @return boolean + */ + public static function phpErrorHandler($errno, $errstr, $errfile, $errline) + { + static $base = null; + + $proc = false; + + if (is_null($base)) { + $base = dirname(__FILE__) . DIRECTORY_SEPARATOR; + } + + if (!(error_reporting() & $errno)) { + return $proc; + } + + $errfile = str_replace($base, '', $errfile); + + switch ($errno) { + case E_WARNING: + case E_USER_WARNING: + elFinder::$phpErrors[] = "WARNING: $errstr in $errfile line $errline."; + $proc = true; + break; + + case E_NOTICE: + case E_USER_NOTICE: + elFinder::$phpErrors[] = "NOTICE: $errstr in $errfile line $errline."; + $proc = true; + break; + + case E_STRICT: + elFinder::$phpErrors[] = "STRICT: $errstr in $errfile line $errline."; + $proc = true; + break; + + case E_RECOVERABLE_ERROR: + elFinder::$phpErrors[] = "RECOVERABLE_ERROR: $errstr in $errfile line $errline."; + $proc = true; + break; + } + + if (defined('E_DEPRECATED')) { + switch ($errno) { + case E_DEPRECATED: + case E_USER_DEPRECATED: + elFinder::$phpErrors[] = "DEPRECATED: $errstr in $errfile line $errline."; + $proc = true; + break; + } + } + + return $proc; + } + + /***************************************************************************/ + /* utils */ + /***************************************************************************/ + + /** + * Return root - file's owner + * + * @param string file hash + * + * @return elFinderVolumeDriver|boolean (false) + * @author Dmitry (dio) Levashov + **/ + protected function volume($hash) + { + foreach ($this->volumes as $id => $v) { + if (strpos('' . $hash, $id) === 0) { + return $this->volumes[$id]; + } + } + return false; + } + + /** + * Return files info array + * + * @param array $data one file info or files info + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function toArray($data) + { + return isset($data['hash']) || !is_array($data) ? array($data) : $data; + } + + /** + * Return fils hashes list + * + * @param array $files files info + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function hashes($files) + { + $ret = array(); + foreach ($files as $file) { + $ret[] = $file['hash']; + } + return $ret; + } + + /** + * Remove from files list hidden files and files with required mime types + * + * @param array $files files info + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function filter($files) + { + $exists = array(); + foreach ($files as $i => $file) { + if (isset($file['hash'])) { + if (isset($exists[$file['hash']]) || !empty($file['hidden']) || !$this->default->mimeAccepted($file['mime'])) { + unset($files[$i]); + } + $exists[$file['hash']] = true; + } + } + return array_values($files); + } + + protected function utime() + { + $time = explode(" ", microtime()); + return (double)$time[1] + (double)$time[0]; + } + + /** + * Return Network mount volume unique ID + * + * @param array $netVolumes Saved netvolumes array + * @param string $prefix Id prefix + * + * @return string|false + * @author Naoki Sawada + **/ + protected function getNetVolumeUniqueId($netVolumes = null, $prefix = 'nm') + { + if (is_null($netVolumes)) { + $netVolumes = $this->getNetVolumes(); + } + $ids = array(); + foreach ($netVolumes as $vOps) { + if (isset($vOps['id']) && strpos($vOps['id'], $prefix) === 0) { + $ids[$vOps['id']] = true; + } + } + if (!$ids) { + $id = $prefix . '1'; + } else { + $i = 0; + while (isset($ids[$prefix . ++$i]) && $i < 10000) ; + $id = $prefix . $i; + if (isset($ids[$id])) { + $id = false; + } + } + return $id; + } + + /** + * Is item locked? + * + * @param string $hash + * + * @return boolean + */ + protected function itemLocked($hash) + { + if (!elFinder::$commonTempPath) { + return false; + } + $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . $hash . '.lock'; + if (file_exists($lock)) { + if (filemtime($lock) + $this->itemLockExpire < time()) { + unlink($lock); + return false; + } + return true; + } + + return false; + } + + /** + * Do lock target item + * + * @param array|string $hashes + * @param boolean $autoUnlock + * + * @return void + */ + protected function itemLock($hashes, $autoUnlock = true) + { + if (!elFinder::$commonTempPath) { + return; + } + if (!is_array($hashes)) { + $hashes = array($hashes); + } + foreach ($hashes as $hash) { + $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . $hash . '.lock'; + if ($this->itemLocked($hash)) { + $cnt = file_get_contents($lock) + 1; + } else { + $cnt = 1; + } + if (file_put_contents($lock, $cnt, LOCK_EX)) { + if ($autoUnlock) { + $this->autoUnlocks[] = $hash; + } + } + } + } + + /** + * Do unlock target item + * + * @param string $hash + * + * @return boolean + */ + protected function itemUnlock($hash) + { + if (!$this->itemLocked($hash)) { + return true; + } + $lock = elFinder::$commonTempPath . DIRECTORY_SEPARATOR . $hash . '.lock'; + $cnt = file_get_contents($lock); + if (--$cnt < 1) { + unlink($lock); + return true; + } else { + file_put_contents($lock, $cnt, LOCK_EX); + return false; + } + } + + /** + * unlock locked items on command completion + * + * @return void + */ + public function itemAutoUnlock() + { + if ($this->autoUnlocks) { + foreach ($this->autoUnlocks as $hash) { + $this->itemUnlock($hash); + } + $this->autoUnlocks = array(); + } + } + + /** + * Ensure directories recursively + * + * @param object $volume Volume object + * @param string $target Target hash + * @param array $dirs Array of directory tree to ensure + * @param string $path Relative path form target hash + * + * @return array|false array('stats' => array([stat of maked directory]), 'hashes' => array('[path]' => '[hash]'), 'makes' => array([New directory hashes]), 'error' => array([Error name])) + * @author Naoki Sawada + **/ + protected function ensureDirsRecursively($volume, $target, $dirs, $path = '') + { + $res = array('stats' => array(), 'hashes' => array(), 'makes' => array(), 'error' => array()); + foreach ($dirs as $name => $sub) { + $name = (string)$name; + $dir = $newDir = null; + if ((($parent = $volume->realpath($target)) && ($dir = $volume->dir($volume->getHash($parent, $name)))) || ($newDir = $volume->mkdir($target, $name))) { + $_path = $path . '/' . $name; + if ($newDir) { + $res['makes'][] = $newDir['hash']; + $dir = $newDir; + } + $res['stats'][] = $dir; + $res['hashes'][$_path] = $dir['hash']; + if (count($sub)) { + $res = array_merge_recursive($res, $this->ensureDirsRecursively($volume, $dir['hash'], $sub, $_path)); + } + } else { + $res['error'][] = $name; + } + } + return $res; + } + + /** + * Sets the toast error handler. + * + * @param array $opts The options + */ + public function setToastErrorHandler($opts) + { + $this->toastParams = $this->toastParamsDefault; + if (!$opts) { + restore_error_handler(); + } else { + $this->toastParams = array_merge($this->toastParams, $opts); + set_error_handler(array($this, 'toastErrorHandler')); + } + } + + /** + * String encode convert to UTF-8 + * + * @param string $str Input string + * + * @return string UTF-8 string + */ + public function utf8Encode($str) + { + static $mbencode = null; + $str = (string) $str; + if (@iconv('utf-8', 'utf-8//IGNORE', $str) === $str) { + return $str; + } + + if ($this->utf8Encoder) { + return $this->utf8Encoder($str); + } + + if ($mbencode === null) { + $mbencode = function_exists('mb_convert_encoding') && function_exists('mb_detect_encoding'); + } + + if ($mbencode) { + if ($enc = mb_detect_encoding($str, mb_detect_order(), true)) { + $_str = mb_convert_encoding($str, 'UTF-8', $enc); + if (@iconv('utf-8', 'utf-8//IGNORE', $_str) === $_str) { + return $_str; + } + } + } + return utf8_encode($str); + } + + /***************************************************************************/ + /* static utils */ + /***************************************************************************/ + + /** + * Return full version of API that this connector supports all functions + * + * @return string + */ + public static function getApiFullVersion() + { + return (string)self::$ApiVersion . '.' . (string)self::$ApiRevision; + } + + /** + * Return Is Animation Gif + * + * @param string $path server local path of target image + * + * @return bool + */ + public static function isAnimationGif($path) + { + list(, , $type) = getimagesize($path); + switch ($type) { + case IMAGETYPE_GIF: + break; + default: + return false; + } + + $imgcnt = 0; + $fp = fopen($path, 'rb'); + fread($fp, 4); + $c = fread($fp, 1); + if (ord($c) != 0x39) { // GIF89a + return false; + } + + while (!feof($fp)) { + do { + $c = fread($fp, 1); + } while (ord($c) != 0x21 && !feof($fp)); + + if (feof($fp)) { + break; + } + + $c2 = fread($fp, 2); + if (bin2hex($c2) == "f904") { + $imgcnt++; + if ($imgcnt === 2) { + break; + } + } + + if (feof($fp)) { + break; + } + } + + if ($imgcnt > 1) { + return true; + } else { + return false; + } + } + + /** + * Return Is Animation Png + * + * @param string $path server local path of target image + * + * @return bool + */ + public static function isAnimationPng($path) + { + list(, , $type) = getimagesize($path); + switch ($type) { + case IMAGETYPE_PNG: + break; + default: + return false; + } + + $fp = fopen($path, 'rb'); + $img_bytes = fread($fp, 1024); + fclose($fp); + if ($img_bytes) { + if (strpos(substr($img_bytes, 0, strpos($img_bytes, 'IDAT')), 'acTL') !== false) { + return true; + } + } + return false; + } + + /** + * Return Is seekable stream resource + * + * @param resource $resource + * + * @return bool + */ + public static function isSeekableStream($resource) + { + $metadata = stream_get_meta_data($resource); + return $metadata['seekable']; + } + + /** + * Rewind stream resource + * + * @param resource $resource + * + * @return void + */ + public static function rewind($resource) + { + self::isSeekableStream($resource) && rewind($resource); + } + + /** + * Determines whether the specified resource is seekable url. + * + * @param $resource The resource + * + * @return boolean True if the specified resource is seekable url, False otherwise. + */ + public static function isSeekableUrl($resource) + { + $id = (int)$resource; + if (isset(elFinder::$seekableUrlFps[$id])) { + return elFinder::$seekableUrlFps[$id]; + } + return null; + } + + /** + * serialize and base64_encode of session data (If needed) + * + * @deprecated + * + * @param mixed $var target variable + * + * @author Naoki Sawada + * @return mixed|string + */ + public static function sessionDataEncode($var) + { + if (self::$base64encodeSessionData) { + $var = base64_encode(serialize($var)); + } + return $var; + } + + /** + * base64_decode and unserialize of session data (If needed) + * + * @deprecated + * + * @param mixed $var target variable + * @param bool $checkIs data type for check (array|string|object|int) + * + * @author Naoki Sawada + * @return bool|mixed + */ + public static function sessionDataDecode(&$var, $checkIs = null) + { + if (self::$base64encodeSessionData) { + $data = unserialize(base64_decode($var)); + } else { + $data = $var; + } + $chk = true; + if ($checkIs) { + switch ($checkIs) { + case 'array': + $chk = is_array($data); + break; + case 'string': + $chk = is_string($data); + break; + case 'object': + $chk = is_object($data); + break; + case 'int': + $chk = is_int($data); + break; + } + } + if (!$chk) { + unset($var); + return false; + } + return $data; + } + + /** + * Call session_write_close() if session is restarted + * + * @deprecated + * @return void + */ + public static function sessionWrite() + { + if (session_id()) { + session_write_close(); + } + } + + /** + * Return elFinder static variable + * + * @param $key + * + * @return mixed|null + */ + public static function getStaticVar($key) + { + return isset(elFinder::$$key) ? elFinder::$$key : null; + } + + /** + * Extend PHP execution time limit and also check connection is aborted + * + * @param Int $time + * + * @return void + * @throws elFinderAbortException + */ + public static function extendTimeLimit($time = null) + { + static $defLimit = null; + if (!self::aborted()) { + if (is_null($defLimit)) { + $defLimit = ini_get('max_execution_time'); + } + if ($defLimit != 0) { + $time = is_null($time) ? $defLimit : max($defLimit, $time); + set_time_limit($time); + } + } else { + throw new elFinderAbortException(); + } + } + + /** + * Check connection is aborted + * Script stop immediately if connection aborted + * + * @return void + * @throws elFinderAbortException + */ + public static function checkAborted() + { + elFinder::extendTimeLimit(); + } + + /** + * Return bytes from php.ini value + * + * @param string $iniName + * @param string $val + * + * @return number + */ + public static function getIniBytes($iniName = '', $val = '') + { + if ($iniName !== '') { + $val = ini_get($iniName); + if ($val === false) { + return 0; + } + } + $val = trim($val, "bB \t\n\r\0\x0B"); + $last = strtolower($val[strlen($val) - 1]); + $val = sprintf('%u', $val); + switch ($last) { + case 'y': + $val = elFinder::xKilobyte($val); + case 'z': + $val = elFinder::xKilobyte($val); + case 'e': + $val = elFinder::xKilobyte($val); + case 'p': + $val = elFinder::xKilobyte($val); + case 't': + $val = elFinder::xKilobyte($val); + case 'g': + $val = elFinder::xKilobyte($val); + case 'm': + $val = elFinder::xKilobyte($val); + case 'k': + $val = elFinder::xKilobyte($val); + } + return $val; + } + + /** + * Return X 1KByte + * + * @param integer|string $val The value + * + * @return number + */ + public static function xKilobyte($val) + { + if (strpos((string)$val * 1024, 'E') !== false) { + if (strpos((string)$val * 1.024, 'E') === false) { + $val *= 1.024; + } + $val .= '000'; + } else { + $val *= 1024; + } + return $val; + } + + /** + * Get script url. + * + * @return string full URL + * @author Naoki Sawada + */ + public static function getConnectorUrl() + { + if (defined('ELFINDER_CONNECTOR_URL')) { + return ELFINDER_CONNECTOR_URL; + } + + $https = (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off'); + $url = ($https ? 'https://' : 'http://') + . $_SERVER['SERVER_NAME'] // host + . ((empty($_SERVER['SERVER_PORT']) || (!$https && $_SERVER['SERVER_PORT'] == 80) || ($https && $_SERVER['SERVER_PORT'] == 443)) ? '' : (':' . $_SERVER['SERVER_PORT'])) // port + . $_SERVER['REQUEST_URI']; // path & query + list($url) = explode('?', $url); + + return $url; + } + + /** + * Get stream resource pointer by URL + * + * @param array $data array('target'=>'URL', 'headers' => array()) + * @param int $redirectLimit + * + * @return resource|boolean + * @author Naoki Sawada + */ + public static function getStreamByUrl($data, $redirectLimit = 5) + { + if (isset($data['target'])) { + $data = array( + 'cnt' => 0, + 'url' => $data['target'], + 'headers' => isset($data['headers']) ? $data['headers'] : array(), + 'postData' => isset($data['postData']) ? $data['postData'] : array(), + 'cookies' => array(), + ); + } + if ($data['cnt'] > $redirectLimit) { + return false; + } + $dlurl = $data['url']; + $data['url'] = ''; + $headers = $data['headers']; + + if ($dlurl) { + $url = parse_url($dlurl); + $ports = array( + 'http' => '80', + 'https' => '443', + 'ftp' => '21' + ); + $url['scheme'] = strtolower($url['scheme']); + if (!isset($url['port']) && isset($ports[$url['scheme']])) { + $url['port'] = $ports[$url['scheme']]; + } + if (!isset($url['port'])) { + return false; + } + $cookies = array(); + if ($data['cookies']) { + foreach ($data['cookies'] as $d => $c) { + if (strpos($url['host'], $d) !== false) { + $cookies[] = $c; + } + } + } + + $transport = ($url['scheme'] === 'https') ? 'ssl' : 'tcp'; + $query = isset($url['query']) ? '?' . $url['query'] : ''; + if (!($stream = stream_socket_client($transport . '://' . $url['host'] . ':' . $url['port']))) { + return false; + } + + $body = ''; + if (!empty($data['postData'])) { + $method = 'POST'; + if (is_array($data['postData'])) { + $body = http_build_query($data['postData']); + } else { + $body = $data['postData']; + } + } else { + $method = 'GET'; + } + + $sends = array(); + $sends[] = "$method {$url['path']}{$query} HTTP/1.1"; + $sends[] = "Host: {$url['host']}"; + foreach ($headers as $header) { + $sends[] = trim($header, "\r\n"); + } + $sends[] = 'Connection: Close'; + if ($cookies) { + $sends[] = 'Cookie: ' . implode('; ', $cookies); + } + if ($method === 'POST') { + $sends[] = 'Content-Type: application/x-www-form-urlencoded'; + $sends[] = 'Content-Length: ' . strlen($body); + } + $sends[] = "\r\n" . $body; + + stream_set_timeout($stream, 300); + fputs($stream, join("\r\n", $sends) . "\r\n"); + + while (($res = trim(fgets($stream))) !== '') { + // find redirect + if (preg_match('/^Location: (.+)$/i', $res, $m)) { + $data['url'] = $m[1]; + } + // fetch cookie + if (strpos($res, 'Set-Cookie:') === 0) { + $domain = $url['host']; + if (preg_match('/^Set-Cookie:(.+)(?:domain=\s*([^ ;]+))?/i', $res, $c1)) { + if (!empty($c1[2])) { + $domain = trim($c1[2]); + } + if (preg_match('/([^ ]+=[^;]+)/', $c1[1], $c2)) { + $data['cookies'][$domain] = $c2[1]; + } + } + } + // is seekable url + if (preg_match('/^(Accept-Ranges|Content-Range): bytes/i', $res)) { + elFinder::$seekableUrlFps[(int)$stream] = true; + } + } + if ($data['url']) { + ++$data['cnt']; + fclose($stream); + + return self::getStreamByUrl($data, $redirectLimit); + } + + return $stream; + } + + return false; + } + + /** + * Gets the fetch cookie file for curl. + * + * @return string The fetch cookie file. + */ + public function getFetchCookieFile() + { + $file = ''; + if ($tmpDir = $this->getTempDir()) { + $file = $tmpDir . '/.elFinderAnonymousCookie'; + } + return $file; + } + + /** + * Call curl_exec() with supported redirect on `safe_mode` or `open_basedir` + * + * @param resource $curl + * @param array $options + * @param array $headers + * @param array $postData + * + * @throws \Exception + * @return mixed + * @author Naoki Sawada + */ + public static function curlExec($curl, $options = array(), $headers = array(), $postData = array()) + { + $followLocation = (!ini_get('safe_mode') && !ini_get('open_basedir')); + if ($followLocation) { + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + } + + if ($options) { + curl_setopt_array($curl, $options); + } + + if ($headers) { + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + } + + $result = curl_exec($curl); + + if (!$followLocation && $redirect = curl_getinfo($curl, CURLINFO_REDIRECT_URL)) { + if ($stream = self::getStreamByUrl(array('target' => $redirect, 'headers' => $headers, 'postData' => $postData))) { + $result = stream_get_contents($stream); + } + } + + if ($result === false) { + if (curl_errno($curl)) { + throw new \Exception('curl_exec() failed: ' . curl_error($curl)); + } else { + throw new \Exception('curl_exec(): empty response'); + } + } + + curl_close($curl); + + return $result; + } + + /** + * Return bool that current request was aborted by client side + * + * @return boolean + */ + public static function aborted() + { + if ($file = self::$abortCheckFile) { + (version_compare(PHP_VERSION, '5.3.0') >= 0) ? clearstatcache(true, $file) : clearstatcache(); + if (!is_file($file)) { + // GC (expire 12h) + list($ptn) = explode('elfreq', $file); + self::GlobGC($ptn . 'elfreq*', 43200); + return true; + } + } + return false; + } + + /** + * Return array ["name without extention", "extention"] by filename + * + * @param string $name + * + * @return array + */ + public static function splitFileExtention($name) + { + if (preg_match('/^(.+?)?\.((?:tar\.(?:gz|bz|bz2|z|lzo))|cpio\.gz|ps\.gz|xcf\.(?:gz|bz2)|[a-z0-9]{1,10})$/i', $name, $m)) { + return array((string)$m[1], $m[2]); + } else { + return array($name, ''); + } + } + + /** + * Gets the memory size by imageinfo. + * + * @param array $imgInfo array that result of getimagesize() + * + * @return integer The memory size by imageinfo. + */ + public static function getMemorySizeByImageInfo($imgInfo) + { + $width = $imgInfo[0]; + $height = $imgInfo[1]; + $bits = isset($imgInfo['bits']) ? $imgInfo['bits'] : 24; + $channels = isset($imgInfo['channels']) ? $imgInfo['channels'] : 3; + return round(($width * $height * $bits * $channels / 8 + Pow(2, 16)) * 1.65); + } + + /** + * Auto expand memory for GD processing + * + * @param array $imgInfos The image infos + */ + public static function expandMemoryForGD($imgInfos) + { + if (elFinder::$memoryLimitGD != 0 && $imgInfos && is_array($imgInfos)) { + if (!is_array($imgInfos[0])) { + $imgInfos = array($imgInfos); + } + $limit = self::getIniBytes('', elFinder::$memoryLimitGD); + $memLimit = self::getIniBytes('memory_limit'); + $needs = 0; + foreach ($imgInfos as $info) { + $needs += self::getMemorySizeByImageInfo($info); + } + $needs += memory_get_usage(); + if ($needs > $memLimit && ($limit == -1 || $limit > $needs)) { + ini_set('memory_limit', $needs); + } + } + } + + /** + * Execute shell command + * + * @param string $command command line + * @param string $output stdout strings + * @param int $return_var process exit code + * @param string $error_output stderr strings + * + * @return int exit code + * @throws elFinderAbortException + * @author Alexey Sukhotin + */ + public static function procExec($command, &$output = '', &$return_var = -1, &$error_output = '') + { + + static $allowed = null; + + if ($allowed === null) { + if ($allowed = function_exists('proc_open')) { + if ($disabled = ini_get('disable_functions')) { + $funcs = array_map('trim', explode(',', $disabled)); + $allowed = !in_array('proc_open', $funcs); + } + } + } + + if (!$allowed) { + $return_var = -1; + return $return_var; + } + + if (!$command) { + $return_var = 0; + return $return_var; + } + + $descriptorspec = array( + 0 => array("pipe", "r"), // stdin + 1 => array("pipe", "w"), // stdout + 2 => array("pipe", "w") // stderr + ); + + $process = proc_open($command, $descriptorspec, $pipes, null, null); + + if (is_resource($process)) { + stream_set_blocking($pipes[1], 0); + stream_set_blocking($pipes[2], 0); + + fclose($pipes[0]); + + $tmpout = ''; + $tmperr = ''; + while (feof($pipes[1]) === false || feof($pipes[2]) === false) { + elFinder::extendTimeLimit(); + $read = array($pipes[1], $pipes[2]); + $write = null; + $except = null; + $ret = stream_select($read, $write, $except, 1); + if ($ret === false) { + // error + break; + } else if ($ret === 0) { + // timeout + continue; + } else { + foreach ($read as $sock) { + if ($sock === $pipes[1]) { + $tmpout .= fread($sock, 4096); + } else if ($sock === $pipes[2]) { + $tmperr .= fread($sock, 4096); + } + } + } + } + + fclose($pipes[1]); + fclose($pipes[2]); + + $output = $tmpout; + $error_output = $tmperr; + $return_var = proc_close($process); + + } else { + $return_var = -1; + } + + return $return_var; + + } + + /***************************************************************************/ + /* callbacks */ + /***************************************************************************/ + + /** + * Get command name of binded "commandName.subName" + * + * @param string $cmd + * + * @return string + */ + protected static function getCmdOfBind($cmd) + { + list($ret) = explode('.', $cmd); + return trim($ret); + } + + /** + * Add subName to commandName + * + * @param string $cmd + * @param string $sub + * + * @return string + */ + protected static function addSubToBindName($cmd, $sub) + { + return $cmd . '.' . trim($sub); + } + + /** + * Remove a file if connection is disconnected + * + * @param string $file + */ + public static function rmFileInDisconnected($file) + { + (connection_aborted() || connection_status() !== CONNECTION_NORMAL) && is_file($file) && unlink($file); + } + + /** + * Call back function on shutdown + * - delete files in $GLOBALS['elFinderTempFiles'] + */ + public static function onShutdown() + { + self::$abortCheckFile = null; + if (!empty($GLOBALS['elFinderTempFps'])) { + foreach (array_keys($GLOBALS['elFinderTempFps']) as $fp) { + is_resource($fp) && fclose($fp); + } + } + if (!empty($GLOBALS['elFinderTempFiles'])) { + foreach (array_keys($GLOBALS['elFinderTempFiles']) as $f) { + is_file($f) && is_writable($f) && unlink($f); + } + } + } + + /** + * Garbage collection with glob + * + * @param string $pattern + * @param integer $time + */ + public static function GlobGC($pattern, $time) + { + $now = time(); + foreach (glob($pattern) as $file) { + (filemtime($file) < ($now - $time)) && unlink($file); + } + } + +} // END class + +/** + * Custom exception class for aborting request + */ +class elFinderAbortException extends Exception +{ +} + +class elFinderTriggerException extends Exception +{ +} \ No newline at end of file diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderConnector.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderConnector.class.php new file mode 100644 index 0000000..dbbb27e --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderConnector.class.php @@ -0,0 +1,380 @@ +elFinder = $elFinder; + $this->reqMethod = strtoupper($_SERVER["REQUEST_METHOD"]); + if ($debug) { + self::$contentType = 'Content-Type: text/plain; charset=utf-8'; + } + } + + /** + * Execute elFinder command and output result + * + * @return void + * @throws Exception + * @author Dmitry (dio) Levashov + */ + public function run() + { + $isPost = $this->reqMethod === 'POST'; + $src = $isPost ? array_merge($_GET, $_POST) : $_GET; + $maxInputVars = (!$src || isset($src['targets'])) ? ini_get('max_input_vars') : null; + if ((!$src || $maxInputVars) && $rawPostData = file_get_contents('php://input')) { + // for max_input_vars and supports IE XDomainRequest() + $parts = explode('&', $rawPostData); + if (!$src || $maxInputVars < count($parts)) { + $src = array(); + foreach ($parts as $part) { + list($key, $value) = array_pad(explode('=', $part), 2, ''); + $key = rawurldecode($key); + if (preg_match('/^(.+?)\[([^\[\]]*)\]$/', $key, $m)) { + $key = $m[1]; + $idx = $m[2]; + if (!isset($src[$key])) { + $src[$key] = array(); + } + if ($idx) { + $src[$key][$idx] = rawurldecode($value); + } else { + $src[$key][] = rawurldecode($value); + } + } else { + $src[$key] = rawurldecode($value); + } + } + $_POST = $this->input_filter($src); + $_REQUEST = $this->input_filter(array_merge_recursive($src, $_REQUEST)); + } + } + + if (isset($src['targets']) && $this->elFinder->maxTargets && count($src['targets']) > $this->elFinder->maxTargets) { + $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_MAX_TARGTES))); + } + + $cmd = isset($src['cmd']) ? $src['cmd'] : ''; + $args = array(); + + if (!function_exists('json_encode')) { + $error = $this->elFinder->error(elFinder::ERROR_CONF, elFinder::ERROR_CONF_NO_JSON); + $this->output(array('error' => '{"error":["' . implode('","', $error) . '"]}', 'raw' => true)); + } + + if (!$this->elFinder->loaded()) { + $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_CONF, elFinder::ERROR_CONF_NO_VOL), 'debug' => $this->elFinder->mountErrors)); + } + + // telepat_mode: on + if (!$cmd && $isPost) { + $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UPLOAD, elFinder::ERROR_UPLOAD_TOTAL_SIZE), 'header' => 'Content-Type: text/html')); + } + // telepat_mode: off + + if (!$this->elFinder->commandExists($cmd)) { + $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UNKNOWN_CMD))); + } + + // collect required arguments to exec command + $hasFiles = false; + foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) { + if ($name === 'FILES') { + if (isset($_FILES)) { + $hasFiles = true; + } elseif ($req) { + $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd))); + } + } else { + $arg = isset($src[$name]) ? $src[$name] : ''; + + if (!is_array($arg) && $req !== '') { + $arg = trim($arg); + } + if ($req && $arg === '') { + $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd))); + } + $args[$name] = $arg; + } + } + + $args['debug'] = isset($src['debug']) ? !!$src['debug'] : false; + + $args = $this->input_filter($args); + if ($hasFiles) { + $args['FILES'] = $_FILES; + } + + try { + $this->output($this->elFinder->exec($cmd, $args)); + } catch (elFinderAbortException $e) { + // connection aborted + // unlock session data for multiple access + $this->elFinder->getSession()->close(); + // HTTP response code + header('HTTP/1.0 204 No Content'); + // clear output buffer + while (ob_get_level() && ob_end_clean()) { + } + exit(); + } + } + + /** + * Sets the header. + * + * @param array|string $value HTTP header(s) + */ + public function setHeader($value) + { + $this->header = $value; + } + + /** + * Output json + * + * @param array data to output + * + * @return void + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function output(array $data) + { + // unlock session data for multiple access + $this->elFinder->getSession()->close(); + // client disconnect should abort + ignore_user_abort(false); + + if ($this->header) { + self::sendHeader($this->header); + } + + if (isset($data['pointer'])) { + // set time limit to 0 + elFinder::extendTimeLimit(0); + + // send optional header + if (!empty($data['header'])) { + self::sendHeader($data['header']); + } + + // clear output buffer + while (ob_get_level() && ob_end_clean()) { + } + + $toEnd = true; + $fp = $data['pointer']; + $sendData = !($this->reqMethod === 'HEAD' || !empty($data['info']['xsendfile'])); + $psize = null; + if (($this->reqMethod === 'GET' || !$sendData) + && (elFinder::isSeekableStream($fp) || elFinder::isSeekableUrl($fp)) + && (array_search('Accept-Ranges: none', headers_list()) === false)) { + header('Accept-Ranges: bytes'); + if (!empty($_SERVER['HTTP_RANGE'])) { + $size = $data['info']['size']; + $end = $size - 1; + if (preg_match('/bytes=(\d*)-(\d*)(,?)/i', $_SERVER['HTTP_RANGE'], $matches)) { + if (empty($matches[3])) { + if (empty($matches[1]) && $matches[1] !== '0') { + $start = $size - $matches[2]; + } else { + $start = intval($matches[1]); + if (!empty($matches[2])) { + $end = intval($matches[2]); + if ($end >= $size) { + $end = $size - 1; + } + $toEnd = ($end == ($size - 1)); + } + } + $psize = $end - $start + 1; + + header('HTTP/1.1 206 Partial Content'); + header('Content-Length: ' . $psize); + header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size); + + // Apache mod_xsendfile dose not support range request + if (isset($data['info']['xsendfile']) && strtolower($data['info']['xsendfile']) === 'x-sendfile') { + if (function_exists('header_remove')) { + header_remove($data['info']['xsendfile']); + } else { + header($data['info']['xsendfile'] . ':'); + } + unset($data['info']['xsendfile']); + if ($this->reqMethod !== 'HEAD') { + $sendData = true; + } + } + + $sendData && !elFinder::isSeekableUrl($fp) && fseek($fp, $start); + } + } + } + if ($sendData && is_null($psize)) { + elFinder::rewind($fp); + } + } else { + header('Accept-Ranges: none'); + if (isset($data['info']) && !$data['info']['size']) { + if (function_exists('header_remove')) { + header_remove('Content-Length'); + } else { + header('Content-Length:'); + } + } + } + + if ($sendData) { + if ($toEnd || elFinder::isSeekableUrl($fp)) { + // PHP < 5.6 has a bug of fpassthru + // see https://bugs.php.net/bug.php?id=66736 + if (version_compare(PHP_VERSION, '5.6', '<')) { + file_put_contents('php://output', $fp); + } else { + fpassthru($fp); + } + } else { + $out = fopen('php://output', 'wb'); + stream_copy_to_stream($fp, $out, $psize); + fclose($out); + } + } + + if (!empty($data['volume'])) { + $data['volume']->close($fp, $data['info']['hash']); + } else { + fclose($fp); + } + exit(); + } else { + self::outputJson($data); + exit(0); + } + } + + /** + * Remove null & stripslashes applies on "magic_quotes_gpc" + * + * @param mixed $args + * + * @return mixed + * @author Naoki Sawada + */ + protected function input_filter($args) + { + static $magic_quotes_gpc = NULL; + + if ($magic_quotes_gpc === NULL) + $magic_quotes_gpc = (version_compare(PHP_VERSION, '5.4', '<') && get_magic_quotes_gpc()); + + if (is_array($args)) { + return array_map(array(& $this, 'input_filter'), $args); + } + $res = str_replace("\0", '', $args); + $magic_quotes_gpc && ($res = stripslashes($res)); + return $res; + } + + /** + * Send HTTP header + * + * @param string|array $header optional header + */ + protected static function sendHeader($header = null) + { + if ($header) { + if (is_array($header)) { + foreach ($header as $h) { + header($h); + } + } else { + header($header); + } + } + } + + /** + * Output JSON + * + * @param array $data + */ + public static function outputJson($data) + { + // send header + $header = isset($data['header']) ? $data['header'] : self::$contentType; + self::sendHeader($header); + + unset($data['header']); + + if (!empty($data['raw']) && isset($data['error'])) { + $out = $data['error']; + } else { + if (isset($data['debug']) && isset($data['debug']['backendErrors'])) { + $data['debug']['backendErrors'] = array_merge($data['debug']['backendErrors'], elFinder::$phpErrors); + } + $out = json_encode($data); + } + + // clear output buffer + while (ob_get_level() && ob_end_clean()) { + } + + header('Content-Length: ' . strlen($out)); + + echo $out; + + flush(); + } +}// END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderFlysystemGoogleDriveNetmount.php b/upload/system/library/sitecreator/elFinder/php/elFinderFlysystemGoogleDriveNetmount.php new file mode 100644 index 0000000..b3345cd --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderFlysystemGoogleDriveNetmount.php @@ -0,0 +1,380 @@ + '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#', + 'rootCssClass' => 'elfinder-navbar-root-googledrive', + 'gdAlias' => '%s@GDrive', + 'gdCacheDir' => __DIR__ . '/.tmp', + 'gdCachePrefix' => 'gd-', + 'gdCacheExpire' => 600 + ); + + $this->options = array_merge($this->options, $opts); + } + + /** + * Prepare driver before mount volume. + * Return true if volume is ready. + * + * @return bool + **/ + protected function init() + { + if (empty($this->options['icon'])) { + $this->options['icon'] = true; + } + if ($res = parent::init()) { + if ($this->options['icon'] === true) { + unset($this->options['icon']); + } + // enable command archive + $this->options['useRemoteArchive'] = true; + } + return $res; + } + + /** + * Prepare + * Call from elFinder::netmout() before volume->mount() + * + * @param $options + * + * @return Array + * @author Naoki Sawada + */ + public function netmountPrepare($options) + { + if (empty($options['client_id']) && defined('ELFINDER_GOOGLEDRIVE_CLIENTID')) { + $options['client_id'] = ELFINDER_GOOGLEDRIVE_CLIENTID; + } + if (empty($options['client_secret']) && defined('ELFINDER_GOOGLEDRIVE_CLIENTSECRET')) { + $options['client_secret'] = ELFINDER_GOOGLEDRIVE_CLIENTSECRET; + } + + if (!isset($options['pass'])) { + $options['pass'] = ''; + } + + try { + $client = new \Google_Client(); + $client->setClientId($options['client_id']); + $client->setClientSecret($options['client_secret']); + + if ($options['pass'] === 'reauth') { + $options['pass'] = ''; + $this->session->set('GoogleDriveAuthParams', [])->set('GoogleDriveTokens', []); + } else if ($options['pass'] === 'googledrive') { + $options['pass'] = ''; + } + + $options = array_merge($this->session->get('GoogleDriveAuthParams', []), $options); + + if (!isset($options['access_token'])) { + $options['access_token'] = $this->session->get('GoogleDriveTokens', []); + $this->session->remove('GoogleDriveTokens'); + } + $aToken = $options['access_token']; + + $rootObj = $service = null; + if ($aToken) { + try { + $client->setAccessToken($aToken); + if ($client->isAccessTokenExpired()) { + $aToken = array_merge($aToken, $client->fetchAccessTokenWithRefreshToken()); + $client->setAccessToken($aToken); + } + $service = new \Google_Service_Drive($client); + $rootObj = $service->files->get('root'); + + $options['access_token'] = $aToken; + $this->session->set('GoogleDriveAuthParams', $options); + + } catch (Exception $e) { + $aToken = []; + $options['access_token'] = []; + if ($options['user'] !== 'init') { + $this->session->set('GoogleDriveAuthParams', $options); + return array('exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE); + } + } + + } + + $itpCare = isset($options['code']); + $code = $itpCare? $options['code'] : (isset($_GET['code'])? $_GET['code'] : ''); + if ($code || $options['user'] === 'init') { + if (empty($options['url'])) { + $options['url'] = elFinder::getConnectorUrl(); + } + + if (isset($options['id'])) { + $callback = $options['url'] . (strpos($options['url'], '?') !== false? '&' : '?') . 'cmd=netmount&protocol=googledrive&host=' . ($options['id'] === 'elfinder'? '1' : $options['id']); + $client->setRedirectUri($callback); + } + + if (!$aToken && empty($code)) { + $client->setScopes([Google_Service_Drive::DRIVE]); + if (!empty($options['offline'])) { + $client->setApprovalPrompt('force'); + $client->setAccessType('offline'); + } + $url = $client->createAuthUrl(); + + $html = ''; + $html .= ''; + if (empty($options['pass']) && $options['host'] !== '1') { + $options['pass'] = 'return'; + $this->session->set('GoogleDriveAuthParams', $options); + return array('exit' => true, 'body' => $html); + } else { + $out = array( + 'node' => $options['id'], + 'json' => '{"protocol": "googledrive", "mode": "makebtn", "body" : "' . str_replace($html, '"', '\\"') . '", "error" : "' . elFinder::ERROR_ACCESS_DENIED . '"}', + 'bind' => 'netmount' + ); + return array('exit' => 'callback', 'out' => $out); + } + } else { + if ($code) { + if (!empty($options['id'])) { + $aToken = $client->fetchAccessTokenWithAuthCode($code); + $options['access_token'] = $aToken; + unset($options['code']); + $this->session->set('GoogleDriveTokens', $aToken)->set('GoogleDriveAuthParams', $options); + $out = array( + 'node' => $options['id'], + 'json' => '{"protocol": "googledrive", "mode": "done", "reset": 1}', + 'bind' => 'netmount' + ); + } else { + $nodeid = ($_GET['host'] === '1')? 'elfinder' : $_GET['host']; + $out = array( + 'node' => $nodeid, + 'json' => json_encode(array( + 'protocol' => 'googledrive', + 'host' => $nodeid, + 'mode' => 'redirect', + 'options' => array( + 'id' => $nodeid, + 'code'=> $code + ) + )), + 'bind' => 'netmount' + ); + } + if (!$itpCare) { + return array('exit' => 'callback', 'out' => $out); + } else { + return array('exit' => true, 'body' => $out['json']); + } + } + $folders = []; + foreach ($service->files->listFiles([ + 'pageSize' => 1000, + 'q' => 'trashed = false and mimeType = "application/vnd.google-apps.folder"' + ]) as $f) { + $folders[$f->getId()] = $f->getName(); + } + natcasesort($folders); + $folders = ['root' => $rootObj->getName()] + $folders; + $folders = json_encode($folders); + $json = '{"protocol": "googledrive", "mode": "done", "folders": ' . $folders . '}'; + $options['pass'] = 'return'; + $html = 'Google.com'; + $html .= ''; + $this->session->set('GoogleDriveAuthParams', $options); + return array('exit' => true, 'body' => $html); + } + } + } catch (Exception $e) { + $this->session->remove('GoogleDriveAuthParams')->remove('GoogleDriveTokens'); + if (empty($options['pass'])) { + return array('exit' => true, 'body' => '{msg:' . elFinder::ERROR_ACCESS_DENIED . '}' . ' ' . $e->getMessage()); + } else { + return array('exit' => true, 'error' => [elFinder::ERROR_ACCESS_DENIED, $e->getMessage()]); + } + } + + if (!$aToken) { + return array('exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE); + } + + if ($options['path'] === '/') { + $options['path'] = 'root'; + } + + try { + $file = $service->files->get($options['path']); + $options['alias'] = sprintf($this->options['gdAlias'], $file->getName()); + if (!empty($this->options['netkey'])) { + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']); + } + } catch (Google_Service_Exception $e) { + $err = json_decode($e->getMessage(), true); + if (isset($err['error']) && $err['error']['code'] == 404) { + return array('exit' => true, 'error' => [elFinder::ERROR_TRGDIR_NOT_FOUND, $options['path']]); + } else { + return array('exit' => true, 'error' => $e->getMessage()); + } + } catch (Exception $e) { + return array('exit' => true, 'error' => $e->getMessage()); + } + + foreach (['host', 'user', 'pass', 'id', 'offline'] as $key) { + unset($options[$key]); + } + + return $options; + } + + /** + * process of on netunmount + * Drop table `dropbox` & rm thumbs + * + * @param $netVolumes + * @param $key + * + * @return bool + * @internal param array $options + */ + public function netunmount($netVolumes, $key) + { + $cache = $this->options['gdCacheDir'] . DIRECTORY_SEPARATOR . $this->options['gdCachePrefix'] . $this->netMountKey; + if (file_exists($cache) && is_writeable($cache)) { + unlink($cache); + } + if ($tmbs = glob($this->tmbPath . DIRECTORY_SEPARATOR . $this->netMountKey . '*')) { + foreach ($tmbs as $file) { + unlink($file); + } + } + return true; + } + + /** + * "Mount" volume. + * Return true if volume available for read or write, + * false - otherwise + * + * @param array $opts + * + * @return bool + * @author Naoki Sawada + */ + public function mount(array $opts) + { + $creds = null; + if (isset($opts['access_token'])) { + $this->netMountKey = md5(join('-', array('googledrive', $opts['path'], (isset($opts['access_token']['refresh_token']) ? $opts['access_token']['refresh_token'] : $opts['access_token']['access_token'])))); + } + + $client = new \Google_Client(); + $client->setClientId($opts['client_id']); + $client->setClientSecret($opts['client_secret']); + + if (!empty($opts['access_token'])) { + $client->setAccessToken($opts['access_token']); + } + if ($this->needOnline && $client->isAccessTokenExpired()) { + try { + $creds = $client->fetchAccessTokenWithRefreshToken(); + } catch (LogicException $e) { + $this->session->remove('GoogleDriveAuthParams'); + throw $e; + } + } + + $service = new \Google_Service_Drive($client); + + // If path is not set, use the root + if (!isset($opts['path']) || $opts['path'] === '') { + $opts['path'] = 'root'; + } + + $googleDrive = new GoogleDriveAdapter($service, $opts['path'], ['useHasDir' => true]); + + $opts['fscache'] = null; + if ($this->options['gdCacheDir'] && is_writeable($this->options['gdCacheDir'])) { + if ($this->options['gdCacheExpire']) { + $opts['fscache'] = new elFinderVolumeFlysystemGoogleDriveCache(new Local($this->options['gdCacheDir']), $this->options['gdCachePrefix'] . $this->netMountKey, $this->options['gdCacheExpire']); + } + } + if ($opts['fscache']) { + $filesystem = new Filesystem(new CachedAdapter($googleDrive, $opts['fscache'])); + } else { + $filesystem = new Filesystem($googleDrive); + } + + $opts['driver'] = 'FlysystemExt'; + $opts['filesystem'] = $filesystem; + $opts['separator'] = '/'; + $opts['checkSubfolders'] = true; + if (!isset($opts['alias'])) { + $opts['alias'] = 'GoogleDrive'; + } + + if ($res = parent::mount($opts)) { + // update access_token of session data + if ($creds) { + $netVolumes = $this->session->get('netvolume'); + $netVolumes[$this->netMountKey]['access_token'] = array_merge($netVolumes[$this->netMountKey]['access_token'], $creds); + $this->session->set('netvolume', $netVolumes); + } + } + + return $res; + } + + /** + * @inheritdoc + */ + protected function tmbname($stat) + { + return $this->netMountKey . substr(substr($stat['hash'], strlen($this->id)), -38) . $stat['ts'] . '.png'; + } + + /** + * Return debug info for client. + * + * @return array + **/ + public function debug() + { + $res = parent::debug(); + if (!empty($this->options['netkey']) && empty($this->options['refresh_token']) && $this->options['access_token'] && isset($this->options['access_token']['refresh_token'])) { + $res['refresh_token'] = $this->options['access_token']['refresh_token']; + } + + return $res; + } +} diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderPlugin.php b/upload/system/library/sitecreator/elFinder/php/elFinderPlugin.php new file mode 100644 index 0000000..2bc85c7 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderPlugin.php @@ -0,0 +1,113 @@ +opts; + if (is_object($volume)) { + $volOpts = $volume->getOptionsPlugin($name); + if (is_array($volOpts)) { + $opts = array_merge($opts, $volOpts); + } + } + return $opts; + } + + /** + * Is enabled with options + * + * @param array $opts + * @param elFinder $elfinder + * + * @return boolean + */ + protected function iaEnabled($opts, $elfinder = null) + { + if (!$opts['enable']) { + return false; + } + + // check post var 'contentSaveId' to disable this plugin + if ($elfinder && !empty($opts['disableWithContentSaveId'])) { + $session = $elfinder->getSession(); + $urlContentSaveIds = $session->get('urlContentSaveIds', array()); + if (!empty(elFinder::$currentArgs['contentSaveId']) && ($contentSaveId = elFinder::$currentArgs['contentSaveId'])) { + if (!empty($urlContentSaveIds[$contentSaveId])) { + $elfinder->removeUrlContentSaveId($contentSaveId); + return false; + } + } + } + + if (isset($opts['onDropWith']) && !is_null($opts['onDropWith'])) { + // plugin disabled by default, enabled only if given key is pressed + if (isset($_REQUEST['dropWith']) && $_REQUEST['dropWith']) { + $onDropWith = $opts['onDropWith']; + $action = (int)$_REQUEST['dropWith']; + if (!is_array($onDropWith)) { + $onDropWith = array($onDropWith); + } + foreach ($onDropWith as $key) { + $key = (int)$key; + if (($action & $key) === $key) { + return true; + } + } + } + return false; + } + + if (isset($opts['offDropWith']) && !is_null($opts['offDropWith']) && isset($_REQUEST['dropWith'])) { + // plugin enabled by default, disabled only if given key is pressed + $offDropWith = $opts['offDropWith']; + $action = (int)$_REQUEST['dropWith']; + if (!is_array($offDropWith)) { + $offDropWith = array($offDropWith); + } + $res = true; + foreach ($offDropWith as $key) { + $key = (int)$key; + if ($key === 0) { + if ($action === 0) { + $res = false; + break; + } + } else { + if (($action & $key) === $key) { + $res = false; + break; + } + } + } + if (!$res) { + return false; + } + } + + return true; + } +} diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderSession.php b/upload/system/library/sitecreator/elFinder/php/elFinderSession.php new file mode 100644 index 0000000..f17fa5b --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderSession.php @@ -0,0 +1,337 @@ + false, + 'keys' => array( + 'default' => 'elFinderCaches', + 'netvolume' => 'elFinderNetVolumes' + ), + 'cookieParams' => array() + ); + + /** + * Constractor + * + * @param array $opts The options + * + * @return self Instanse of this class + */ + public function __construct($opts) + { + $this->opts = array_merge($this->opts, $opts); + $this->base64encode = !empty($this->opts['base64encode']); + $this->keys = $this->opts['keys']; + if (function_exists('apache_get_version') || $this->opts['cookieParams']) { + $this->fixCookieRegist = true; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get($key, $empty = null) + { + $closed = false; + if (!$this->started) { + $closed = true; + $this->start(); + } + + $data = null; + + if ($this->started) { + $session =& $this->getSessionRef($key); + $data = $session; + if ($data && $this->base64encode) { + $data = $this->decodeData($data); + } + } + + $checkFn = null; + if (!is_null($empty)) { + if (is_string($empty)) { + $checkFn = 'is_string'; + } elseif (is_array($empty)) { + $checkFn = 'is_array'; + } elseif (is_object($empty)) { + $checkFn = 'is_object'; + } elseif (is_float($empty)) { + $checkFn = 'is_float'; + } elseif (is_int($empty)) { + $checkFn = 'is_int'; + } + } + + if (is_null($data) || ($checkFn && !$checkFn($data))) { + $session = $data = $empty; + } + + if ($closed) { + $this->close(); + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function start() + { + set_error_handler(array($this, 'session_start_error'), E_NOTICE | E_WARNING); + + // apache2 SAPI has a bug of session cookie register + // see https://bugs.php.net/bug.php?id=75554 + // see https://github.com/php/php-src/pull/3231 + if ($this->fixCookieRegist === true) { + if ((int)ini_get('session.use_cookies') === 1) { + if (ini_set('session.use_cookies', 0) === false) { + $this->fixCookieRegist === false; + } + } + } + + if (version_compare(PHP_VERSION, '5.4.0', '>=')) { + if (session_status() !== PHP_SESSION_ACTIVE) { + session_start(); + } + } else { + session_start(); + } + $this->started = session_id() ? true : false; + + restore_error_handler(); + + return $this; + } + + /** + * Get variable reference of $_SESSION + * + * @param string $key key of $_SESSION array + * + * @return mixed|null + */ + protected function & getSessionRef($key) + { + $session = null; + if ($this->started) { + list($cat, $name) = array_pad(explode('.', $key, 2), 2, null); + if (is_null($name)) { + if (!isset($this->keys[$cat])) { + $name = $cat; + $cat = 'default'; + } + } + if (isset($this->keys[$cat])) { + $cat = $this->keys[$cat]; + } else { + $name = $cat . '.' . $name; + $cat = $this->keys['default']; + } + if (is_null($name)) { + if (!isset($_SESSION[$cat])) { + $_SESSION[$cat] = null; + } + $session =& $_SESSION[$cat]; + } else { + if (!isset($_SESSION[$cat]) || !is_array($_SESSION[$cat])) { + $_SESSION[$cat] = array(); + } + if (!isset($_SESSION[$cat][$name])) { + $_SESSION[$cat][$name] = null; + } + $session =& $_SESSION[$cat][$name]; + } + } + return $session; + } + + /** + * base64 decode of session val + * + * @param $data + * + * @return bool|mixed|string|null + */ + protected function decodeData($data) + { + if ($this->base64encode) { + if (is_string($data)) { + if (($data = base64_decode($data)) !== false) { + $data = unserialize($data); + } else { + $data = null; + } + } else { + $data = null; + } + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function close() + { + if ($this->started) { + if ($this->fixCookieRegist === true) { + // regist cookie only once for apache2 SAPI + $cParm = session_get_cookie_params(); + if ($this->opts['cookieParams'] && is_array($this->opts['cookieParams'])) { + $cParm = array_merge($cParm, $this->opts['cookieParams']); + } + if (version_compare(PHP_VERSION, '7.3', '<')) { + setcookie(session_name(), session_id(), 0, $cParm['path'] . (!empty($cParm['SameSite'])? '; SameSite=' . $cParm['SameSite'] : ''), $cParm['domain'], $cParm['secure'], $cParm['httponly']); + } else { + $allows = array('expires' => true, 'path' => true, 'domain' => true, 'secure' => true, 'httponly' => true, 'samesite' => true); + foreach(array_keys($cParm) as $_k) { + if (!isset($allows[$_k])) { + unset($cParm[$_k]); + } + } + setcookie(session_name(), session_id(), $cParm); + } + $this->fixCookieRegist = false; + } + session_write_close(); + } + $this->started = false; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function set($key, $data) + { + $closed = false; + if (!$this->started) { + $closed = true; + $this->start(); + } + $session =& $this->getSessionRef($key); + if ($this->base64encode) { + $data = $this->encodeData($data); + } + $session = $data; + + if ($closed) { + $this->close(); + } + + return $this; + } + + /** + * base64 encode for session val + * + * @param $data + * + * @return string + */ + protected function encodeData($data) + { + if ($this->base64encode) { + $data = base64_encode(serialize($data)); + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function remove($key) + { + $closed = false; + if (!$this->started) { + $closed = true; + $this->start(); + } + + list($cat, $name) = array_pad(explode('.', $key, 2), 2, null); + if (is_null($name)) { + if (!isset($this->keys[$cat])) { + $name = $cat; + $cat = 'default'; + } + } + if (isset($this->keys[$cat])) { + $cat = $this->keys[$cat]; + } else { + $name = $cat . '.' . $name; + $cat = $this->keys['default']; + } + if (is_null($name)) { + unset($_SESSION[$cat]); + } else { + if (isset($_SESSION[$cat]) && is_array($_SESSION[$cat])) { + unset($_SESSION[$cat][$name]); + } + } + + if ($closed) { + $this->close(); + } + + return $this; + } + + /** + * sessioin error handler (Only for suppression of error at session start) + * + * @param $errno + * @param $errstr + */ + protected function session_start_error($errno, $errstr) + { + } +} diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderSessionInterface.php b/upload/system/library/sitecreator/elFinder/php/elFinderSessionInterface.php new file mode 100644 index 0000000..ff90332 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderSessionInterface.php @@ -0,0 +1,57 @@ + '', + 'client_secret' => '', + 'accessToken' => '', + 'root' => 'Box.com', + 'path' => '/', + 'separator' => '/', + 'tmbPath' => '', + 'tmbURL' => '', + 'tmpPath' => '', + 'acceptedName' => '#^[^\\\/]+$#', + 'rootCssClass' => 'elfinder-navbar-root-box', + ); + $this->options = array_merge($this->options, $opts); + $this->options['mimeDetect'] = 'internal'; + } + + /*********************************************************************/ + /* ORIGINAL FUNCTIONS */ + /*********************************************************************/ + + /** + * Get Parent ID, Item ID, Parent Path as an array from path. + * + * @param string $path + * + * @return array + */ + protected function _bd_splitPath($path) + { + $path = trim($path, '/'); + $pid = ''; + if ($path === '') { + $id = '0'; + $parent = ''; + } else { + $paths = explode('/', trim($path, '/')); + $id = array_pop($paths); + if ($paths) { + $parent = '/' . implode('/', $paths); + $pid = array_pop($paths); + } else { + $pid = '0'; + $parent = '/'; + } + } + + return array($pid, $id, $parent); + } + + /** + * Obtains a new access token from OAuth. This token is valid for one hour. + * + * @param string $clientSecret The Box client secret + * @param string $code The code returned by Box after + * successful log in + * @param string $redirectUri Must be the same as the redirect URI passed + * to LoginUrl + * + * @return bool|object + * @throws \Exception Thrown if this Client instance's clientId is not set + * @throws \Exception Thrown if the redirect URI of this Client instance's + * state is not set + */ + protected function _bd_obtainAccessToken($client_id, $client_secret, $code) + { + if (null === $client_id) { + return $this->setError('The client ID must be set to call obtainAccessToken()'); + } + + if (null === $client_secret) { + return $this->setError('The client Secret must be set to call obtainAccessToken()'); + } + + if (null === $code) { + return $this->setError('Authorization code must be set to call obtainAccessToken()'); + } + + $url = self::TOKEN_URL; + + $curl = curl_init(); + + $fields = http_build_query( + array( + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'code' => $code, + 'grant_type' => 'authorization_code', + ) + ); + + curl_setopt_array($curl, array( + // General options. + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $fields, + CURLOPT_URL => $url, + )); + + $decoded = $this->_bd_curlExec($curl, true, array('Content-Length: ' . strlen($fields))); + + $res = (object)array( + 'expires' => time() + $decoded->expires_in - 30, + 'initialToken' => '', + 'data' => $decoded + ); + if (!empty($decoded->refresh_token)) { + $res->initialToken = md5($client_id . $decoded->refresh_token); + } + return $res; + } + + /** + * Get token and auto refresh. + * + * @return true|string error message + * @throws Exception + */ + protected function _bd_refreshToken() + { + if (!property_exists($this->token, 'expires') || $this->token->expires < time()) { + if (!$this->options['client_id']) { + $this->options['client_id'] = ELFINDER_BOX_CLIENTID; + } + + if (!$this->options['client_secret']) { + $this->options['client_secret'] = ELFINDER_BOX_CLIENTSECRET; + } + + if (empty($this->token->data->refresh_token)) { + throw new \Exception(elFinder::ERROR_REAUTH_REQUIRE); + } else { + $refresh_token = $this->token->data->refresh_token; + $initialToken = $this->_bd_getInitialToken(); + } + + $lock = ''; + $aTokenFile = $this->aTokenFile? $this->aTokenFile : $this->_bd_getATokenFile(); + if ($aTokenFile && is_file($aTokenFile)) { + $lock = $aTokenFile . '.lock'; + if (file_exists($lock)) { + // Probably updating on other instance + return true; + } + touch($lock); + $GLOBALS['elFinderTempFiles'][$lock] = true; + } + + $postData = array( + 'client_id' => $this->options['client_id'], + 'client_secret' => $this->options['client_secret'], + 'grant_type' => 'refresh_token', + 'refresh_token' => $refresh_token + ); + + $url = self::TOKEN_URL; + + $curl = curl_init(); + + curl_setopt_array($curl, array( + // General options. + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, // i am sending post data + CURLOPT_POSTFIELDS => http_build_query($postData), + CURLOPT_URL => $url, + )); + + $decoded = $error = ''; + try { + $decoded = $this->_bd_curlExec($curl, true, array(), $postData); + } catch (Exception $e) { + $error = $e->getMessage(); + } + if (!$decoded && !$error) { + $error = 'Tried to renew the access token, but did not get a response from the Box server.'; + } + if ($error) { + $lock && unlink($lock); + throw new \Exception('Box access token update failed. ('.$error.') If this message appears repeatedly, please notify the administrator.'); + } + + if (empty($decoded->access_token)) { + if ($aTokenFile) { + if (is_file($aTokenFile)) { + unlink($aTokenFile); + } + } + $err = property_exists($decoded, 'error')? ' ' . $decoded->error : ''; + $err .= property_exists($decoded, 'error_description')? ' ' . $decoded->error_description : ''; + throw new \Exception($err? $err : elFinder::ERROR_REAUTH_REQUIRE); + } + + $token = (object)array( + 'expires' => time() + $decoded->expires_in - 300, + 'initialToken' => $initialToken, + 'data' => $decoded, + ); + + $this->token = $token; + $json = json_encode($token); + + if (!empty($decoded->refresh_token)) { + if (empty($this->options['netkey']) && $aTokenFile) { + file_put_contents($aTokenFile, json_encode($token), LOCK_EX); + $this->options['accessToken'] = $json; + } else if (!empty($this->options['netkey'])) { + // OAuth2 refresh token can be used only once, + // so update it if it is the same as the token file + if ($aTokenFile && is_file($aTokenFile)) { + if ($_token = json_decode(file_get_contents($aTokenFile))) { + if ($_token->data->refresh_token === $refresh_token) { + file_put_contents($aTokenFile, $json, LOCK_EX); + } + } + } + $this->options['accessToken'] = $json; + // update session value + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'accessToken', $json); + $this->session->set('BoxTokens', $token); + } else { + throw new \Exception(ERROR_CREATING_TEMP_DIR); + } + } + $lock && unlink($lock); + } + + return true; + } + + /** + * Creates a base cURL object which is compatible with the Box.com API. + * + * @param array $options cURL options + * + * @return resource A compatible cURL object + */ + protected function _bd_prepareCurl($options = array()) + { + $curl = curl_init(); + + $defaultOptions = array( + // General options. + CURLOPT_RETURNTRANSFER => true, + ); + + curl_setopt_array($curl, $options + $defaultOptions); + + return $curl; + } + + /** + * Creates a base cURL object which is compatible with the Box.com API. + * + * @param $url + * @param bool $contents + * + * @return boolean|array + * @throws Exception + */ + protected function _bd_fetch($url, $contents = false) + { + $curl = curl_init($url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + if ($contents) { + return $this->_bd_curlExec($curl, false); + } else { + $result = $this->_bd_curlExec($curl); + + if (isset($result->entries)) { + $res = $result->entries; + $cnt = count($res); + $total = $result->total_count; + $offset = $result->offset; + $single = ($result->limit == 1) ? true : false; + if (!$single && $total > ($offset + $cnt)) { + $offset = $offset + $cnt; + if (strpos($url, 'offset=') === false) { + $url .= '&offset=' . $offset; + } else { + $url = preg_replace('/^(.+?offset=)\d+(.*)$/', '${1}' . $offset . '$2', $url); + } + $more = $this->_bd_fetch($url); + if (is_array($more)) { + $res = array_merge($res, $more); + } + } + + return $res; + } else { + if (isset($result->type) && $result->type === 'error') { + return false; + } else { + return $result; + } + } + } + } + + /** + * Call curl_exec(). + * + * @param resource $curl + * @param bool|string $decodeOrParent + * @param array $headers + * + * @throws \Exception + * @return mixed + */ + protected function _bd_curlExec($curl, $decodeOrParent = true, $headers = array(), $postData = array()) + { + if ($this->token) { + $headers = array_merge(array( + 'Authorization: Bearer ' . $this->token->data->access_token, + ), $headers); + } + + $result = elFinder::curlExec($curl, array(), $headers, $postData); + + if (!$decodeOrParent) { + return $result; + } + + $decoded = json_decode($result); + + if ($error = !empty($decoded->error_code)) { + $errmsg = $decoded->error_code; + if (!empty($decoded->message)) { + $errmsg .= ': ' . $decoded->message; + } + throw new \Exception($errmsg); + } else if ($error = !empty($decoded->error)) { + $errmsg = $decoded->error; + if (!empty($decoded->error_description)) { + $errmsg .= ': ' . $decoded->error_description; + } + throw new \Exception($errmsg); + } + + // make catch + if ($decodeOrParent && $decodeOrParent !== true) { + $raws = null; + if (isset($decoded->entries)) { + $raws = $decoded->entries; + } elseif (isset($decoded->id)) { + $raws = array($decoded); + } + if ($raws) { + foreach ($raws as $raw) { + if (isset($raw->id)) { + $stat = $this->_bd_parseRaw($raw); + $itemPath = $this->_joinPath($decodeOrParent, $raw->id); + $this->updateCache($itemPath, $stat); + } + } + } + } + + return $decoded; + } + + /** + * Drive query and fetchAll. + * + * @param $itemId + * @param bool $fetch_self + * @param bool $recursive + * + * @return bool|object + * @throws Exception + */ + protected function _bd_query($itemId, $fetch_self = false, $recursive = false) + { + $result = []; + + if (null === $itemId) { + $itemId = '0'; + } + + if ($fetch_self) { + $path = '/folders/' . $itemId . '?fields=' . self::FETCHFIELDS; + } else { + $path = '/folders/' . $itemId . '/items?limit=1000&fields=' . self::FETCHFIELDS; + } + + $url = self::API_URL . $path; + + if ($recursive) { + foreach ($this->_bd_fetch($url) as $file) { + if ($file->type == 'folder') { + $result[] = $file; + $result = array_merge($result, $this->_bd_query($file->id, $fetch_self = false, $recursive = true)); + } elseif ($file->type == 'file') { + $result[] = $file; + } + } + } else { + $result = $this->_bd_fetch($url); + if ($fetch_self && !$result) { + $path = '/files/' . $itemId . '?fields=' . self::FETCHFIELDS; + $url = self::API_URL . $path; + $result = $this->_bd_fetch($url); + } + } + + return $result; + } + + /** + * Get dat(box metadata) from Box.com. + * + * @param string $path + * + * @return object box metadata + * @throws Exception + */ + protected function _bd_getRawItem($path) + { + if ($path == '/') { + return $this->_bd_query('0', $fetch_self = true); + } + + list(, $itemId) = $this->_bd_splitPath($path); + + try { + return $this->_bd_query($itemId, $fetch_self = true); + } catch (Exception $e) { + $empty = new stdClass; + return $empty; + } + } + + /** + * Parse line from box metadata output and return file stat (array). + * + * @param object $raw line from ftp_rawlist() output + * + * @return array + * @author Dmitry Levashov + **/ + protected function _bd_parseRaw($raw) + { + $stat = array(); + + $stat['rev'] = isset($raw->id) ? $raw->id : 'root'; + $stat['name'] = $raw->name; + if (!empty($raw->modified_at)) { + $stat['ts'] = strtotime($raw->modified_at); + } + + if ($raw->type === 'folder') { + $stat['mime'] = 'directory'; + $stat['size'] = 0; + $stat['dirs'] = -1; + } else { + $stat['size'] = (int)$raw->size; + if (!empty($raw->shared_link->url) && $raw->shared_link->access == 'open') { + if ($url = $this->getSharedWebContentLink($raw)) { + $stat['url'] = $url; + } + } elseif (!$this->disabledGetUrl) { + $stat['url'] = '1'; + } + } + + return $stat; + } + + /** + * Get thumbnail from Box.com. + * + * @param string $path + * @param string $size + * + * @return string | boolean + */ + protected function _bd_getThumbnail($path) + { + list(, $itemId) = $this->_bd_splitPath($path); + + try { + $url = self::API_URL . '/files/' . $itemId . '/thumbnail.png?min_height=' . $this->tmbSize . '&min_width=' . $this->tmbSize; + + $contents = $this->_bd_fetch($url, true); + return $contents; + } catch (Exception $e) { + return false; + } + } + + /** + * Remove item. + * + * @param string $path file path + * + * @return bool + **/ + protected function _bd_unlink($path, $type = null) + { + try { + list(, $itemId) = $this->_bd_splitPath($path); + + if ($type == 'folders') { + $url = self::API_URL . '/' . $type . '/' . $itemId . '?recursive=true'; + } else { + $url = self::API_URL . '/' . $type . '/' . $itemId; + } + + $curl = $this->_bd_prepareCurl(array( + CURLOPT_URL => $url, + CURLOPT_CUSTOMREQUEST => 'DELETE', + )); + + //unlink or delete File or Folder in the Parent + $this->_bd_curlExec($curl); + } catch (Exception $e) { + return $this->setError('Box error: ' . $e->getMessage()); + } + + return true; + } + + /** + * Get AccessToken file path + * + * @return string ( description_of_the_return_value ) + */ + protected function _bd_getATokenFile() + { + $tmp = $aTokenFile = ''; + if (!empty($this->token->data->refresh_token)) { + if (!$this->tmp) { + $tmp = elFinder::getStaticVar('commonTempPath'); + if (!$tmp) { + $tmp = $this->getTempPath(); + } + $this->tmp = $tmp; + } + if ($tmp) { + $aTokenFile = $tmp . DIRECTORY_SEPARATOR . $this->_bd_getInitialToken() . '.btoken'; + } + } + return $aTokenFile; + } + + /** + * Get Initial Token (MD5 hash) + * + * @return string + */ + protected function _bd_getInitialToken() + { + return (empty($this->token->initialToken)? md5($this->options['client_id'] . (!empty($this->token->data->refresh_token)? $this->token->data->refresh_token : $this->token->data->access_token)) : $this->token->initialToken); + } + + /*********************************************************************/ + /* OVERRIDE FUNCTIONS */ + /*********************************************************************/ + + /** + * Prepare + * Call from elFinder::netmout() before volume->mount(). + * + * @return array + * @author Naoki Sawada + * @author Raja Sharma updating for Box + **/ + public function netmountPrepare($options) + { + if (empty($options['client_id']) && defined('ELFINDER_BOX_CLIENTID')) { + $options['client_id'] = ELFINDER_BOX_CLIENTID; + } + if (empty($options['client_secret']) && defined('ELFINDER_BOX_CLIENTSECRET')) { + $options['client_secret'] = ELFINDER_BOX_CLIENTSECRET; + } + + if (isset($options['pass']) && $options['pass'] === 'reauth') { + $options['user'] = 'init'; + $options['pass'] = ''; + $this->session->remove('BoxTokens'); + } + + if (isset($options['id'])) { + $this->session->set('nodeId', $options['id']); + } else if ($_id = $this->session->get('nodeId')) { + $options['id'] = $_id; + $this->session->set('nodeId', $_id); + } + + if (!empty($options['tmpPath'])) { + if ((is_dir($options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($options['tmpPath'])) { + $this->tmp = $options['tmpPath']; + } + } + + try { + if (empty($options['client_id']) || empty($options['client_secret'])) { + return array('exit' => true, 'body' => '{msg:errNetMountNoDriver}'); + } + + $itpCare = isset($options['code']); + $code = $itpCare? $options['code'] : (isset($_GET['code'])? $_GET['code'] : ''); + if ($code) { + try { + if (!empty($options['id'])) { + // Obtain the token using the code received by the Box.com API + $this->session->set('BoxTokens', + $this->_bd_obtainAccessToken($options['client_id'], $options['client_secret'], $code)); + + $out = array( + 'node' => $options['id'], + 'json' => '{"protocol": "box", "mode": "done", "reset": 1}', + 'bind' => 'netmount' + ); + } else { + $nodeid = ($_GET['host'] === '1')? 'elfinder' : $_GET['host']; + $out = array( + 'node' => $nodeid, + 'json' => json_encode(array( + 'protocol' => 'box', + 'host' => $nodeid, + 'mode' => 'redirect', + 'options' => array( + 'id' => $nodeid, + 'code'=> $code + ) + )), + 'bind' => 'netmount' + ); + } + if (!$itpCare) { + return array('exit' => 'callback', 'out' => $out); + } else { + return array('exit' => true, 'body' => $out['json']); + } + } catch (Exception $e) { + $out = array( + 'node' => $options['id'], + 'json' => json_encode(array('error' => $e->getMessage())), + ); + + return array('exit' => 'callback', 'out' => $out); + } + } elseif (!empty($_GET['error'])) { + $out = array( + 'node' => $options['id'], + 'json' => json_encode(array('error' => elFinder::ERROR_ACCESS_DENIED)), + ); + + return array('exit' => 'callback', 'out' => $out); + } + + if ($options['user'] === 'init') { + $this->token = $this->session->get('BoxTokens'); + + if ($this->token) { + try { + $this->_bd_refreshToken(); + } catch (Exception $e) { + $this->setError($e->getMessage()); + $this->token = null; + $this->session->remove('BoxTokens'); + } + } + + if (empty($this->token)) { + $result = false; + } else { + $path = $options['path']; + if ($path === '/' || $path === 'root') { + $path = '0'; + } + $result = $this->_bd_query($path, $fetch_self = false, $recursive = false); + } + + if ($result === false) { + $redirect = elFinder::getConnectorUrl(); + $redirect .= (strpos($redirect, '?') !== false? '&' : '?') . 'cmd=netmount&protocol=box&host=' . ($options['id'] === 'elfinder'? '1' : $options['id']); + + try { + $this->session->set('BoxTokens', (object)array('token' => null)); + $url = self::AUTH_URL . '?' . http_build_query(array('response_type' => 'code', 'client_id' => $options['client_id'], 'redirect_uri' => $redirect)); + } catch (Exception $e) { + return array('exit' => true, 'body' => '{msg:errAccess}'); + } + + $html = ''; + $html .= ''; + + return array('exit' => true, 'body' => $html); + } else { + $folders = []; + + if ($result) { + foreach ($result as $res) { + if ($res->type == 'folder') { + $folders[$res->id . ' '] = $res->name; + } + } + natcasesort($folders); + } + + if ($options['pass'] === 'folders') { + return ['exit' => true, 'folders' => $folders]; + } + + $folders = ['root' => 'My Box'] + $folders; + $folders = json_encode($folders); + + $expires = empty($this->token->data->refresh_token) ? (int)$this->token->expires : 0; + $mnt2res = empty($this->token->data->refresh_token) ? '' : ', "mnt2res": 1'; + $json = '{"protocol": "box", "mode": "done", "folders": ' . $folders . ', "expires": ' . $expires . $mnt2res . '}'; + $html = 'Box.com'; + $html .= ''; + + return array('exit' => true, 'body' => $html); + } + } + } catch (Exception $e) { + return array('exit' => true, 'body' => '{msg:errNetMountNoDriver}'); + } + + if ($_aToken = $this->session->get('BoxTokens')) { + $options['accessToken'] = json_encode($_aToken); + if ($this->options['path'] === 'root' || !$this->options['path']) { + $this->options['path'] = '/'; + } + } else { + $this->session->remove('BoxTokens'); + $this->setError(elFinder::ERROR_NETMOUNT, $options['host'], implode(' ', $this->error())); + + return array('exit' => true, 'error' => $this->error()); + } + + $this->session->remove('nodeId'); + unset($options['user'], $options['pass'], $options['id']); + + return $options; + } + + /** + * process of on netunmount + * Drop `box` & rm thumbs. + * + * @param $netVolumes + * @param $key + * + * @return bool + */ + public function netunmount($netVolumes, $key) + { + if ($tmbs = glob(rtrim($this->options['tmbPath'], '\\/') . DIRECTORY_SEPARATOR . $this->tmbPrefix . '*.png')) { + foreach ($tmbs as $file) { + unlink($file); + } + } + + return true; + } + + /** + * Return debug info for client. + * + * @return array + **/ + public function debug() + { + $res = parent::debug(); + if (!empty($this->options['netkey']) && !empty($this->options['accessToken'])) { + $res['accessToken'] = $this->options['accessToken']; + } + + return $res; + } + + /*********************************************************************/ + /* INIT AND CONFIGURE */ + /*********************************************************************/ + + /** + * Prepare FTP connection + * Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn. + * + * @return bool + * @throws Exception + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + */ + protected function init() + { + if (!$this->options['accessToken']) { + return $this->setError('Required option `accessToken` is undefined.'); + } + + if (!empty($this->options['tmpPath'])) { + if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($this->options['tmpPath'])) { + $this->tmp = $this->options['tmpPath']; + } + } + + $error = false; + try { + $this->token = json_decode($this->options['accessToken']); + if (!is_object($this->token)) { + throw new Exception('Required option `accessToken` is invalid JSON.'); + } + + // make net mount key + if (empty($this->options['netkey'])) { + $this->netMountKey = $this->_bd_getInitialToken(); + } else { + $this->netMountKey = $this->options['netkey']; + } + + if ($this->aTokenFile = $this->_bd_getATokenFile()) { + if (empty($this->options['netkey'])) { + if ($this->aTokenFile) { + if (is_file($this->aTokenFile)) { + $this->token = json_decode(file_get_contents($this->aTokenFile)); + if (!is_object($this->token)) { + unlink($this->aTokenFile); + throw new Exception('Required option `accessToken` is invalid JSON.'); + } + } else { + file_put_contents($this->aTokenFile, json_encode($this->token), LOCK_EX); + } + } + } else if (is_file($this->aTokenFile)) { + // If the refresh token is the same as the permanent volume + $this->token = json_decode(file_get_contents($this->aTokenFile)); + } + } + + $this->needOnline && $this->_bd_refreshToken(); + } catch (Exception $e) { + $this->token = null; + $error = true; + $this->setError($e->getMessage()); + } + + if ($this->netMountKey) { + $this->tmbPrefix = 'box' . base_convert($this->netMountKey, 16, 32); + } + + if ($error) { + if (empty($this->options['netkey']) && $this->tmbPrefix) { + // for delete thumbnail + $this->netunmount(null, null); + } + return false; + } + + // normalize root path + if ($this->options['path'] == 'root') { + $this->options['path'] = '/'; + } + + $this->root = $this->options['path'] = $this->_normpath($this->options['path']); + + $this->options['root'] = ($this->options['root'] == '')? 'Box.com' : $this->options['root']; + + if (empty($this->options['alias'])) { + if ($this->needOnline) { + list(, $itemId) = $this->_bd_splitPath($this->options['path']); + $this->options['alias'] = ($this->options['path'] === '/') ? $this->options['root'] : + $this->_bd_query($itemId, $fetch_self = true)->name . '@Box'; + if (!empty($this->options['netkey'])) { + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']); + } + } else { + $this->options['alias'] = $this->options['root']; + } + } + + $this->rootName = $this->options['alias']; + + // This driver dose not support `syncChkAsTs` + $this->options['syncChkAsTs'] = false; + + // 'lsPlSleep' minmum 10 sec + $this->options['lsPlSleep'] = max(10, $this->options['lsPlSleep']); + + // enable command archive + $this->options['useRemoteArchive'] = true; + + return true; + } + + /** + * Configure after successfull mount. + * + * @author Dmitry (dio) Levashov + * @throws elFinderAbortException + */ + protected function configure() + { + parent::configure(); + + // fallback of $this->tmp + if (!$this->tmp && $this->tmbPathWritable) { + $this->tmp = $this->tmbPath; + } + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /** + * Close opened connection. + * + * @author Dmitry (dio) Levashov + **/ + public function umount() + { + } + + /** + * Return fileinfo based on filename + * For item ID based path file system + * Please override if needed on each drivers. + * + * @param string $path file cache + * + * @return array|boolean + * @throws elFinderAbortException + */ + protected function isNameExists($path) + { + list(, $name, $parent) = $this->_bd_splitPath($path); + + // We can not use it because the search of Box.com there is a time lag. + // ref. https://docs.box.com/reference#searching-for-content + // > Note: If an item is added to Box then it becomes accessible through the search endpoint after ten minutes. + + /*** + * $url = self::API_URL.'/search?limit=1&offset=0&content_types=name&ancestor_folder_ids='.rawurlencode($pid) + * .'&query='.rawurlencode('"'.$name.'"') + * .'fields='.self::FETCHFIELDS; + * $raw = $this->_bd_fetch($url); + * if (is_array($raw) && count($raw)) { + * return $this->_bd_parseRaw($raw); + * } + ***/ + + $phash = $this->encode($parent); + + // do not recursive search + $searchExDirReg = $this->options['searchExDirReg']; + $this->options['searchExDirReg'] = '/.*/'; + $search = $this->search($name, array(), $phash); + $this->options['searchExDirReg'] = $searchExDirReg; + + if ($search) { + $f = false; + foreach($search as $f) { + if ($f['name'] !== $name) { + $f = false; + } + if ($f) { + break; + } + } + return $f; + } + + return false; + } + + /** + * Cache dir contents. + * + * @param string $path dir path + * + * @return + * @throws Exception + * @author Dmitry Levashov + */ + protected function cacheDir($path) + { + $this->dirsCache[$path] = array(); + $hasDir = false; + + if ($path == '/') { + $items = $this->_bd_query('0', $fetch_self = true); // get root directory with folder & files + $itemId = $items->id; + } else { + list(, $itemId) = $this->_bd_splitPath($path); + } + + $res = $this->_bd_query($itemId); + + if ($res) { + foreach ($res as $raw) { + if ($stat = $this->_bd_parseRaw($raw)) { + $itemPath = $this->_joinPath($path, $raw->id); + $stat = $this->updateCache($itemPath, $stat); + if (empty($stat['hidden'])) { + if (!$hasDir && $stat['mime'] === 'directory') { + $hasDir = true; + } + $this->dirsCache[$path][] = $itemPath; + } + } + } + } + + if (isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'][$path] = $hasDir; + } + + return $this->dirsCache[$path]; + } + + /** + * Copy file/recursive copy dir only in current volume. + * Return new file path or false. + * + * @param string $src source path + * @param string $dst destination dir path + * @param string $name new file name (optionaly) + * + * @return string|false + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + **/ + protected function copy($src, $dst, $name) + { + if ($res = $this->_copy($src, $dst, $name)) { + $this->added[] = $this->stat($res); + return $res; + } else { + return $this->setError(elFinder::ERROR_COPY, $this->_path($src)); + } + } + + /** + * Remove file/ recursive remove dir. + * + * @param string $path file path + * @param bool $force try to remove even if file locked + * + * @return bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function remove($path, $force = false) + { + $stat = $this->stat($path); + $stat['realpath'] = $path; + $this->rmTmb($stat); + $this->clearcache(); + + if (empty($stat)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path), elFinder::ERROR_FILE_NOT_FOUND); + } + + if (!$force && !empty($stat['locked'])) { + return $this->setError(elFinder::ERROR_LOCKED, $this->_path($path)); + } + + if ($stat['mime'] == 'directory') { + if (!$this->_rmdir($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } else { + if (!$this->_unlink($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } + + $this->removed[] = $stat; + + return true; + } + + /** + * Create thumnbnail and return it's URL on success. + * + * @param string $path file path + * @param $stat + * + * @return string|false + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function createTmb($path, $stat) + { + if (!$stat || !$this->canCreateTmb($path, $stat)) { + return false; + } + + $name = $this->tmbname($stat); + $tmb = $this->tmbPath . DIRECTORY_SEPARATOR . $name; + + // copy image into tmbPath so some drivers does not store files on local fs + if (!$data = $this->_bd_getThumbnail($path)) { + // try get full contents as fallback + if (!$data = $this->_getContents($path)) { + return false; + } + } + if (!file_put_contents($tmb, $data)) { + return false; + } + + $tmbSize = $this->tmbSize; + + if (($s = getimagesize($tmb)) == false) { + return false; + } + + $result = true; + /* If image smaller or equal thumbnail size - just fitting to thumbnail square */ + if ($s[0] <= $tmbSize && $s[1] <= $tmbSize) { + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } else { + if ($this->options['tmbCrop']) { + + /* Resize and crop if image bigger than thumbnail */ + if (!(($s[0] > $tmbSize && $s[1] <= $tmbSize) || ($s[0] <= $tmbSize && $s[1] > $tmbSize)) || ($s[0] > $tmbSize && $s[1] > $tmbSize)) { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, false, 'png'); + } + + if ($result && ($s = getimagesize($tmb)) != false) { + $x = $s[0] > $tmbSize ? intval(($s[0] - $tmbSize) / 2) : 0; + $y = $s[1] > $tmbSize ? intval(($s[1] - $tmbSize) / 2) : 0; + $result = $this->imgCrop($tmb, $tmbSize, $tmbSize, $x, $y, 'png'); + } + } else { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, true, 'png'); + } + + if ($result) { + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } + } + + if (!$result) { + unlink($tmb); + + return false; + } + + return $name; + } + + /** + * Return thumbnail file name for required file. + * + * @param array $stat file stat + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function tmbname($stat) + { + return $this->tmbPrefix . $stat['rev'] . $stat['ts'] . '.png'; + } + + /** + * Return content URL. + * + * @param object $raw data + * + * @return string + * @author Naoki Sawada + **/ + protected function getSharedWebContentLink($raw) + { + if ($raw->shared_link->url) { + return sprintf('https://app.box.com/index.php?rm=box_download_shared_file&shared_name=%s&file_id=f_%s', basename($raw->shared_link->url), $raw->id); + } elseif ($raw->shared_link->download_url) { + return $raw->shared_link->download_url; + } + + return false; + } + + /** + * Return content URL. + * + * @param string $hash file hash + * @param array $options options + * + * @return string + * @throws Exception + * @author Naoki Sawada + */ + public function getContentUrl($hash, $options = array()) + { + if (!empty($options['onetime']) && $this->options['onetimeUrl']) { + return parent::getContentUrl($hash, $options); + } + if (!empty($options['temporary'])) { + // try make temporary file + $url = parent::getContentUrl($hash, $options); + if ($url) { + return $url; + } + } + if (($file = $this->file($hash)) == false || !$file['url'] || $file['url'] == 1) { + $path = $this->decode($hash); + + list(, $itemId) = $this->_bd_splitPath($path); + $params['shared_link']['access'] = 'open'; //open|company|collaborators + + $url = self::API_URL . '/files/' . $itemId; + + $curl = $this->_bd_prepareCurl(array( + CURLOPT_URL => $url, + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => json_encode($params), + )); + $res = $this->_bd_curlExec($curl, true, array( + // The data is sent as JSON as per Box documentation. + 'Content-Type: application/json', + )); + + if ($url = $this->getSharedWebContentLink($res)) { + return $url; + } + } + + return ''; + } + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dirname($path) + { + list(, , $dirname) = $this->_bd_splitPath($path); + + return $dirname; + } + + /** + * Return file name. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _basename($path) + { + list(, $basename) = $this->_bd_splitPath($path); + + return $basename; + } + + /** + * Join dir name and file name and retur full path. + * + * @param string $dir + * @param string $name + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _joinPath($dir, $name) + { + if (strval($dir) === '0') { + $dir = ''; + } + + return $this->_normpath($dir . '/' . $name); + } + + /** + * Return normalized path, this works the same as os.path.normpath() in Python. + * + * @param string $path path + * + * @return string + * @author Troex Nevelin + **/ + protected function _normpath($path) + { + if (DIRECTORY_SEPARATOR !== '/') { + $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + } + $path = '/' . ltrim($path, '/'); + + return $path; + } + + /** + * Return file path related to root dir. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _relpath($path) + { + return $path; + } + + /** + * Convert path related to root dir into real path. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _abspath($path) + { + return $path; + } + + /** + * Return fake path started from root dir. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _path($path) + { + return $this->rootName . $this->_normpath(substr($path, strlen($this->root))); + } + + /** + * Return true if $path is children of $parent. + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _inpath($path, $parent) + { + return $path == $parent || strpos($path, $parent . '/') === 0; + } + + /***************** file stat ********************/ + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally. + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * + * @return array|false + * @throws Exception + * @author Dmitry (dio) Levashov + */ + protected function _stat($path) + { + if ($raw = $this->_bd_getRawItem($path)) { + return $this->_bd_parseRaw($raw); + } + + return false; + } + + /** + * Return true if path is dir and has at least one childs directory. + * + * @param string $path dir path + * + * @return bool + * @throws Exception + * @author Dmitry (dio) Levashov + */ + protected function _subdirs($path) + { + list(, $itemId) = $this->_bd_splitPath($path); + + $path = '/folders/' . $itemId . '/items?limit=1&offset=0&fields=' . self::FETCHFIELDS; + + $url = self::API_URL . $path; + + if ($res = $this->_bd_fetch($url)) { + if ($res[0]->type == 'folder') { + return true; + } + } + + return false; + } + + /** + * Return object width and height + * Ususaly used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * + * @return string + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function _dimensions($path, $mime) + { + if (strpos($mime, 'image') !== 0) { + return ''; + } + + $ret = ''; + if ($work = $this->getWorkFile($path)) { + if ($size = @getimagesize($work)) { + $cache['width'] = $size[0]; + $cache['height'] = $size[1]; + $ret = array('dim' => $size[0] . 'x' . $size[1]); + $srcfp = fopen($work, 'rb'); + $target = isset(elFinder::$currentArgs['target'])? elFinder::$currentArgs['target'] : ''; + if ($subImgLink = $this->getSubstituteImgLink($target, $size, $srcfp)) { + $ret['url'] = $subImgLink; + } + } + } + is_file($work) && @unlink($work); + + return $ret; + } + + /******************** file/dir content *********************/ + + /** + * Return files list in directory. + * + * @param string $path dir path + * + * @return array + * @throws Exception + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + */ + protected function _scandir($path) + { + return isset($this->dirsCache[$path]) + ? $this->dirsCache[$path] + : $this->cacheDir($path); + } + + /** + * Open file and return file pointer. + * + * @param string $path file path + * @param string $mode + * + * @return resource|false + * @author Dmitry (dio) Levashov + */ + protected function _fopen($path, $mode = 'rb') + { + if ($mode === 'rb' || $mode === 'r') { + list(, $itemId) = $this->_bd_splitPath($path); + $data = array( + 'target' => self::API_URL . '/files/' . $itemId . '/content', + 'headers' => array('Authorization: Bearer ' . $this->token->data->access_token), + ); + + // to support range request + if (func_num_args() > 2) { + $opts = func_get_arg(2); + } else { + $opts = array(); + } + if (!empty($opts['httpheaders'])) { + $data['headers'] = array_merge($opts['httpheaders'], $data['headers']); + } + + return elFinder::getStreamByUrl($data); + } + + return false; + } + + /** + * Close opened file. + * + * @param resource $fp file pointer + * @param string $path + * + * @return void + * @author Dmitry (dio) Levashov + */ + protected function _fclose($fp, $path = '') + { + is_resource($fp) && fclose($fp); + if ($path) { + unlink($this->getTempFile($path)); + } + } + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed. + * + * @param string $path parent dir path + * @param string $name new directory name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkdir($path, $name) + { + try { + list(, $parentId) = $this->_bd_splitPath($path); + $params = array('name' => $name, 'parent' => array('id' => $parentId)); + + $url = self::API_URL . '/folders'; + + $curl = $this->_bd_prepareCurl(array( + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($params), + )); + + //create the Folder in the Parent + $folder = $this->_bd_curlExec($curl, $path); + + return $this->_joinPath($path, $folder->id); + } catch (Exception $e) { + return $this->setError('Box error: ' . $e->getMessage()); + } + } + + /** + * Create file and return it's path or false on failed. + * + * @param string $path parent dir path + * @param string $name new file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkfile($path, $name) + { + return $this->_save($this->tmpfile(), $path, $name, array()); + } + + /** + * Create symlink. FTP driver does not support symlinks. + * + * @param string $target link target + * @param string $path symlink path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _symlink($target, $path, $name) + { + return false; + } + + /** + * Copy file into another file. + * + * @param string $source source file path + * @param string $targetDir target directory path + * @param string $name new file name + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function _copy($source, $targetDir, $name) + { + try { + //Set the Parent id + list(, $parentId) = $this->_bd_splitPath($targetDir); + list(, $srcId) = $this->_bd_splitPath($source); + + $srcItem = $this->_bd_getRawItem($source); + + $properties = array('name' => $name, 'parent' => array('id' => $parentId)); + $data = (object)$properties; + + $type = ($srcItem->type === 'folder') ? 'folders' : 'files'; + $url = self::API_URL . '/' . $type . '/' . $srcId . '/copy'; + + $curl = $this->_bd_prepareCurl(array( + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($data), + )); + + //copy File in the Parent + $result = $this->_bd_curlExec($curl, $targetDir); + + if (isset($result->id)) { + if ($type === 'folders' && isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'][$targetDir] = true; + } + + return $this->_joinPath($targetDir, $result->id); + } + + return false; + } catch (Exception $e) { + return $this->setError('Box error: ' . $e->getMessage()); + } + } + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param string $target target dir path + * @param string $name file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _move($source, $targetDir, $name) + { + try { + //moving and renaming a file or directory + //Set new Parent and remove old parent + list(, $parentId) = $this->_bd_splitPath($targetDir); + list(, $itemId) = $this->_bd_splitPath($source); + + $srcItem = $this->_bd_getRawItem($source); + + //rename or move file or folder in destination target + $properties = array('name' => $name, 'parent' => array('id' => $parentId)); + + $type = ($srcItem->type === 'folder') ? 'folders' : 'files'; + $url = self::API_URL . '/' . $type . '/' . $itemId; + $data = (object)$properties; + + $curl = $this->_bd_prepareCurl(array( + CURLOPT_URL => $url, + CURLOPT_CUSTOMREQUEST => 'PUT', + CURLOPT_POSTFIELDS => json_encode($data), + )); + + $result = $this->_bd_curlExec($curl, $targetDir, array( + // The data is sent as JSON as per Box documentation. + 'Content-Type: application/json', + )); + + if ($result && isset($result->id)) { + return $this->_joinPath($targetDir, $result->id); + } + + return false; + } catch (Exception $e) { + return $this->setError('Box error: ' . $e->getMessage()); + } + } + + /** + * Remove file. + * + * @param string $path file path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _unlink($path) + { + return $this->_bd_unlink($path, 'files'); + } + + /** + * Remove dir. + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _rmdir($path) + { + return $this->_bd_unlink($path, 'folders'); + } + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param string $dir target dir path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Dmitry (dio) Levashov + **/ + protected function _save($fp, $path, $name, $stat) + { + $itemId = ''; + if ($name === '') { + list($parentId, $itemId, $parent) = $this->_bd_splitPath($path); + } else { + if ($stat) { + if (isset($stat['name'])) { + $name = $stat['name']; + } + if (isset($stat['rev']) && strpos($stat['hash'], $this->id) === 0) { + $itemId = $stat['rev']; + } + } + list(, $parentId) = $this->_bd_splitPath($path); + $parent = $path; + } + + try { + //Create or Update a file + $metaDatas = stream_get_meta_data($fp); + $tmpFilePath = isset($metaDatas['uri']) ? $metaDatas['uri'] : ''; + // remote contents + if (!$tmpFilePath || empty($metaDatas['seekable'])) { + $tmpHandle = $this->tmpfile(); + stream_copy_to_stream($fp, $tmpHandle); + $metaDatas = stream_get_meta_data($tmpHandle); + $tmpFilePath = $metaDatas['uri']; + } + + if ($itemId === '') { + //upload or create new file in destination target + $properties = array('name' => $name, 'parent' => array('id' => $parentId)); + $url = self::UPLOAD_URL . '/files/content'; + } else { + //update existing file in destination target + $properties = array('name' => $name); + $url = self::UPLOAD_URL . '/files/' . $itemId . '/content'; + } + + if (class_exists('CURLFile')) { + $cfile = new CURLFile($tmpFilePath); + } else { + $cfile = '@' . $tmpFilePath; + } + $params = array('attributes' => json_encode($properties), 'file' => $cfile); + $curl = $this->_bd_prepareCurl(array( + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $params, + )); + + $file = $this->_bd_curlExec($curl, $parent); + + return $this->_joinPath($parent, $file->entries[0]->id); + } catch (Exception $e) { + return $this->setError('Box error: ' . $e->getMessage()); + } + } + + /** + * Get file contents. + * + * @param string $path file path + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function _getContents($path) + { + try { + list(, $itemId) = $this->_bd_splitPath($path); + $url = self::API_URL . '/files/' . $itemId . '/content'; + + $contents = $this->_bd_fetch($url, true); + } catch (Exception $e) { + return $this->setError('Box error: ' . $e->getMessage()); + } + + return $contents; + } + + /** + * Write a string to a file. + * + * @param string $path file path + * @param string $content new file content + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _filePutContents($path, $content) + { + $res = false; + + if ($local = $this->getTempFile($path)) { + if (file_put_contents($local, $content, LOCK_EX) !== false + && ($fp = fopen($local, 'rb'))) { + clearstatcache(); + $res = $this->_save($fp, $path, '', array()); + fclose($fp); + } + file_exists($local) && unlink($local); + } + + return $res; + } + + /** + * Detect available archivers. + **/ + protected function _checkArchivers() + { + // die('Not yet implemented. (_checkArchivers)'); + return array(); + } + + /** + * chmod implementation. + * + * @return bool + **/ + protected function _chmod($path, $mode) + { + return false; + } + + /** + * Extract files from archive. + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return true + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _extract($path, $arc) + { + die('Not yet implemented. (_extract)'); + } + + /** + * Create archive and return its path. + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _archive($dir, $files, $name, $arc) + { + die('Not yet implemented. (_archive)'); + } +} // END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDriver.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDriver.class.php new file mode 100644 index 0000000..4fe8583 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDriver.class.php @@ -0,0 +1,7620 @@ + array(), + 'extract' => array() + ); + + /** + * Static var of $this->options['maxArcFilesSize'] + * + * @var int|string + */ + protected static $maxArcFilesSize; + + /** + * Server character encoding + * + * @var string or null + **/ + protected $encoding = null; + + /** + * How many subdirs levels return for tree + * + * @var int + **/ + protected $treeDeep = 1; + + /** + * Errors from last failed action + * + * @var array + **/ + protected $error = array(); + + /** + * Today 24:00 timestamp + * + * @var int + **/ + protected $today = 0; + + /** + * Yesterday 24:00 timestamp + * + * @var int + **/ + protected $yesterday = 0; + + /** + * Force make dirctory on extract + * + * @var int + **/ + protected $extractToNewdir = 'auto'; + + /** + * Object configuration + * + * @var array + **/ + protected $options = array( + // Driver ID (Prefix of volume ID), Normally, the value specified for each volume driver is used. + 'driverId' => '', + // Id (Suffix of volume ID), Normally, the number incremented according to the specified number of volumes is used. + 'id' => '', + // revision id of root directory that uses for caching control of root stat + 'rootRev' => '', + // driver type it uses volume root's CSS class name. e.g. 'group' -> Adds 'elfinder-group' to CSS class name. + 'type' => '', + // root directory path + 'path' => '', + // Folder hash value on elFinder to be the parent of this volume + 'phash' => '', + // Folder hash value on elFinder to trash bin of this volume, it require 'copyJoin' to true + 'trashHash' => '', + // open this path on initial request instead of root path + 'startPath' => '', + // how many subdirs levels return per request + 'treeDeep' => 1, + // root url, not set to URL via the connector. If you want to hide the file URL, do not set this value. (replacement for old "fileURL" option) + 'URL' => '', + // enable onetime URL to a file - (true, false, 'auto' (true if a temporary directory is available) or callable (A function that return onetime URL)) + 'onetimeUrl' => 'auto', + // directory link url to own manager url with folder hash (`true`, `false`, `'hide'`(No show) or default `'auto'`: URL is empty then `true` else `false`) + 'dirUrlOwn' => 'auto', + // directory separator. required by client to show paths correctly + 'separator' => DIRECTORY_SEPARATOR, + // Use '/' as directory separator when the path hash encode/decode on the Windows server too + 'winHashFix' => false, + // Server character encoding (default is '': UTF-8) + 'encoding' => '', + // for convert character encoding (default is '': Not change locale) + 'locale' => '', + // URL of volume icon image + 'icon' => '', + // CSS Class of volume root in tree + 'rootCssClass' => '', + // Items to disable session caching + 'noSessionCache' => array(), + // enable i18n folder name that convert name to elFinderInstance.messages['folder_'+name] + 'i18nFolderName' => false, + // Search timeout (sec) + 'searchTimeout' => 30, + // Search exclusion directory regex pattern (require demiliter e.g. '#/path/to/exclude_directory#i') + 'searchExDirReg' => '', + // library to crypt/uncrypt files names (not implemented) + 'cryptLib' => '', + // how to detect files mimetypes. (auto/internal/finfo/mime_content_type) + 'mimeDetect' => 'auto', + // mime.types file path (for mimeDetect==internal) + 'mimefile' => '', + // Static extension/MIME of general server side scripts to security issues + 'staticMineMap' => array( + 'php:*' => 'text/x-php', + 'pht:*' => 'text/x-php', + 'php3:*' => 'text/x-php', + 'php4:*' => 'text/x-php', + 'php5:*' => 'text/x-php', + 'php7:*' => 'text/x-php', + 'phtml:*' => 'text/x-php', + 'cgi:*' => 'text/x-httpd-cgi', + 'pl:*' => 'text/x-perl', + 'asp:*' => 'text/x-asap', + 'aspx:*' => 'text/x-asap', + 'py:*' => 'text/x-python', + 'rb:*' => 'text/x-ruby', + 'jsp:*' => 'text/x-jsp' + ), + // mime type normalize map : Array '[ext]:[detected mime type]' => '[normalized mime]' + 'mimeMap' => array( + 'md:application/x-genesis-rom' => 'text/x-markdown', + 'md:text/plain' => 'text/x-markdown', + 'markdown:text/plain' => 'text/x-markdown', + 'css:text/x-asm' => 'text/css', + 'css:text/plain' => 'text/css', + 'csv:text/plain' => 'text/csv', + 'java:text/x-c' => 'text/x-java-source', + 'json:text/plain' => 'application/json', + 'sql:text/plain' => 'text/x-sql', + 'rtf:text/rtf' => 'application/rtf', + 'rtfd:text/rtfd' => 'application/rtfd', + 'ico:image/vnd.microsoft.icon' => 'image/x-icon', + 'svg:text/plain' => 'image/svg+xml', + 'pxd:application/octet-stream' => 'image/x-pixlr-data', + 'dng:image/tiff' => 'image/x-adobe-dng', + 'sketch:application/zip' => 'image/x-sketch', + 'sketch:application/octet-stream' => 'image/x-sketch', + 'xcf:application/octet-stream' => 'image/x-xcf', + 'amr:application/octet-stream' => 'audio/amr', + 'm4a:video/mp4' => 'audio/mp4', + 'oga:application/ogg' => 'audio/ogg', + 'ogv:application/ogg' => 'video/ogg', + 'zip:application/x-zip' => 'application/zip', + 'm3u8:text/plain' => 'application/x-mpegURL', + 'mpd:text/plain' => 'application/dash+xml', + 'mpd:application/xml' => 'application/dash+xml', + '*:application/x-dosexec' => 'application/x-executable', + 'doc:application/vnd.ms-office' => 'application/msword', + 'xls:application/vnd.ms-office' => 'application/vnd.ms-excel', + 'ppt:application/vnd.ms-office' => 'application/vnd.ms-powerpoint', + 'yml:text/plain' => 'text/x-yaml', + 'ai:application/pdf' => 'application/postscript', + 'cgm:text/plain' => 'image/cgm', + 'dxf:text/plain' => 'image/vnd.dxf', + 'dds:application/octet-stream' => 'image/vnd-ms.dds', + 'hpgl:text/plain' => 'application/vnd.hp-hpgl', + 'igs:text/plain' => 'model/iges', + 'iges:text/plain' => 'model/iges', + 'plt:application/octet-stream' => 'application/plt', + 'plt:text/plain' => 'application/plt', + 'sat:text/plain' => 'application/sat', + 'step:text/plain' => 'application/step', + 'stp:text/plain' => 'application/step' + ), + // An option to add MimeMap to the `mimeMap` option + // Array '[ext]:[detected mime type]' => '[normalized mime]' + 'additionalMimeMap' => array(), + // MIME-Type of filetype detected as unknown + 'mimeTypeUnknown' => 'application/octet-stream', + // MIME regex of send HTTP header "Content-Disposition: inline" or allow preview in quicklook + // '.' is allow inline of all of MIME types + // '$^' is not allow inline of all of MIME types + 'dispInlineRegex' => '^(?:(?:video|audio)|image/(?!.+\+xml)|application/(?:ogg|x-mpegURL|dash\+xml)|(?:text/plain|application/pdf)$)', + // temporary content URL's base path + 'tmpLinkPath' => '', + // temporary content URL's base URL + 'tmpLinkUrl' => '', + // directory for thumbnails + 'tmbPath' => '.tmb', + // mode to create thumbnails dir + 'tmbPathMode' => 0777, + // thumbnails dir URL. Set it if store thumbnails outside root directory + 'tmbURL' => '', + // thumbnails size (px) + 'tmbSize' => 48, + // thumbnails crop (true - crop, false - scale image to fit thumbnail size) + 'tmbCrop' => true, + // thumbnail URL require custom data as the GET query + 'tmbReqCustomData' => false, + // thumbnails background color (hex #rrggbb or 'transparent') + 'tmbBgColor' => 'transparent', + // image rotate fallback background color (hex #rrggbb) + 'bgColorFb' => '#ffffff', + // image manipulations library (imagick|gd|convert|auto|none, none - Does not check the image library at all.) + 'imgLib' => 'auto', + // Fallback self image to thumbnail (nothing imgLib) + 'tmbFbSelf' => true, + // Video to Image converters ['TYPE or MIME' => ['func' => function($file){ /* Converts $file to Image */ return true; }, 'maxlen' => (int)TransferLength]] + 'imgConverter' => array(), + // Max length of transfer to image converter + 'tmbVideoConvLen' => 10000000, + // Captre point seccond + 'tmbVideoConvSec' => 6, + // Life time (hour) for thumbnail garbage collection ("0" means no GC) + 'tmbGcMaxlifeHour' => 0, + // Percentage of garbage collection executed for thumbnail creation command ("1" means "1%") + 'tmbGcPercentage' => 1, + // Resource path of fallback icon images defailt: php/resouces + 'resourcePath' => '', + // Jpeg image saveing quality + 'jpgQuality' => 100, + // Save as progressive JPEG on image editing + 'jpgProgressive' => true, + // enable to get substitute image with command `dim` + 'substituteImg' => true, + // on paste file - if true - old file will be replaced with new one, if false new file get name - original_name-number.ext + 'copyOverwrite' => true, + // if true - join new and old directories content on paste + 'copyJoin' => true, + // on upload - if true - old file will be replaced with new one, if false new file get name - original_name-number.ext + 'uploadOverwrite' => true, + // mimetypes allowed to upload + 'uploadAllow' => array(), + // mimetypes not allowed to upload + 'uploadDeny' => array(), + // order to process uploadAllow and uploadDeny options + 'uploadOrder' => array('deny', 'allow'), + // maximum upload file size. NOTE - this is size for every uploaded files + 'uploadMaxSize' => 0, + // maximum number of chunked upload connection. `-1` to disable chunked upload + 'uploadMaxConn' => 3, + // maximum get file size. NOTE - Maximum value is 50% of PHP memory_limit + 'getMaxSize' => 0, + // files dates format + 'dateFormat' => 'j M Y H:i', + // files time format + 'timeFormat' => 'H:i', + // if true - every folder will be check for children folders, -1 - every folder will be check asynchronously, false - all folders will be marked as having subfolders + 'checkSubfolders' => true, // true, false or -1 + // allow to copy from this volume to other ones? + 'copyFrom' => true, + // allow to copy from other volumes to this one? + 'copyTo' => true, + // cmd duplicate suffix format e.g. '_%s_' to without spaces + 'duplicateSuffix' => ' %s ', + // unique name numbar format e.g. '(%d)' to (1), (2)... + 'uniqueNumFormat' => '%d', + // list of commands disabled on this root + 'disabled' => array(), + // enable file owner, group & mode info, `false` to inactivate "chmod" command. + 'statOwner' => false, + // allow exec chmod of read-only files + 'allowChmodReadOnly' => false, + // regexp or function name to validate new file name + 'acceptedName' => '/^[^\.].*/', // Notice: overwritten it in some volume drivers contractor + // regexp or function name to validate new directory name + 'acceptedDirname' => '', // used `acceptedName` if empty value + // function/class method to control files permissions + 'accessControl' => null, + // some data required by access control + 'accessControlData' => null, + // root stat that return without asking the system when mounted and not the current volume. Query to the system with false. array|false + 'rapidRootStat' => array( + 'read' => true, + 'write' => true, + 'locked' => false, + 'hidden' => false, + 'size' => 0, // Unknown + 'ts' => 0, // Unknown + 'dirs' => -1, // Check on demand for subdirectories + 'mime' => 'directory' + ), + // default permissions. + 'defaults' => array( + 'read' => true, + 'write' => true, + 'locked' => false, + 'hidden' => false + ), + // files attributes + 'attributes' => array(), + // max allowed archive files size (0 - no limit) + 'maxArcFilesSize' => '2G', + // Allowed archive's mimetypes to create. Leave empty for all available types. + 'archiveMimes' => array(), + // Manual config for archivers. See example below. Leave empty for auto detect + 'archivers' => array(), + // Use Archive function for remote volume + 'useRemoteArchive' => false, + // plugin settings + 'plugin' => array(), + // Is support parent directory time stamp update on add|remove|rename item + // Default `null` is auto detection that is LocalFileSystem, FTP or Dropbox are `true` + 'syncChkAsTs' => null, + // Long pooling sync checker function for syncChkAsTs is true + // Calls with args (TARGET DIRCTORY PATH, STAND-BY(sec), OLD TIMESTAMP, VOLUME DRIVER INSTANCE, ELFINDER INSTANCE) + // This function must return the following values. Changed: New Timestamp or Same: Old Timestamp or Error: false + // Default `null` is try use elFinderVolumeLocalFileSystem::localFileSystemInotify() on LocalFileSystem driver + // another driver use elFinder stat() checker + 'syncCheckFunc' => null, + // Long polling sync stand-by time (sec) + 'plStandby' => 30, + // Sleep time (sec) for elFinder stat() checker (syncChkAsTs is true) + 'tsPlSleep' => 10, + // Sleep time (sec) for elFinder ls() checker (syncChkAsTs is false) + 'lsPlSleep' => 30, + // Client side sync interval minimum (ms) + // Default `null` is auto set to ('tsPlSleep' or 'lsPlSleep') * 1000 + // `0` to disable auto sync + 'syncMinMs' => null, + // required to fix bug on macos + // However, we recommend to use the Normalizer plugin instead this option + 'utf8fix' => false, + // й ё Й Ё Ø Å + 'utf8patterns' => array("\u0438\u0306", "\u0435\u0308", "\u0418\u0306", "\u0415\u0308", "\u00d8A", "\u030a"), + 'utf8replace' => array("\u0439", "\u0451", "\u0419", "\u0401", "\u00d8", "\u00c5"), + // cache control HTTP headers for commands `file` and `get` + 'cacheHeaders' => array( + 'Cache-Control: max-age=3600', + 'Expires:', + 'Pragma:' + ), + // Header to use to accelerate sending local files to clients (e.g. 'X-Sendfile', 'X-Accel-Redirect') + 'xsendfile' => '', + // Root path to xsendfile target. Probably, this is required for 'X-Accel-Redirect' on Nginx. + 'xsendfilePath' => '' + ); + + /** + * Defaults permissions + * + * @var array + **/ + protected $defaults = array( + 'read' => true, + 'write' => true, + 'locked' => false, + 'hidden' => false + ); + + /** + * Access control function/class + * + * @var mixed + **/ + protected $attributes = array(); + + /** + * Access control function/class + * + * @var mixed + **/ + protected $access = null; + + /** + * Mime types allowed to upload + * + * @var array + **/ + protected $uploadAllow = array(); + + /** + * Mime types denied to upload + * + * @var array + **/ + protected $uploadDeny = array(); + + /** + * Order to validate uploadAllow and uploadDeny + * + * @var array + **/ + protected $uploadOrder = array(); + + /** + * Maximum allowed upload file size. + * Set as number or string with unit - "10M", "500K", "1G" + * + * @var int|string + **/ + protected $uploadMaxSize = 0; + + /** + * Run time setting of overwrite items on upload + * + * @var string + */ + protected $uploadOverwrite = true; + + /** + * Maximum allowed get file size. + * Set as number or string with unit - "10M", "500K", "1G" + * + * @var int|string + **/ + protected $getMaxSize = -1; + + /** + * Mimetype detect method + * + * @var string + **/ + protected $mimeDetect = 'auto'; + + /** + * Flag - mimetypes from externail file was loaded + * + * @var bool + **/ + private static $mimetypesLoaded = false; + + /** + * Finfo resource for mimeDetect == 'finfo' + * + * @var resource + **/ + protected $finfo = null; + + /** + * List of disabled client's commands + * + * @var array + **/ + protected $disabled = array(); + + /** + * overwrite extensions/mimetypes to mime.types + * + * @var array + **/ + protected static $mimetypes = array( + // applications + 'exe' => 'application/x-executable', + 'jar' => 'application/x-jar', + // archives + 'gz' => 'application/x-gzip', + 'tgz' => 'application/x-gzip', + 'tbz' => 'application/x-bzip2', + 'rar' => 'application/x-rar', + // texts + 'php' => 'text/x-php', + 'js' => 'text/javascript', + 'rtfd' => 'application/rtfd', + 'py' => 'text/x-python', + 'rb' => 'text/x-ruby', + 'sh' => 'text/x-shellscript', + 'pl' => 'text/x-perl', + 'xml' => 'text/xml', + 'c' => 'text/x-csrc', + 'h' => 'text/x-chdr', + 'cpp' => 'text/x-c++src', + 'hh' => 'text/x-c++hdr', + 'md' => 'text/x-markdown', + 'markdown' => 'text/x-markdown', + 'yml' => 'text/x-yaml', + // images + 'bmp' => 'image/x-ms-bmp', + 'tga' => 'image/x-targa', + 'xbm' => 'image/xbm', + 'pxm' => 'image/pxm', + //audio + 'wav' => 'audio/wav', + // video + 'dv' => 'video/x-dv', + 'wm' => 'video/x-ms-wmv', + 'ogm' => 'video/ogg', + 'm2ts' => 'video/MP2T', + 'mts' => 'video/MP2T', + 'ts' => 'video/MP2T', + 'm3u8' => 'application/x-mpegURL', + 'mpd' => 'application/dash+xml' + ); + + /** + * Directory separator - required by client + * + * @var string + **/ + protected $separator = DIRECTORY_SEPARATOR; + + /** + * Directory separator for decode/encode hash + * + * @var string + **/ + protected $separatorForHash = ''; + + /** + * System Root path (Unix like: '/', Windows: '\', 'C:\' or 'D:\'...) + * + * @var string + **/ + protected $systemRoot = DIRECTORY_SEPARATOR; + + /** + * Mimetypes allowed to display + * + * @var array + **/ + protected $onlyMimes = array(); + + /** + * Store files moved or overwrited files info + * + * @var array + **/ + protected $removed = array(); + + /** + * Store files added files info + * + * @var array + **/ + protected $added = array(); + + /** + * Cache storage + * + * @var array + **/ + protected $cache = array(); + + /** + * Cache by folders + * + * @var array + **/ + protected $dirsCache = array(); + + /** + * You should use `$this->sessionCache['subdirs']` instead + * + * @var array + * @deprecated + */ + protected $subdirsCache = array(); + + /** + * This volume session cache + * + * @var array + */ + protected $sessionCache; + + /** + * Session caching item list + * + * @var array + */ + protected $sessionCaching = array('rootstat' => true, 'subdirs' => true); + + /** + * elFinder session wrapper object + * + * @var elFinderSessionInterface + */ + protected $session; + + /** + * Search start time + * + * @var int + */ + protected $searchStart; + + /** + * Current query word on doSearch + * + * @var array + **/ + protected $doSearchCurrentQuery = array(); + + /** + * Is root modified (for clear root stat cache) + * + * @var bool + */ + protected $rootModified = false; + + /** + * Is disable of command `url` + * + * @var string + */ + protected $disabledGetUrl = false; + + /** + * Accepted filename validator + * + * @var string | callable + */ + protected $nameValidator; + + /** + * Accepted dirname validator + * + * @var string | callable + */ + protected $dirnameValidator; + + /** + * This request require online state + * + * @var boolean + */ + protected $needOnline; + + /*********************************************************************/ + /* INITIALIZATION */ + /*********************************************************************/ + + /** + * Sets the need online. + * + * @param boolean $state The state + */ + public function setNeedOnline($state = null) + { + if ($state !== null) { + $this->needOnline = (bool)$state; + return; + } + + $need = false; + $arg = $this->ARGS; + $id = $this->id; + + $target = !empty($arg['target'])? $arg['target'] : (!empty($arg['dst'])? $arg['dst'] : ''); + $targets = !empty($arg['targets'])? $arg['targets'] : array(); + if (!is_array($targets)) { + $targets = array($targets); + } + + if ($target && strpos($target, $id) === 0) { + $need = true; + } else if ($targets) { + foreach($targets as $t) { + if ($t && strpos($t, $id) === 0) { + $need = true; + break; + } + } + } + + $this->needOnline = $need; + } + + /** + * Prepare driver before mount volume. + * Return true if volume is ready. + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function init() + { + return true; + } + + /** + * Configure after successfull mount. + * By default set thumbnails path and image manipulation library. + * + * @return void + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function configure() + { + // set thumbnails path + $path = $this->options['tmbPath']; + if ($path) { + if (!file_exists($path)) { + if (mkdir($path)) { + chmod($path, $this->options['tmbPathMode']); + } else { + $path = ''; + } + } + + if (is_dir($path) && is_readable($path)) { + $this->tmbPath = $path; + $this->tmbPathWritable = is_writable($path); + } + } + // set resouce path + if (!is_dir($this->options['resourcePath'])) { + $this->options['resourcePath'] = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'resources'; + } + + // set image manipulation library + $type = preg_match('/^(imagick|gd|convert|auto|none)$/i', $this->options['imgLib']) + ? strtolower($this->options['imgLib']) + : 'auto'; + + if ($type === 'none') { + $this->imgLib = ''; + } else { + if (($type === 'imagick' || $type === 'auto') && extension_loaded('imagick')) { + $this->imgLib = 'imagick'; + } else if (($type === 'gd' || $type === 'auto') && function_exists('gd_info')) { + $this->imgLib = 'gd'; + } else { + $convertCache = 'imgLibConvert'; + if (($convertCmd = $this->session->get($convertCache, false)) !== false) { + $this->imgLib = $convertCmd; + } else { + $this->imgLib = ($this->procExec(ELFINDER_CONVERT_PATH . ' -version') === 0) ? 'convert' : ''; + $this->session->set($convertCache, $this->imgLib); + } + } + if ($type !== 'auto' && $this->imgLib === '') { + // fallback + $this->imgLib = extension_loaded('imagick') ? 'imagick' : (function_exists('gd_info') ? 'gd' : ''); + } + } + + // check video to img converter + if (!empty($this->options['imgConverter']) && is_array($this->options['imgConverter'])) { + foreach ($this->options['imgConverter'] as $_type => $_converter) { + if (isset($_converter['func'])) { + $this->imgConverter[strtolower($_type)] = $_converter; + } + } + } + if (!isset($this->imgConverter['video'])) { + $videoLibCache = 'videoLib'; + if (($videoLibCmd = $this->session->get($videoLibCache, false)) === false) { + $videoLibCmd = ($this->procExec(ELFINDER_FFMPEG_PATH . ' -version') === 0) ? 'ffmpeg' : ''; + $this->session->set($videoLibCache, $videoLibCmd); + } + if ($videoLibCmd) { + $this->imgConverter['video'] = array( + 'func' => array($this, $videoLibCmd . 'ToImg'), + 'maxlen' => $this->options['tmbVideoConvLen'] + ); + } + } + + // check onetimeUrl + if (strtolower($this->options['onetimeUrl']) === 'auto') { + $this->options['onetimeUrl'] = elFinder::getStaticVar('commonTempPath')? true : false; + } + + // check archivers + if (empty($this->archivers['create'])) { + $this->disabled[] = 'archive'; + } + if (empty($this->archivers['extract'])) { + $this->disabled[] = 'extract'; + } + $_arc = $this->getArchivers(); + if (empty($_arc['create'])) { + $this->disabled[] = 'zipdl'; + } + + if ($this->options['maxArcFilesSize']) { + $this->options['maxArcFilesSize'] = elFinder::getIniBytes('', $this->options['maxArcFilesSize']); + } + self::$maxArcFilesSize = $this->options['maxArcFilesSize']; + + // check 'statOwner' for command `chmod` + if (empty($this->options['statOwner'])) { + $this->disabled[] = 'chmod'; + } + + // check 'mimeMap' + if (!is_array($this->options['mimeMap'])) { + $this->options['mimeMap'] = array(); + } + if (is_array($this->options['staticMineMap']) && $this->options['staticMineMap']) { + $this->options['mimeMap'] = array_merge($this->options['mimeMap'], $this->options['staticMineMap']); + } + if (is_array($this->options['additionalMimeMap']) && $this->options['additionalMimeMap']) { + $this->options['mimeMap'] = array_merge($this->options['mimeMap'], $this->options['additionalMimeMap']); + } + + // check 'url' in disabled commands + if (in_array('url', $this->disabled)) { + $this->disabledGetUrl = true; + } + + // set run time setting uploadOverwrite + $this->uploadOverwrite = $this->options['uploadOverwrite']; + } + + /** + * @deprecated + */ + protected function sessionRestart() + { + $this->sessionCache = $this->session->start()->get($this->id, array()); + return true; + } + + /*********************************************************************/ + /* PUBLIC API */ + /*********************************************************************/ + + /** + * Return driver id. Used as a part of volume id. + * + * @return string + * @author Dmitry (dio) Levashov + **/ + public function driverId() + { + return $this->driverId; + } + + /** + * Return volume id + * + * @return string + * @author Dmitry (dio) Levashov + **/ + public function id() + { + return $this->id; + } + + /** + * Assign elFinder session wrapper object + * + * @param $session elFinderSessionInterface + */ + public function setSession($session) + { + $this->session = $session; + } + + /** + * Get elFinder sesson wrapper object + * + * @return object The session object + */ + public function getSession() + { + return $this->session; + } + + /** + * Save session cache data + * Calls this function before umount this volume on elFinder::exec() + * + * @return void + */ + public function saveSessionCache() + { + $this->session->set($this->id, $this->sessionCache); + } + + /** + * Return debug info for client + * + * @return array + * @author Dmitry (dio) Levashov + **/ + public function debug() + { + return array( + 'id' => $this->id(), + 'name' => strtolower(substr(get_class($this), strlen('elfinderdriver'))), + 'mimeDetect' => $this->mimeDetect, + 'imgLib' => $this->imgLib + ); + } + + /** + * chmod a file or folder + * + * @param string $hash file or folder hash to chmod + * @param string $mode octal string representing new permissions + * + * @return array|false + * @author David Bartle + **/ + public function chmod($hash, $mode) + { + if ($this->commandDisabled('chmod')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if (!($file = $this->file($hash))) { + return $this->setError(elFinder::ERROR_FILE_NOT_FOUND); + } + + if (!$this->options['allowChmodReadOnly']) { + if (!$this->attr($this->decode($hash), 'write', null, ($file['mime'] === 'directory'))) { + return $this->setError(elFinder::ERROR_PERM_DENIED, $file['name']); + } + } + + $path = $this->decode($hash); + $write = $file['write']; + + if ($this->convEncOut(!$this->_chmod($this->convEncIn($path), $mode))) { + return $this->setError(elFinder::ERROR_PERM_DENIED, $file['name']); + } + + $this->clearstatcache(); + if ($path == $this->root) { + $this->rootModified = true; + } + + if ($file = $this->stat($path)) { + $files = array($file); + if ($file['mime'] === 'directory' && $write !== $file['write']) { + foreach ($this->getScandir($path) as $stat) { + if ($this->mimeAccepted($stat['mime'])) { + $files[] = $stat; + } + } + } + return $files; + } else { + return $this->setError(elFinder::ERROR_FILE_NOT_FOUND); + } + } + + /** + * stat a file or folder for elFinder cmd exec + * + * @param string $hash file or folder hash to chmod + * + * @return array + * @author Naoki Sawada + **/ + public function fstat($hash) + { + $path = $this->decode($hash); + return $this->stat($path); + } + + /** + * Clear PHP stat cache & all of inner stat caches + */ + public function clearstatcache() + { + clearstatcache(); + $this->clearcache(); + } + + /** + * Clear inner stat caches for target hash + * + * @param string $hash + */ + public function clearcaches($hash = null) + { + if ($hash === null) { + $this->clearcache(); + } else { + $path = $this->decode($hash); + unset($this->cache[$path], $this->dirsCache[$path]); + } + } + + /** + * "Mount" volume. + * Return true if volume available for read or write, + * false - otherwise + * + * @param array $opts + * + * @return bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + */ + public function mount(array $opts) + { + $this->options = array_merge($this->options, $opts); + + if (!isset($this->options['path']) || $this->options['path'] === '') { + return $this->setError('Path undefined.'); + } + + if (!$this->session) { + return $this->setError('Session wrapper dose not set. Need to `$volume->setSession(elFinderSessionInterface);` before mount.'); + } + if (!($this->session instanceof elFinderSessionInterface)) { + return $this->setError('Session wrapper instance must be "elFinderSessionInterface".'); + } + + // set driverId + if (!empty($this->options['driverId'])) { + $this->driverId = $this->options['driverId']; + } + + $this->id = $this->driverId . (!empty($this->options['id']) ? $this->options['id'] : elFinder::$volumesCnt++) . '_'; + $this->root = $this->normpathCE($this->options['path']); + $this->separator = isset($this->options['separator']) ? $this->options['separator'] : DIRECTORY_SEPARATOR; + if (!empty($this->options['winHashFix'])) { + $this->separatorForHash = ($this->separator !== '/') ? '/' : ''; + } + $this->systemRoot = isset($this->options['systemRoot']) ? $this->options['systemRoot'] : $this->separator; + + // set ARGS + $this->ARGS = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET; + + $argInit = !empty($this->ARGS['init']); + + // set $this->needOnline + if (!is_bool($this->needOnline)) { + $this->setNeedOnline(); + } + + // session cache + if ($argInit) { + $this->session->set($this->id, array()); + } + $this->sessionCache = $this->session->get($this->id, array()); + + // default file attribute + $this->defaults = array( + 'read' => isset($this->options['defaults']['read']) ? !!$this->options['defaults']['read'] : true, + 'write' => isset($this->options['defaults']['write']) ? !!$this->options['defaults']['write'] : true, + 'locked' => isset($this->options['defaults']['locked']) ? !!$this->options['defaults']['locked'] : false, + 'hidden' => isset($this->options['defaults']['hidden']) ? !!$this->options['defaults']['hidden'] : false + ); + + // root attributes + $this->attributes[] = array( + 'pattern' => '~^' . preg_quote($this->separator) . '$~', + 'locked' => true, + 'hidden' => false + ); + // set files attributes + if (!empty($this->options['attributes']) && is_array($this->options['attributes'])) { + + foreach ($this->options['attributes'] as $a) { + // attributes must contain pattern and at least one rule + if (!empty($a['pattern']) || (is_array($a) && count($a) > 1)) { + $this->attributes[] = $a; + } + } + } + + if (!empty($this->options['accessControl']) && is_callable($this->options['accessControl'])) { + $this->access = $this->options['accessControl']; + } + + $this->today = mktime(0, 0, 0, date('m'), date('d'), date('Y')); + $this->yesterday = $this->today - 86400; + + if (!$this->init()) { + return false; + } + + // set server encoding + if (!empty($this->options['encoding']) && strtoupper($this->options['encoding']) !== 'UTF-8') { + $this->encoding = $this->options['encoding']; + } else { + $this->encoding = null; + } + + // check some options is arrays + $this->uploadAllow = isset($this->options['uploadAllow']) && is_array($this->options['uploadAllow']) + ? $this->options['uploadAllow'] + : array(); + + $this->uploadDeny = isset($this->options['uploadDeny']) && is_array($this->options['uploadDeny']) + ? $this->options['uploadDeny'] + : array(); + + $this->options['uiCmdMap'] = (isset($this->options['uiCmdMap']) && is_array($this->options['uiCmdMap'])) + ? $this->options['uiCmdMap'] + : array(); + + if (is_string($this->options['uploadOrder'])) { // telephat_mode on, compatibility with 1.x + $parts = explode(',', isset($this->options['uploadOrder']) ? $this->options['uploadOrder'] : 'deny,allow'); + $this->uploadOrder = array(trim($parts[0]), trim($parts[1])); + } else { // telephat_mode off + $this->uploadOrder = !empty($this->options['uploadOrder']) ? $this->options['uploadOrder'] : array('deny', 'allow'); + } + + if (!empty($this->options['uploadMaxSize'])) { + $this->uploadMaxSize = elFinder::getIniBytes('', $this->options['uploadMaxSize']); + } + // Set maximum to PHP_INT_MAX + if (!defined('PHP_INT_MAX')) { + define('PHP_INT_MAX', 2147483647); + } + if ($this->uploadMaxSize < 1 || $this->uploadMaxSize > PHP_INT_MAX) { + $this->uploadMaxSize = PHP_INT_MAX; + } + + // Set to get maximum size to 50% of memory_limit + $memLimit = elFinder::getIniBytes('memory_limit') / 2; + if ($memLimit > 0) { + $this->getMaxSize = empty($this->options['getMaxSize']) ? $memLimit : min($memLimit, elFinder::getIniBytes('', $this->options['getMaxSize'])); + } else { + $this->getMaxSize = -1; + } + + $this->disabled = isset($this->options['disabled']) && is_array($this->options['disabled']) + ? array_values(array_diff($this->options['disabled'], array('open'))) // 'open' is required + : array(); + + $this->cryptLib = $this->options['cryptLib']; + $this->mimeDetect = $this->options['mimeDetect']; + + // find available mimetype detect method + $type = strtolower($this->options['mimeDetect']); + $type = preg_match('/^(finfo|mime_content_type|internal|auto)$/i', $type) ? $type : 'auto'; + $regexp = '/text\/x\-(php|c\+\+)/'; + + if (($type == 'finfo' || $type == 'auto') + && class_exists('finfo', false)) { + $tmpFileInfo = explode(';', finfo_file(finfo_open(FILEINFO_MIME), __FILE__)); + } else { + $tmpFileInfo = false; + } + + $type = 'internal'; + if ($tmpFileInfo && preg_match($regexp, array_shift($tmpFileInfo))) { + $type = 'finfo'; + $this->finfo = finfo_open(FILEINFO_MIME); + } elseif (($type == 'mime_content_type' || $type == 'auto') && function_exists('mime_content_type')) { + $_mimetypes = explode(';', mime_content_type(__FILE__)); + if (preg_match($regexp, array_shift($_mimetypes))) { + $type = 'mime_content_type'; + } + } + $this->mimeDetect = $type; + + // load mimes from external file for mimeDetect == 'internal' + // based on Alexey Sukhotin idea and patch: http://elrte.org/redmine/issues/163 + // file must be in file directory or in parent one + if ($this->mimeDetect === 'internal' && !elFinderVolumeDriver::$mimetypesLoaded) { + elFinderVolumeDriver::loadMimeTypes(!empty($this->options['mimefile']) ? $this->options['mimefile'] : ''); + } + + $this->rootName = empty($this->options['alias']) ? $this->basenameCE($this->root) : $this->options['alias']; + + // This get's triggered if $this->root == '/' and alias is empty. + // Maybe modify _basename instead? + if ($this->rootName === '') $this->rootName = $this->separator; + + $this->_checkArchivers(); + + $root = $this->stat($this->root); + + if (!$root) { + return $this->setError('Root folder does not exist.'); + } + if (!$root['read'] && !$root['write']) { + return $this->setError('Root folder has not read and write permissions.'); + } + + if ($root['read']) { + if ($argInit) { + // check startPath - path to open by default instead of root + $startPath = $this->options['startPath'] ? $this->normpathCE($this->options['startPath']) : ''; + if ($startPath) { + $start = $this->stat($startPath); + if (!empty($start) + && $start['mime'] == 'directory' + && $start['read'] + && empty($start['hidden']) + && $this->inpathCE($startPath, $this->root)) { + $this->startPath = $startPath; + if (substr($this->startPath, -1, 1) == $this->options['separator']) { + $this->startPath = substr($this->startPath, 0, -1); + } + } + } + } + } else { + $this->options['URL'] = ''; + $this->options['tmbURL'] = ''; + $this->options['tmbPath'] = ''; + // read only volume + array_unshift($this->attributes, array( + 'pattern' => '/.*/', + 'read' => false + )); + } + $this->treeDeep = $this->options['treeDeep'] > 0 ? (int)$this->options['treeDeep'] : 1; + $this->tmbSize = $this->options['tmbSize'] > 0 ? (int)$this->options['tmbSize'] : 48; + $this->URL = $this->options['URL']; + if ($this->URL && preg_match("|[^/?&=]$|", $this->URL)) { + $this->URL .= '/'; + } + + $dirUrlOwn = strtolower($this->options['dirUrlOwn']); + if ($dirUrlOwn === 'auto') { + $this->options['dirUrlOwn'] = $this->URL ? false : true; + } else if ($dirUrlOwn === 'hide') { + $this->options['dirUrlOwn'] = 'hide'; + } else { + $this->options['dirUrlOwn'] = (bool)$this->options['dirUrlOwn']; + } + + $this->tmbURL = !empty($this->options['tmbURL']) ? $this->options['tmbURL'] : ''; + if ($this->tmbURL && $this->tmbURL !== 'self' && preg_match("|[^/?&=]$|", $this->tmbURL)) { + $this->tmbURL .= '/'; + } + + $this->nameValidator = !empty($this->options['acceptedName']) && (is_string($this->options['acceptedName']) || is_callable($this->options['acceptedName'])) + ? $this->options['acceptedName'] + : ''; + + $this->dirnameValidator = !empty($this->options['acceptedDirname']) && (is_callable($this->options['acceptedDirname']) || (is_string($this->options['acceptedDirname']) && preg_match($this->options['acceptedDirname'], '') !== false)) + ? $this->options['acceptedDirname'] + : $this->nameValidator; + + // enabling archivers['create'] with options['useRemoteArchive'] + if ($this->options['useRemoteArchive'] && empty($this->archivers['create']) && $this->getTempPath()) { + $_archivers = $this->getArchivers(); + $this->archivers['create'] = $_archivers['create']; + } + + // manual control archive types to create + if (!empty($this->options['archiveMimes']) && is_array($this->options['archiveMimes'])) { + foreach ($this->archivers['create'] as $mime => $v) { + if (!in_array($mime, $this->options['archiveMimes'])) { + unset($this->archivers['create'][$mime]); + } + } + } + + // manualy add archivers + if (!empty($this->options['archivers']['create']) && is_array($this->options['archivers']['create'])) { + foreach ($this->options['archivers']['create'] as $mime => $conf) { + if (strpos($mime, 'application/') === 0 + && !empty($conf['cmd']) + && isset($conf['argc']) + && !empty($conf['ext']) + && !isset($this->archivers['create'][$mime])) { + $this->archivers['create'][$mime] = $conf; + } + } + } + + if (!empty($this->options['archivers']['extract']) && is_array($this->options['archivers']['extract'])) { + foreach ($this->options['archivers']['extract'] as $mime => $conf) { + if (strpos($mime, 'application/') === 0 + && !empty($conf['cmd']) + && isset($conf['argc']) + && !empty($conf['ext']) + && !isset($this->archivers['extract'][$mime])) { + $this->archivers['extract'][$mime] = $conf; + } + } + } + + if (!empty($this->options['noSessionCache']) && is_array($this->options['noSessionCache'])) { + foreach ($this->options['noSessionCache'] as $_key) { + $this->sessionCaching[$_key] = false; + unset($this->sessionCache[$_key]); + } + } + if ($this->sessionCaching['subdirs']) { + if (!isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'] = array(); + } + } + + + $this->configure(); + + // Normalize disabled (array_merge`for type array of JSON) + $this->disabled = array_values(array_unique($this->disabled)); + + // fix sync interval + if ($this->options['syncMinMs'] !== 0) { + $this->options['syncMinMs'] = max($this->options[$this->options['syncChkAsTs'] ? 'tsPlSleep' : 'lsPlSleep'] * 1000, intval($this->options['syncMinMs'])); + } + + // ` copyJoin` is required for the trash function + if ($this->options['trashHash'] && empty($this->options['copyJoin'])) { + $this->options['trashHash'] = ''; + } + + // set tmpLinkPath + if (elFinder::$tmpLinkPath && !$this->options['tmpLinkPath']) { + if (is_writeable(elFinder::$tmpLinkPath)) { + $this->options['tmpLinkPath'] = elFinder::$tmpLinkPath; + } else { + elFinder::$tmpLinkPath = ''; + } + } + if ($this->options['tmpLinkPath'] && is_writable($this->options['tmpLinkPath'])) { + $this->tmpLinkPath = realpath($this->options['tmpLinkPath']); + } else if (!$this->tmpLinkPath && $this->tmbURL && $this->tmbPath) { + $this->tmpLinkPath = $this->tmbPath; + $this->options['tmpLinkUrl'] = $this->tmbURL; + } else if (!$this->options['URL'] && is_writable('../files/.tmb')) { + $this->tmpLinkPath = realpath('../files/.tmb'); + $this->options['tmpLinkUrl'] = ''; + if (!elFinder::$tmpLinkPath) { + elFinder::$tmpLinkPath = $this->tmpLinkPath; + elFinder::$tmpLinkUrl = ''; + } + } + + // set tmpLinkUrl + if (elFinder::$tmpLinkUrl && !$this->options['tmpLinkUrl']) { + $this->options['tmpLinkUrl'] = elFinder::$tmpLinkUrl; + } + if ($this->options['tmpLinkUrl']) { + $this->tmpLinkUrl = $this->options['tmpLinkUrl']; + } + if ($this->tmpLinkPath && !$this->tmpLinkUrl) { + $cur = realpath('./'); + $i = 0; + while ($cur !== $this->systemRoot && strpos($this->tmpLinkPath, $cur) !== 0) { + $i++; + $cur = dirname($cur); + } + list($req) = explode('?', $_SERVER['REQUEST_URI']); + $reqs = explode('/', dirname($req)); + $uri = join('/', array_slice($reqs, 0, count($reqs) - 1)) . substr($this->tmpLinkPath, strlen($cur)); + $https = (isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off'); + $this->tmpLinkUrl = ($https ? 'https://' : 'http://') + . $_SERVER['SERVER_NAME'] // host + . (((!$https && $_SERVER['SERVER_PORT'] == 80) || ($https && $_SERVER['SERVER_PORT'] == 443)) ? '' : (':' . $_SERVER['SERVER_PORT'])) // port + . $uri; + if (!elFinder::$tmpLinkUrl) { + elFinder::$tmpLinkUrl = $this->tmpLinkUrl; + } + } + + // remove last '/' + if ($this->tmpLinkPath) { + $this->tmpLinkPath = rtrim($this->tmpLinkPath, '/'); + } + if ($this->tmpLinkUrl) { + $this->tmpLinkUrl = rtrim($this->tmpLinkUrl, '/'); + } + + // to update options cache + $this->updateCache($this->root, $root); + + return $this->mounted = true; + } + + /** + * Some "unmount" stuffs - may be required by virtual fs + * + * @return void + * @author Dmitry (dio) Levashov + **/ + public function umount() + { + } + + /** + * Remove session cache of this volume + */ + public function clearSessionCache() + { + $this->sessionCache = array(); + } + + /** + * Return error message from last failed action + * + * @return array + * @author Dmitry (dio) Levashov + **/ + public function error() + { + return $this->error; + } + + /** + * Return is uploadable that given file name + * + * @param string $name file name + * @param bool $allowUnknown + * + * @return bool + * @author Naoki Sawada + **/ + public function isUploadableByName($name, $allowUnknown = false) + { + $mimeByName = $this->mimetype($name, true); + return (($allowUnknown && $mimeByName === 'unknown') || $this->allowPutMime($mimeByName)); + } + + /** + * Return Extention/MIME Table (elFinderVolumeDriver::$mimetypes) + * + * @return array + * @author Naoki Sawada + */ + public function getMimeTable() + { + // load mime.types + if (!elFinderVolumeDriver::$mimetypesLoaded) { + elFinderVolumeDriver::loadMimeTypes(); + } + return elFinderVolumeDriver::$mimetypes; + } + + /** + * Return file extention detected by MIME type + * + * @param string $mime MIME type + * @param string $suffix Additional suffix + * + * @return string + * @author Naoki Sawada + */ + public function getExtentionByMime($mime, $suffix = '') + { + static $extTable = null; + + if (is_null($extTable)) { + $extTable = array_flip(array_unique($this->getMimeTable())); + foreach ($this->options['mimeMap'] as $pair => $_mime) { + list($ext) = explode(':', $pair); + if ($ext !== '*' && !isset($extTable[$_mime])) { + $extTable[$_mime] = $ext; + } + } + } + + if ($mime && isset($extTable[$mime])) { + return $suffix ? ($extTable[$mime] . $suffix) : $extTable[$mime]; + } + return ''; + } + + /** + * Set mimetypes allowed to display to client + * + * @param array $mimes + * + * @return void + * @author Dmitry (dio) Levashov + **/ + public function setMimesFilter($mimes) + { + if (is_array($mimes)) { + $this->onlyMimes = $mimes; + } + } + + /** + * Return root folder hash + * + * @return string + * @author Dmitry (dio) Levashov + **/ + public function root() + { + return $this->encode($this->root); + } + + /** + * Return root path + * + * @return string + * @author Naoki Sawada + **/ + public function getRootPath() + { + return $this->root; + } + + /** + * Return target path hash + * + * @param string $path + * @param string $name + * + * @author Naoki Sawada + * @return string + */ + public function getHash($path, $name = '') + { + if ($name !== '') { + $path = $this->joinPathCE($path, $name); + } + return $this->encode($path); + } + + /** + * Return decoded path of target hash + * This method do not check the stat of target + * Use method `realpath()` to do check of the stat of target + * + * @param string $hash + * + * @author Naoki Sawada + * @return string + */ + public function getPath($hash) + { + return $this->decode($hash); + } + + /** + * Return root or startPath hash + * + * @return string + * @author Dmitry (dio) Levashov + **/ + public function defaultPath() + { + return $this->encode($this->startPath ? $this->startPath : $this->root); + } + + /** + * Return volume options required by client: + * + * @param $hash + * + * @return array + * @author Dmitry (dio) Levashov + */ + public function options($hash) + { + $create = $createext = array(); + if (isset($this->archivers['create']) && is_array($this->archivers['create'])) { + foreach ($this->archivers['create'] as $m => $v) { + $create[] = $m; + $createext[$m] = $v['ext']; + } + } + $opts = array( + 'path' => $hash ? $this->path($hash) : '', + 'url' => $this->URL, + 'tmbUrl' => (!$this->imgLib && $this->options['tmbFbSelf']) ? 'self' : $this->tmbURL, + 'disabled' => $this->disabled, + 'separator' => $this->separator, + 'copyOverwrite' => intval($this->options['copyOverwrite']), + 'uploadOverwrite' => intval($this->options['uploadOverwrite']), + 'uploadMaxSize' => intval($this->uploadMaxSize), + 'uploadMaxConn' => intval($this->options['uploadMaxConn']), + 'uploadMime' => array( + 'firstOrder' => isset($this->uploadOrder[0]) ? $this->uploadOrder[0] : 'deny', + 'allow' => $this->uploadAllow, + 'deny' => $this->uploadDeny + ), + 'dispInlineRegex' => $this->options['dispInlineRegex'], + 'jpgQuality' => intval($this->options['jpgQuality']), + 'archivers' => array( + 'create' => $create, + 'extract' => isset($this->archivers['extract']) && is_array($this->archivers['extract']) ? array_keys($this->archivers['extract']) : array(), + 'createext' => $createext + ), + 'uiCmdMap' => (isset($this->options['uiCmdMap']) && is_array($this->options['uiCmdMap'])) ? $this->options['uiCmdMap'] : array(), + 'syncChkAsTs' => intval($this->options['syncChkAsTs']), + 'syncMinMs' => intval($this->options['syncMinMs']), + 'i18nFolderName' => intval($this->options['i18nFolderName']), + 'tmbCrop' => intval($this->options['tmbCrop']), + 'tmbReqCustomData' => (bool)$this->options['tmbReqCustomData'], + 'substituteImg' => (bool)$this->options['substituteImg'], + 'onetimeUrl' => (bool)$this->options['onetimeUrl'], + ); + if (!empty($this->options['trashHash'])) { + $opts['trashHash'] = $this->options['trashHash']; + } + if ($hash === null) { + // call from getRootStatExtra() + if (!empty($this->options['icon'])) { + $opts['icon'] = $this->options['icon']; + } + if (!empty($this->options['rootCssClass'])) { + $opts['csscls'] = $this->options['rootCssClass']; + } + if (isset($this->options['netkey'])) { + $opts['netkey'] = $this->options['netkey']; + } + } + return $opts; + } + + /** + * Get option value of this volume + * + * @param string $name target option name + * + * @return NULL|mixed target option value + * @author Naoki Sawada + */ + public function getOption($name) + { + return isset($this->options[$name]) ? $this->options[$name] : null; + } + + /** + * Get plugin values of this options + * + * @param string $name Plugin name + * + * @return NULL|array Plugin values + * @author Naoki Sawada + */ + public function getOptionsPlugin($name = '') + { + if ($name) { + return isset($this->options['plugin'][$name]) ? $this->options['plugin'][$name] : array(); + } else { + return $this->options['plugin']; + } + } + + /** + * Return true if command disabled in options + * + * @param string $cmd command name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + public function commandDisabled($cmd) + { + return in_array($cmd, $this->disabled); + } + + /** + * Return true if mime is required mimes list + * + * @param string $mime mime type to check + * @param array $mimes allowed mime types list or not set to use client mimes list + * @param bool|null $empty what to return on empty list + * + * @return bool|null + * @author Dmitry (dio) Levashov + * @author Troex Nevelin + **/ + public function mimeAccepted($mime, $mimes = null, $empty = true) + { + $mimes = is_array($mimes) ? $mimes : $this->onlyMimes; + if (empty($mimes)) { + return $empty; + } + return $mime == 'directory' + || in_array('all', $mimes) + || in_array('All', $mimes) + || in_array($mime, $mimes) + || in_array(substr($mime, 0, strpos($mime, '/')), $mimes); + } + + /** + * Return true if voume is readable. + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + public function isReadable() + { + $stat = $this->stat($this->root); + return $stat['read']; + } + + /** + * Return true if copy from this volume allowed + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + public function copyFromAllowed() + { + return !!$this->options['copyFrom']; + } + + /** + * Return file path related to root with convert encoging + * + * @param string $hash file hash + * + * @return string + * @author Dmitry (dio) Levashov + **/ + public function path($hash) + { + return $this->convEncOut($this->_path($this->convEncIn($this->decode($hash)))); + } + + /** + * Return file real path if file exists + * + * @param string $hash file hash + * + * @return string | false + * @author Dmitry (dio) Levashov + **/ + public function realpath($hash) + { + $path = $this->decode($hash); + return $this->stat($path) ? $path : false; + } + + /** + * Return list of moved/overwrited files + * + * @return array + * @author Dmitry (dio) Levashov + **/ + public function removed() + { + if ($this->removed) { + $unsetSubdir = isset($this->sessionCache['subdirs']) ? true : false; + foreach ($this->removed as $item) { + if ($item['mime'] === 'directory') { + $path = $this->decode($item['hash']); + if ($unsetSubdir) { + unset($this->sessionCache['subdirs'][$path]); + } + if ($item['phash'] !== '') { + $parent = $this->decode($item['phash']); + unset($this->cache[$parent]); + if ($this->root === $parent) { + $this->sessionCache['rootstat'] = array(); + } + if ($unsetSubdir) { + unset($this->sessionCache['subdirs'][$parent]); + } + } + } + } + $this->removed = array_values($this->removed); + } + return $this->removed; + } + + /** + * Return list of added files + * + * @deprecated + * @return array + * @author Naoki Sawada + **/ + public function added() + { + return $this->added; + } + + /** + * Clean removed files list + * + * @return void + * @author Dmitry (dio) Levashov + **/ + public function resetRemoved() + { + $this->resetResultStat(); + } + + /** + * Clean added/removed files list + * + * @return void + **/ + public function resetResultStat() + { + $this->removed = array(); + $this->added = array(); + } + + /** + * Return file/dir hash or first founded child hash with required attr == $val + * + * @param string $hash file hash + * @param string $attr attribute name + * @param bool $val attribute value + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + public function closest($hash, $attr, $val) + { + return ($path = $this->closestByAttr($this->decode($hash), $attr, $val)) ? $this->encode($path) : false; + } + + /** + * Return file info or false on error + * + * @param string $hash file hash + * + * @return array|false + * @internal param bool $realpath add realpath field to file info + * @author Dmitry (dio) Levashov + */ + public function file($hash) + { + $file = $this->stat($this->decode($hash)); + + return ($file) ? $file : $this->setError(elFinder::ERROR_FILE_NOT_FOUND); + } + + /** + * Return folder info + * + * @param string $hash folder hash + * @param bool $resolveLink + * + * @return array|false + * @internal param bool $hidden return hidden file info + * @author Dmitry (dio) Levashov + */ + public function dir($hash, $resolveLink = false) + { + if (($dir = $this->file($hash)) == false) { + return $this->setError(elFinder::ERROR_DIR_NOT_FOUND); + } + + if ($resolveLink && !empty($dir['thash'])) { + $dir = $this->file($dir['thash']); + } + + return $dir && $dir['mime'] == 'directory' && empty($dir['hidden']) + ? $dir + : $this->setError(elFinder::ERROR_NOT_DIR); + } + + /** + * Return directory content or false on error + * + * @param string $hash file hash + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + public function scandir($hash) + { + if (($dir = $this->dir($hash)) == false) { + return false; + } + + $path = $this->decode($hash); + if ($res = $dir['read'] + ? $this->getScandir($path) + : $this->setError(elFinder::ERROR_PERM_DENIED)) { + + $dirs = null; + if ($this->sessionCaching['subdirs'] && isset($this->sessionCache['subdirs'][$path])) { + $dirs = $this->sessionCache['subdirs'][$path]; + } + if ($dirs !== null || (isset($dir['dirs']) && $dir['dirs'] != 1)) { + $_dir = $dir; + if ($dirs || $this->subdirs($hash)) { + $dir['dirs'] = 1; + } else { + unset($dir['dirs']); + } + if ($dir !== $_dir) { + $this->updateCache($path, $dir); + } + } + } + + return $res; + } + + /** + * Return dir files names list + * + * @param string $hash file hash + * @param null $intersect + * + * @return array|false + * @author Dmitry (dio) Levashov + */ + public function ls($hash, $intersect = null) + { + if (($dir = $this->dir($hash)) == false || !$dir['read']) { + return false; + } + + $list = array(); + $path = $this->decode($hash); + + $check = array(); + if ($intersect) { + $check = array_flip($intersect); + } + + foreach ($this->getScandir($path) as $stat) { + if (empty($stat['hidden']) && (!$check || isset($check[$stat['name']])) && $this->mimeAccepted($stat['mime'])) { + $list[$stat['hash']] = $stat['name']; + } + } + + return $list; + } + + /** + * Return subfolders for required folder or false on error + * + * @param string $hash folder hash or empty string to get tree from root folder + * @param int $deep subdir deep + * @param string $exclude dir hash which subfolders must be exluded from result, required to not get stat twice on cwd subfolders + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + public function tree($hash = '', $deep = 0, $exclude = '') + { + $path = $hash ? $this->decode($hash) : $this->root; + + if (($dir = $this->stat($path)) == false || $dir['mime'] != 'directory') { + return false; + } + + $dirs = $this->gettree($path, $deep > 0 ? $deep - 1 : $this->treeDeep - 1, $exclude ? $this->decode($exclude) : null); + array_unshift($dirs, $dir); + return $dirs; + } + + /** + * Return part of dirs tree from required dir up to root dir + * + * @param string $hash directory hash + * @param bool|null $lineal only lineal parents + * + * @return array|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + public function parents($hash, $lineal = false) + { + if (($current = $this->dir($hash)) == false) { + return false; + } + + $args = func_get_args(); + // checks 3rd param `$until` (elFinder >= 2.1.24) + $until = ''; + if (isset($args[2])) { + $until = $args[2]; + } + + $path = $this->decode($hash); + $tree = array(); + + while ($path && $path != $this->root) { + elFinder::checkAborted(); + $path = $this->dirnameCE($path); + if (!($stat = $this->stat($path)) || !empty($stat['hidden']) || !$stat['read']) { + return false; + } + + array_unshift($tree, $stat); + if (!$lineal) { + foreach ($this->gettree($path, 0) as $dir) { + elFinder::checkAborted(); + if (!isset($tree[$dir['hash']])) { + $tree[$dir['hash']] = $dir; + } + } + } + + if ($until && $until === $this->encode($path)) { + break; + } + } + + return $tree ? array_values($tree) : array($current); + } + + /** + * Create thumbnail for required file and return its name or false on failed + * + * @param $hash + * + * @return false|string + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + public function tmb($hash) + { + $path = $this->decode($hash); + $stat = $this->stat($path); + + if (isset($stat['tmb'])) { + $res = $stat['tmb'] == "1" ? $this->createTmb($path, $stat) : $stat['tmb']; + if (!$res) { + list($type) = explode('/', $stat['mime']); + $fallback = $this->options['resourcePath'] . DIRECTORY_SEPARATOR . strtolower($type) . '.png'; + if (is_file($fallback)) { + $res = $this->tmbname($stat); + if (!copy($fallback, $this->tmbPath . DIRECTORY_SEPARATOR . $res)) { + $res = false; + } + } + } + // tmb garbage collection + if ($res && $this->options['tmbGcMaxlifeHour'] && $this->options['tmbGcPercentage'] > 0) { + $rand = mt_rand(1, 10000); + if ($rand <= $this->options['tmbGcPercentage'] * 100) { + register_shutdown_function(array('elFinder', 'GlobGC'), $this->tmbPath . DIRECTORY_SEPARATOR . '*.png', $this->options['tmbGcMaxlifeHour'] * 3600); + } + } + return $res; + } + return false; + } + + /** + * Return file size / total directory size + * + * @param string file hash + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + public function size($hash) + { + return $this->countSize($this->decode($hash)); + } + + /** + * Open file for reading and return file pointer + * + * @param string file hash + * + * @return Resource|false + * @author Dmitry (dio) Levashov + **/ + public function open($hash) + { + if (($file = $this->file($hash)) == false + || $file['mime'] == 'directory') { + return false; + } + // check extra option for network stream pointer + if (func_num_args() > 1) { + $opts = func_get_arg(1); + } else { + $opts = array(); + } + return $this->fopenCE($this->decode($hash), 'rb', $opts); + } + + /** + * Close file pointer + * + * @param Resource $fp file pointer + * @param string $hash file hash + * + * @return void + * @author Dmitry (dio) Levashov + **/ + public function close($fp, $hash) + { + $this->fcloseCE($fp, $this->decode($hash)); + } + + /** + * Create directory and return dir info + * + * @param string $dsthash destination directory hash + * @param string $name directory name + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + public function mkdir($dsthash, $name) + { + if ($this->commandDisabled('mkdir')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if (!$this->nameAccepted($name, true)) { + return $this->setError(elFinder::ERROR_INVALID_DIRNAME); + } + + if (($dir = $this->dir($dsthash)) == false) { + return $this->setError(elFinder::ERROR_TRGDIR_NOT_FOUND, '#' . $dsthash); + } + + $path = $this->decode($dsthash); + + if (!$dir['write'] || !$this->allowCreate($path, $name, true)) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + $dst = $this->joinPathCE($path, $name); + $stat = $this->isNameExists($dst); + if (!empty($stat)) { + return $this->setError(elFinder::ERROR_EXISTS, $name); + } + $this->clearcache(); + + $mkpath = $this->convEncOut($this->_mkdir($this->convEncIn($path), $this->convEncIn($name))); + if ($mkpath) { + $this->clearstatcache(); + $this->updateSubdirsCache($path, true); + $this->updateSubdirsCache($mkpath, false); + } + + return $mkpath ? $this->stat($mkpath) : false; + } + + /** + * Create empty file and return its info + * + * @param string $dst destination directory + * @param string $name file name + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + public function mkfile($dst, $name) + { + if ($this->commandDisabled('mkfile')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if (!$this->nameAccepted($name, false)) { + return $this->setError(elFinder::ERROR_INVALID_NAME); + } + + $mimeByName = $this->mimetype($name, true); + if ($mimeByName && !$this->allowPutMime($mimeByName)) { + return $this->setError(elFinder::ERROR_UPLOAD_FILE_MIME, $name); + } + + if (($dir = $this->dir($dst)) == false) { + return $this->setError(elFinder::ERROR_TRGDIR_NOT_FOUND, '#' . $dst); + } + + $path = $this->decode($dst); + + if (!$dir['write'] || !$this->allowCreate($path, $name, false)) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if ($this->isNameExists($this->joinPathCE($path, $name))) { + return $this->setError(elFinder::ERROR_EXISTS, $name); + } + + $this->clearcache(); + $res = false; + if ($path = $this->convEncOut($this->_mkfile($this->convEncIn($path), $this->convEncIn($name)))) { + $this->clearstatcache(); + $res = $this->stat($path); + } + return $res; + } + + /** + * Rename file and return file info + * + * @param string $hash file hash + * @param string $name new file name + * + * @return array|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + public function rename($hash, $name) + { + if ($this->commandDisabled('rename')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if (!($file = $this->file($hash))) { + return $this->setError(elFinder::ERROR_FILE_NOT_FOUND); + } + + if ($name === $file['name']) { + return $file; + } + + if (!empty($this->options['netkey']) && !empty($file['isroot'])) { + // change alias of netmount root + $rootKey = $this->getRootstatCachekey(); + // delete old cache data + if ($this->sessionCaching['rootstat']) { + unset($this->sessionCaching['rootstat'][$rootKey]); + } + if (elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $name)) { + $this->clearcache(); + $this->rootName = $this->options['alias'] = $name; + return $this->stat($this->root); + } else { + return $this->setError(elFinder::ERROR_TRGDIR_NOT_FOUND, $name); + } + } + + if (!empty($file['locked'])) { + return $this->setError(elFinder::ERROR_LOCKED, $file['name']); + } + + $isDir = ($file['mime'] === 'directory'); + + if (!$this->nameAccepted($name, $isDir)) { + return $this->setError($isDir ? elFinder::ERROR_INVALID_DIRNAME : elFinder::ERROR_INVALID_NAME); + } + + if (!$isDir) { + $mimeByName = $this->mimetype($name, true); + if ($mimeByName && !$this->allowPutMime($mimeByName)) { + return $this->setError(elFinder::ERROR_UPLOAD_FILE_MIME, $name); + } + } + + $path = $this->decode($hash); + $dir = $this->dirnameCE($path); + $stat = $this->isNameExists($this->joinPathCE($dir, $name)); + if ($stat) { + return $this->setError(elFinder::ERROR_EXISTS, $name); + } + + if (!$this->allowCreate($dir, $name, ($file['mime'] === 'directory'))) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + $this->rmTmb($file); // remove old name tmbs, we cannot do this after dir move + + + if ($path = $this->convEncOut($this->_move($this->convEncIn($path), $this->convEncIn($dir), $this->convEncIn($name)))) { + $this->clearcache(); + return $this->stat($path); + } + return false; + } + + /** + * Create file copy with suffix "copy number" and return its info + * + * @param string $hash file hash + * @param string $suffix suffix to add to file name + * + * @return array|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + public function duplicate($hash, $suffix = 'copy') + { + if ($this->commandDisabled('duplicate')) { + return $this->setError(elFinder::ERROR_COPY, '#' . $hash, elFinder::ERROR_PERM_DENIED); + } + + if (($file = $this->file($hash)) == false) { + return $this->setError(elFinder::ERROR_COPY, elFinder::ERROR_FILE_NOT_FOUND); + } + + $path = $this->decode($hash); + $dir = $this->dirnameCE($path); + $name = $this->uniqueName($dir, $file['name'], sprintf($this->options['duplicateSuffix'], $suffix)); + + if (!$this->allowCreate($dir, $name, ($file['mime'] === 'directory'))) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + return ($path = $this->copy($path, $dir, $name)) == false + ? false + : $this->stat($path); + } + + /** + * Save uploaded file. + * On success return array with new file stat and with removed file hash (if existed file was replaced) + * + * @param Resource $fp file pointer + * @param string $dst destination folder hash + * @param $name + * @param string $tmpname file tmp name - required to detect mime type + * @param array $hashes exists files hash array with filename as key + * + * @return array|false + * @throws elFinderAbortException + * @internal param string $src file name + * @author Dmitry (dio) Levashov + */ + public function upload($fp, $dst, $name, $tmpname, $hashes = array()) + { + if ($this->commandDisabled('upload')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if (($dir = $this->dir($dst)) == false) { + return $this->setError(elFinder::ERROR_TRGDIR_NOT_FOUND, '#' . $dst); + } + + if (empty($dir['write'])) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if (!$this->nameAccepted($name, false)) { + return $this->setError(elFinder::ERROR_INVALID_NAME); + } + + $mimeByName = ''; + if ($this->mimeDetect === 'internal') { + $mime = $this->mimetype($tmpname, $name); + } else { + $mime = $this->mimetype($tmpname, $name); + $mimeByName = $this->mimetype($name, true); + if ($mime === 'unknown') { + $mime = $mimeByName; + } + } + + if (!$this->allowPutMime($mime) || ($mimeByName && !$this->allowPutMime($mimeByName))) { + return $this->setError(elFinder::ERROR_UPLOAD_FILE_MIME, '(' . $mime . ')'); + } + + $tmpsize = (int)sprintf('%u', filesize($tmpname)); + if ($this->uploadMaxSize > 0 && $tmpsize > $this->uploadMaxSize) { + return $this->setError(elFinder::ERROR_UPLOAD_FILE_SIZE); + } + + $dstpath = $this->decode($dst); + if (isset($hashes[$name])) { + $test = $this->decode($hashes[$name]); + $file = $this->stat($test); + } else { + $test = $this->joinPathCE($dstpath, $name); + $file = $this->isNameExists($test); + } + + $this->clearcache(); + + if ($file && $file['name'] === $name) { // file exists and check filename for item ID based filesystem + if ($this->uploadOverwrite) { + if (!$file['write']) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } elseif ($file['mime'] == 'directory') { + return $this->setError(elFinder::ERROR_NOT_REPLACE, $name); + } + $this->remove($test); + } else { + $name = $this->uniqueName($dstpath, $name, '-', false); + } + } + + $stat = array( + 'mime' => $mime, + 'width' => 0, + 'height' => 0, + 'size' => $tmpsize); + + // $w = $h = 0; + if (strpos($mime, 'image') === 0 && ($s = getimagesize($tmpname))) { + $stat['width'] = $s[0]; + $stat['height'] = $s[1]; + } + // $this->clearcache(); + if (($path = $this->saveCE($fp, $dstpath, $name, $stat)) == false) { + return false; + } + + $stat = $this->stat($path); + // Try get URL + if (empty($stat['url']) && ($url = $this->getContentUrl($stat['hash']))) { + $stat['url'] = $url; + } + + return $stat; + } + + /** + * Paste files + * + * @param Object $volume source volume + * @param $src + * @param string $dst destination dir hash + * @param bool $rmSrc remove source after copy? + * @param array $hashes + * + * @return array|false + * @throws elFinderAbortException + * @internal param string $source file hash + * @author Dmitry (dio) Levashov + */ + public function paste($volume, $src, $dst, $rmSrc = false, $hashes = array()) + { + $err = $rmSrc ? elFinder::ERROR_MOVE : elFinder::ERROR_COPY; + + if ($this->commandDisabled('paste')) { + return $this->setError($err, '#' . $src, elFinder::ERROR_PERM_DENIED); + } + + if (($file = $volume->file($src, $rmSrc)) == false) { + return $this->setError($err, '#' . $src, elFinder::ERROR_FILE_NOT_FOUND); + } + + $name = $file['name']; + $errpath = $volume->path($file['hash']); + + if (($dir = $this->dir($dst)) == false) { + return $this->setError($err, $errpath, elFinder::ERROR_TRGDIR_NOT_FOUND, '#' . $dst); + } + + if (!$dir['write'] || !$file['read']) { + return $this->setError($err, $errpath, elFinder::ERROR_PERM_DENIED); + } + + $destination = $this->decode($dst); + + if (($test = $volume->closest($src, $rmSrc ? 'locked' : 'read', $rmSrc))) { + return $rmSrc + ? $this->setError($err, $errpath, elFinder::ERROR_LOCKED, $volume->path($test)) + : $this->setError($err, $errpath, empty($file['thash']) ? elFinder::ERROR_PERM_DENIED : elFinder::ERROR_MKOUTLINK); + } + + if (isset($hashes[$name])) { + $test = $this->decode($hashes[$name]); + $stat = $this->stat($test); + } else { + $test = $this->joinPathCE($destination, $name); + $stat = $this->isNameExists($test); + } + $this->clearcache(); + $dstDirExists = false; + if ($stat && $stat['name'] === $name) { // file exists and check filename for item ID based filesystem + if ($this->options['copyOverwrite']) { + // do not replace file with dir or dir with file + if (!$this->isSameType($file['mime'], $stat['mime'])) { + return $this->setError(elFinder::ERROR_NOT_REPLACE, $this->path($stat['hash'])); + } + // existed file is not writable + if (empty($stat['write'])) { + return $this->setError($err, $errpath, elFinder::ERROR_PERM_DENIED); + } + if ($this->options['copyJoin']) { + if (!empty($stat['locked'])) { + return $this->setError(elFinder::ERROR_LOCKED, $this->path($stat['hash'])); + } + } else { + // existed file locked or has locked child + if (($locked = $this->closestByAttr($test, 'locked', true))) { + $stat = $this->stat($locked); + return $this->setError(elFinder::ERROR_LOCKED, $this->path($stat['hash'])); + } + } + // target is entity file of alias + if ($volume === $this && ((isset($file['target']) && $test == $file['target']) || $test == $this->decode($src))) { + return $this->setError(elFinder::ERROR_REPLACE, $errpath); + } + // remove existed file + if (!$this->options['copyJoin'] || $stat['mime'] !== 'directory') { + if (!$this->remove($test)) { + return $this->setError(elFinder::ERROR_REPLACE, $this->path($stat['hash'])); + } + } else if ($stat['mime'] === 'directory') { + $dstDirExists = true; + } + } else { + $name = $this->uniqueName($destination, $name, ' ', false); + } + } + + // copy/move inside current volume + if ($volume === $this) { // changing == operand to === fixes issue #1285 - Paul Canning 24/03/2016 + $source = $this->decode($src); + // do not copy into itself + if ($this->inpathCE($destination, $source)) { + return $this->setError(elFinder::ERROR_COPY_ITSELF, $errpath); + } + $rmDir = false; + if ($rmSrc) { + if ($dstDirExists) { + $rmDir = true; + $method = 'copy'; + } else { + $method = 'move'; + } + } else { + $method = 'copy'; + } + $this->clearcache(); + if ($res = ($path = $this->$method($source, $destination, $name)) ? $this->stat($path) : false) { + if ($rmDir) { + $this->remove($source); + } + } else { + return false; + } + } else { + // copy/move from another volume + if (!$this->options['copyTo'] || !$volume->copyFromAllowed()) { + return $this->setError(elFinder::ERROR_COPY, $errpath, elFinder::ERROR_PERM_DENIED); + } + + $this->error = array(); + if (($path = $this->copyFrom($volume, $src, $destination, $name)) == false) { + return false; + } + + if ($rmSrc && !$this->error()) { + if (!$volume->rm($src)) { + if ($volume->file($src)) { + $this->addError(elFinder::ERROR_RM_SRC); + } else { + $this->removed[] = $file; + } + } + } + $res = $this->stat($path); + } + return $res; + } + + /** + * Return path info array to archive of target items + * + * @param array $hashes + * + * @return array|false + * @throws Exception + * @author Naoki Sawada + */ + public function zipdl($hashes) + { + if ($this->commandDisabled('zipdl')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + $archivers = $this->getArchivers(); + $cmd = null; + if (!$archivers || empty($archivers['create'])) { + return false; + } + $archivers = $archivers['create']; + if (!$archivers) { + return false; + } + $file = $mime = ''; + foreach (array('zip', 'tgz') as $ext) { + $mime = $this->mimetype('file.' . $ext, true); + if (isset($archivers[$mime])) { + $cmd = $archivers[$mime]; + break; + } + } + if (!$cmd) { + $cmd = array_shift($archivers); + if (!empty($ext)) { + $mime = $this->mimetype('file.' . $ext, true); + } + } + $ext = $cmd['ext']; + $res = false; + $mixed = false; + $hashes = array_values($hashes); + $dirname = dirname(str_replace($this->separator, DIRECTORY_SEPARATOR, $this->path($hashes[0]))); + $cnt = count($hashes); + if ($cnt > 1) { + for ($i = 1; $i < $cnt; $i++) { + if ($dirname !== dirname(str_replace($this->separator, DIRECTORY_SEPARATOR, $this->path($hashes[$i])))) { + $mixed = true; + break; + } + } + } + if ($mixed || $this->root == $this->dirnameCE($this->decode($hashes[0]))) { + $prefix = $this->rootName; + } else { + $prefix = basename($dirname); + } + if ($dir = $this->getItemsInHand($hashes)) { + $tmppre = (substr(PHP_OS, 0, 3) === 'WIN') ? 'zd-' : 'elfzdl-'; + $pdir = dirname($dir); + // garbage collection (expire 2h) + register_shutdown_function(array('elFinder', 'GlobGC'), $pdir . DIRECTORY_SEPARATOR . $tmppre . '*', 7200); + $files = self::localScandir($dir); + if ($files && ($arc = tempnam($dir, $tmppre))) { + unlink($arc); + $arc = $arc . '.' . $ext; + $name = basename($arc); + if ($arc = $this->makeArchive($dir, $files, $name, $cmd)) { + $file = tempnam($pdir, $tmppre); + unlink($file); + $res = rename($arc, $file); + $this->rmdirRecursive($dir); + } + } + } + return $res ? array('path' => $file, 'ext' => $ext, 'mime' => $mime, 'prefix' => $prefix) : false; + } + + /** + * Return file contents + * + * @param string $hash file hash + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + public function getContents($hash) + { + $file = $this->file($hash); + + if (!$file) { + return $this->setError(elFinder::ERROR_FILE_NOT_FOUND); + } + + if ($file['mime'] == 'directory') { + return $this->setError(elFinder::ERROR_NOT_FILE); + } + + if (!$file['read']) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if ($this->getMaxSize > 0 && $file['size'] > $this->getMaxSize) { + return $this->setError(elFinder::ERROR_UPLOAD_FILE_SIZE); + } + + return $file['size'] ? $this->_getContents($this->convEncIn($this->decode($hash), true)) : ''; + } + + /** + * Put content in text file and return file info. + * + * @param string $hash file hash + * @param string $content new file content + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + public function putContents($hash, $content) + { + if ($this->commandDisabled('edit')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + $path = $this->decode($hash); + + if (!($file = $this->file($hash))) { + return $this->setError(elFinder::ERROR_FILE_NOT_FOUND); + } + + if (!$file['write']) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + // check data cheme + if (preg_match('~^\0data:(.+?/.+?);base64,~', $content, $m)) { + $dMime = $m[1]; + if ($file['size'] > 0 && $dMime !== $file['mime']) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + $content = base64_decode(substr($content, strlen($m[0]))); + } + + // check MIME + $name = $this->basenameCE($path); + $mime = ''; + $mimeByName = $this->mimetype($name, true); + if ($this->mimeDetect !== 'internal') { + if ($tp = $this->tmpfile()) { + fwrite($tp, $content); + $info = stream_get_meta_data($tp); + $filepath = $info['uri']; + $mime = $this->mimetype($filepath, $name); + fclose($tp); + } + } + if (!$this->allowPutMime($mimeByName) || ($mime && !$this->allowPutMime($mime))) { + return $this->setError(elFinder::ERROR_UPLOAD_FILE_MIME); + } + + $this->clearcache(); + $res = false; + if ($this->convEncOut($this->_filePutContents($this->convEncIn($path), $content))) { + $this->rmTmb($file); + $this->clearstatcache(); + $res = $this->stat($path); + } + return $res; + } + + /** + * Extract files from archive + * + * @param string $hash archive hash + * @param null $makedir + * + * @return array|bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + */ + public function extract($hash, $makedir = null) + { + if ($this->commandDisabled('extract')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if (($file = $this->file($hash)) == false) { + return $this->setError(elFinder::ERROR_FILE_NOT_FOUND); + } + + $archiver = isset($this->archivers['extract'][$file['mime']]) + ? $this->archivers['extract'][$file['mime']] + : array(); + + if (!$archiver) { + return $this->setError(elFinder::ERROR_NOT_ARCHIVE); + } + + $path = $this->decode($hash); + $parent = $this->stat($this->dirnameCE($path)); + + if (!$file['read'] || !$parent['write']) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + $this->clearcache(); + $this->extractToNewdir = is_null($makedir) ? 'auto' : (bool)$makedir; + + if ($path = $this->convEncOut($this->_extract($this->convEncIn($path), $archiver))) { + if (is_array($path)) { + foreach ($path as $_k => $_p) { + $path[$_k] = $this->stat($_p); + } + } else { + $path = $this->stat($path); + } + return $path; + } else { + return false; + } + } + + /** + * Add files to archive + * + * @param $hashes + * @param $mime + * @param string $name + * + * @return array|bool + * @throws Exception + */ + public function archive($hashes, $mime, $name = '') + { + if ($this->commandDisabled('archive')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if ($name !== '' && !$this->nameAccepted($name, false)) { + return $this->setError(elFinder::ERROR_INVALID_NAME); + } + + $archiver = isset($this->archivers['create'][$mime]) + ? $this->archivers['create'][$mime] + : array(); + + if (!$archiver) { + return $this->setError(elFinder::ERROR_ARCHIVE_TYPE); + } + + $files = array(); + $useRemoteArchive = !empty($this->options['useRemoteArchive']); + + $dir = ''; + foreach ($hashes as $hash) { + if (($file = $this->file($hash)) == false) { + return $this->setError(elFinder::ERROR_FILE_NOT_FOUND, '#' . $hash); + } + if (!$file['read']) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + $path = $this->decode($hash); + if ($dir === '') { + $dir = $this->dirnameCE($path); + $stat = $this->stat($dir); + if (!$stat['write']) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + } + + $files[] = $useRemoteArchive ? $hash : $this->basenameCE($path); + } + + if ($name === '') { + $name = count($files) == 1 ? $files[0] : 'Archive'; + } else { + $name = str_replace(array('/', '\\'), '_', preg_replace('/\.' . preg_quote($archiver['ext'], '/') . '$/i', '', $name)); + } + $name .= '.' . $archiver['ext']; + $name = $this->uniqueName($dir, $name, ''); + $this->clearcache(); + if ($useRemoteArchive) { + return ($path = $this->remoteArchive($files, $name, $archiver)) ? $this->stat($path) : false; + } else { + return ($path = $this->convEncOut($this->_archive($this->convEncIn($dir), $this->convEncIn($files), $this->convEncIn($name), $archiver))) ? $this->stat($path) : false; + } + } + + /** + * Create an archive from remote items + * + * @param array $hashes files hashes list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|boolean path of created archive + * @throws Exception + */ + protected function remoteArchive($hashes, $name, $arc) + { + $resPath = false; + $file0 = $this->file($hashes[0]); + if ($file0 && ($dir = $this->getItemsInHand($hashes))) { + $files = self::localScandir($dir); + if ($files) { + if ($arc = $this->makeArchive($dir, $files, $name, $arc)) { + if ($fp = fopen($arc, 'rb')) { + $fstat = stat($arc); + $stat = array( + 'size' => $fstat['size'], + 'ts' => $fstat['mtime'], + 'mime' => $this->mimetype($arc, $name) + ); + $path = $this->decode($file0['phash']); + $resPath = $this->saveCE($fp, $path, $name, $stat); + fclose($fp); + } + } + } + $this->rmdirRecursive($dir); + } + return $resPath; + } + + /** + * Resize image + * + * @param string $hash image file + * @param int $width new width + * @param int $height new height + * @param int $x X start poistion for crop + * @param int $y Y start poistion for crop + * @param string $mode action how to mainpulate image + * @param string $bg background color + * @param int $degree rotete degree + * @param int $jpgQuality JEPG quality (1-100) + * + * @return array|false + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + * @author nao-pon + * @author Troex Nevelin + */ + public function resize($hash, $width, $height, $x, $y, $mode = 'resize', $bg = '', $degree = 0, $jpgQuality = null) + { + if ($this->commandDisabled('resize')) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if ($mode === 'rotate' && $degree == 0) { + return array('losslessRotate' => ($this->procExec(ELFINDER_EXIFTRAN_PATH . ' -h') === 0 || $this->procExec(ELFINDER_JPEGTRAN_PATH . ' -version') === 0)); + } + + if (($file = $this->file($hash)) == false) { + return $this->setError(elFinder::ERROR_FILE_NOT_FOUND); + } + + if (!$file['write'] || !$file['read']) { + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + $path = $this->decode($hash); + + $work_path = $this->getWorkFile($this->encoding ? $this->convEncIn($path, true) : $path); + + if (!$work_path || !is_writable($work_path)) { + if ($work_path && $path !== $work_path && is_file($work_path)) { + unlink($work_path); + } + return $this->setError(elFinder::ERROR_PERM_DENIED); + } + + if ($this->imgLib !== 'imagick' && $this->imgLib !== 'convert') { + if (elFinder::isAnimationGif($work_path)) { + return $this->setError(elFinder::ERROR_UNSUPPORT_TYPE); + } + } + + if (elFinder::isAnimationPng($work_path)) { + return $this->setError(elFinder::ERROR_UNSUPPORT_TYPE); + } + + switch ($mode) { + + case 'propresize': + $result = $this->imgResize($work_path, $width, $height, true, true, null, $jpgQuality); + break; + + case 'crop': + $result = $this->imgCrop($work_path, $width, $height, $x, $y, null, $jpgQuality); + break; + + case 'fitsquare': + $result = $this->imgSquareFit($work_path, $width, $height, 'center', 'middle', ($bg ? $bg : $this->options['tmbBgColor']), null, $jpgQuality); + break; + + case 'rotate': + $result = $this->imgRotate($work_path, $degree, ($bg ? $bg : $this->options['bgColorFb']), null, $jpgQuality); + break; + + default: + $result = $this->imgResize($work_path, $width, $height, false, true, null, $jpgQuality); + break; + } + + $ret = false; + if ($result) { + $this->rmTmb($file); + $this->clearstatcache(); + $fstat = stat($work_path); + $imgsize = getimagesize($work_path); + if ($path !== $work_path) { + $file['size'] = $fstat['size']; + $file['ts'] = $fstat['mtime']; + if ($imgsize) { + $file['width'] = $imgsize[0]; + $file['height'] = $imgsize[1]; + } + if ($fp = fopen($work_path, 'rb')) { + $ret = $this->saveCE($fp, $this->dirnameCE($path), $this->basenameCE($path), $file); + fclose($fp); + } + } else { + $ret = true; + } + if ($ret) { + $this->clearcache(); + $ret = $this->stat($path); + if ($imgsize) { + $ret['width'] = $imgsize[0]; + $ret['height'] = $imgsize[1]; + } + } + } + if ($path !== $work_path) { + is_file($work_path) && unlink($work_path); + } + + return $ret; + } + + /** + * Remove file/dir + * + * @param string $hash file hash + * + * @return bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + public function rm($hash) + { + return $this->commandDisabled('rm') + ? $this->setError(elFinder::ERROR_PERM_DENIED) + : $this->remove($this->decode($hash)); + } + + /** + * Search files + * + * @param string $q search string + * @param array $mimes + * @param null $hash + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + public function search($q, $mimes, $hash = null) + { + $res = array(); + $matchMethod = null; + $args = func_get_args(); + if (!empty($args[3])) { + $matchMethod = 'searchMatch' . $args[3]; + if (!is_callable(array($this, $matchMethod))) { + return array(); + } + } + + $dir = null; + if ($hash) { + $dir = $this->decode($hash); + $stat = $this->stat($dir); + if (!$stat || $stat['mime'] !== 'directory' || !$stat['read']) { + $q = ''; + } + } + if ($mimes && $this->onlyMimes) { + $mimes = array_intersect($mimes, $this->onlyMimes); + if (!$mimes) { + $q = ''; + } + } + $this->searchStart = time(); + + $qs = preg_split('/"([^"]+)"| +/', $q, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $query = $excludes = array(); + foreach ($qs as $_q) { + $_q = trim($_q); + if ($_q !== '') { + if ($_q[0] === '-') { + if (isset($_q[1])) { + $excludes[] = substr($_q, 1); + } + } else { + $query[] = $_q; + } + } + } + if (!$query) { + $q = ''; + } else { + $q = join(' ', $query); + $this->doSearchCurrentQuery = array( + 'q' => $q, + 'excludes' => $excludes, + 'matchMethod' => $matchMethod + ); + } + + if ($q === '' || $this->commandDisabled('search')) { + return $res; + } + + // valided regex $this->options['searchExDirReg'] + if ($this->options['searchExDirReg']) { + if (false === preg_match($this->options['searchExDirReg'], '')) { + $this->options['searchExDirReg'] = ''; + } + } + + // check the leaf root too + if (!$mimes && (is_null($dir) || $dir == $this->root)) { + $rootStat = $this->stat($this->root); + if (!empty($rootStat['phash'])) { + if ($this->stripos($rootStat['name'], $q) !== false) { + $res = array($rootStat); + } + } + } + + return array_merge($res, $this->doSearch(is_null($dir) ? $this->root : $dir, $q, $mimes)); + } + + /** + * Return image dimensions + * + * @param string $hash file hash + * + * @return array|string + * @author Dmitry (dio) Levashov + **/ + public function dimensions($hash) + { + if (($file = $this->file($hash)) == false) { + return false; + } + // Throw additional parameters for some drivers + if (func_num_args() > 1) { + $args = func_get_arg(1); + } else { + $args = array(); + } + return $this->convEncOut($this->_dimensions($this->convEncIn($this->decode($hash)), $file['mime'], $args)); + } + + /** + * Return has subdirs + * + * @param string $hash file hash + * + * @return bool + * @author Naoki Sawada + **/ + public function subdirs($hash) + { + return (bool)$this->subdirsCE($this->decode($hash)); + } + + /** + * Return content URL (for netmout volume driver) + * If file.url == 1 requests from JavaScript client with XHR + * + * @param string $hash file hash + * @param array $options options array + * + * @return boolean|string + * @author Naoki Sawada + */ + public function getContentUrl($hash, $options = array()) + { + if (($file = $this->file($hash)) === false) { + return false; + } + if (!empty($options['onetime']) && $this->options['onetimeUrl']) { + if (is_callable($this->options['onetimeUrl'])) { + return call_user_func_array($this->options['onetimeUrl'], array($file, $options, $this)); + } else { + $ret = false; + if ($tmpdir = elFinder::getStaticVar('commonTempPath')) { + if ($source = $this->open($hash)) { + if ($_dat = tempnam($tmpdir, 'ELF')) { + $token = md5($_dat . session_id()); + $dat = $tmpdir . DIRECTORY_SEPARATOR . 'ELF' . $token; + if (rename($_dat, $dat)) { + $info = stream_get_meta_data($source); + if (!empty($info['uri'])) { + $tmp = $info['uri']; + } else { + $tmp = tempnam($tmpdir, 'ELF'); + if ($dest = fopen($tmp, 'wb')) { + if (!stream_copy_to_stream($source, $dest)) { + $tmp = false; + } + fclose($dest); + } + } + $this->close($source, $hash); + if ($tmp) { + $info = array( + 'file' => base64_encode($tmp), + 'name' => $file['name'], + 'mime' => $file['mime'], + 'ts' => $file['ts'] + ); + if (file_put_contents($dat, json_encode($info))) { + $conUrl = elFinder::getConnectorUrl(); + $ret = $conUrl . (strpos($conUrl, '?') !== false? '&' : '?') . 'cmd=file&onetime=1&target=' . $token; + + } + } + if (!$ret) { + unlink($dat); + } + } else { + unlink($_dat); + } + } + } + } + return $ret; + } + } + if (empty($file['url']) && $this->URL) { + $path = str_replace($this->separator, '/', substr($this->decode($hash), strlen(rtrim($this->root, '/' . $this->separator)) + 1)); + if ($this->encoding) { + $path = $this->convEncIn($path, true); + } + $path = str_replace('%2F', '/', rawurlencode($path)); + return $this->URL . $path; + } else { + $ret = false; + if (!empty($file['url']) && $file['url'] != 1) { + return $file['url']; + } else if (!empty($options['temporary']) && ($tempInfo = $this->getTempLinkInfo('temp_' . md5($hash . session_id())))) { + if (is_readable($tempInfo['path'])) { + touch($tempInfo['path']); + $ret = $tempInfo['url'] . '?' . rawurlencode($file['name']); + } else if ($source = $this->open($hash)) { + if ($dest = fopen($tempInfo['path'], 'wb')) { + if (stream_copy_to_stream($source, $dest)) { + $ret = $tempInfo['url'] . '?' . rawurlencode($file['name']); + } + fclose($dest); + } + $this->close($source, $hash); + } + } + return $ret; + } + } + + /** + * Get temporary contents link infomation + * + * @param string $name + * + * @return boolean|array + * @author Naoki Sawada + */ + public function getTempLinkInfo($name = null) + { + if ($this->tmpLinkPath) { + if (!$name) { + $name = 'temp_' . md5($_SERVER['REMOTE_ADDR'] . (string)microtime(true)); + } else if (substr($name, 0, 5) !== 'temp_') { + $name = 'temp_' . $name; + } + register_shutdown_function(array('elFinder', 'GlobGC'), $this->tmpLinkPath . DIRECTORY_SEPARATOR . 'temp_*', elFinder::$tmpLinkLifeTime); + return array( + 'path' => $path = $this->tmpLinkPath . DIRECTORY_SEPARATOR . $name, + 'url' => $this->tmpLinkUrl . '/' . rawurlencode($name) + ); + } + return false; + } + + /** + * Get URL of substitute image by request args `substitute` or 4th argument $maxSize + * + * @param string $target Target hash + * @param array $srcSize Size info array [width, height] + * @param resource $srcfp Source file file pointer + * @param integer $maxSize Maximum pixel of substitute image + * + * @return boolean + * @throws ImagickException + * @throws elFinderAbortException + */ + public function getSubstituteImgLink($target, $srcSize, $srcfp = null, $maxSize = null) + { + $url = false; + $file = $this->file($target); + $force = !in_array($file['mime'], array('image/jpeg', 'image/png', 'image/gif','image/webp')); + if (!$maxSize) { + $args = elFinder::$currentArgs; + if (!empty($args['substitute'])) { + $maxSize = $args['substitute']; + } + } + if ($maxSize && $srcSize[0] && $srcSize[1]) { + if ($this->getOption('substituteImg')) { + $maxSize = intval($maxSize); + $zoom = min(($maxSize / $srcSize[0]), ($maxSize / $srcSize[1])); + if ($force || $zoom < 1) { + $width = round($srcSize[0] * $zoom); + $height = round($srcSize[1] * $zoom); + $jpgQuality = 50; + $preserveExif = false; + $unenlarge = true; + $checkAnimated = true; + $destformat = $file['mime'] === 'image/jpeg'? null : 'png'; + if (!$srcfp) { + elFinder::checkAborted(); + $srcfp = $this->open($target); + } + if ($srcfp && ($tempLink = $this->getTempLinkInfo())) { + elFinder::checkAborted(); + $dest = fopen($tempLink['path'], 'wb'); + if ($dest && stream_copy_to_stream($srcfp, $dest)) { + fclose($dest); + if ($this->imageUtil('resize', $tempLink['path'], compact('width', 'height', 'jpgQuality', 'preserveExif', 'unenlarge', 'checkAnimated', 'destformat'))) { + $url = $tempLink['url']; + // set expire to 1 min left + touch($tempLink['path'], time() - elFinder::$tmpLinkLifeTime + 60); + } else { + unlink($tempLink['path']); + } + } + $this->close($srcfp, $target); + } + } + } + } + + return $url; + } + + /** + * Return temp path + * + * @return string + * @author Naoki Sawada + */ + public function getTempPath() + { + $tempPath = null; + if (isset($this->tmpPath) && $this->tmpPath && is_writable($this->tmpPath)) { + $tempPath = $this->tmpPath; + } else if (isset($this->tmp) && $this->tmp && is_writable($this->tmp)) { + $tempPath = $this->tmp; + } else if (elFinder::getStaticVar('commonTempPath') && is_writable(elFinder::getStaticVar('commonTempPath'))) { + $tempPath = elFinder::getStaticVar('commonTempPath'); + } else if (function_exists('sys_get_temp_dir')) { + $tempPath = sys_get_temp_dir(); + } else if ($this->tmbPathWritable) { + $tempPath = $this->tmbPath; + } + if ($tempPath && DIRECTORY_SEPARATOR !== '/') { + $tempPath = str_replace('/', DIRECTORY_SEPARATOR, $tempPath); + } + return $tempPath; + } + + /** + * (Make &) Get upload taget dirctory hash + * + * @param string $baseTargetHash + * @param string $path + * @param array $result + * + * @return boolean|string + * @author Naoki Sawada + */ + public function getUploadTaget($baseTargetHash, $path, & $result) + { + $base = $this->decode($baseTargetHash); + $targetHash = $baseTargetHash; + $path = ltrim($path, $this->separator); + $dirs = explode($this->separator, $path); + array_pop($dirs); + foreach ($dirs as $dir) { + $targetPath = $this->joinPathCE($base, $dir); + if (!$_realpath = $this->realpath($this->encode($targetPath))) { + if ($stat = $this->mkdir($targetHash, $dir)) { + $result['added'][] = $stat; + $targetHash = $stat['hash']; + $base = $this->decode($targetHash); + } else { + return false; + } + } else { + $targetHash = $this->encode($_realpath); + if ($this->dir($targetHash)) { + $base = $this->decode($targetHash); + } else { + return false; + } + } + } + return $targetHash; + } + + /** + * Return this uploadMaxSize value + * + * @return integer + * @author Naoki Sawada + */ + public function getUploadMaxSize() + { + return $this->uploadMaxSize; + } + + public function setUploadOverwrite($var) + { + $this->uploadOverwrite = (bool)$var; + } + + /** + * Image file utility + * + * @param string $mode 'resize', 'rotate', 'propresize', 'crop', 'fitsquare' + * @param string $src Image file local path + * @param array $options excute options + * + * @return bool + * @throws ImagickException + * @throws elFinderAbortException + * @author Naoki Sawada + */ + public function imageUtil($mode, $src, $options = array()) + { + if (!isset($options['jpgQuality'])) { + $options['jpgQuality'] = intval($this->options['jpgQuality']); + } + if (!isset($options['bgcolor'])) { + $options['bgcolor'] = '#ffffff'; + } + if (!isset($options['bgColorFb'])) { + $options['bgColorFb'] = $this->options['bgColorFb']; + } + $destformat = !empty($options['destformat'])? $options['destformat'] : null; + + // check 'width' ,'height' + if (in_array($mode, array('resize', 'propresize', 'crop', 'fitsquare'))) { + if (empty($options['width']) || empty($options['height'])) { + return false; + } + } + + if (!empty($options['checkAnimated'])) { + if ($this->imgLib !== 'imagick' && $this->imgLib !== 'convert') { + if (elFinder::isAnimationGif($src)) { + return false; + } + } + if (elFinder::isAnimationPng($src)) { + return false; + } + } + + switch ($mode) { + case 'rotate': + if (empty($options['degree'])) { + return true; + } + return (bool)$this->imgRotate($src, $options['degree'], $options['bgColorFb'], $destformat, $options['jpgQuality']); + + case 'resize': + return (bool)$this->imgResize($src, $options['width'], $options['height'], false, true, $destformat, $options['jpgQuality'], $options); + + case 'propresize': + return (bool)$this->imgResize($src, $options['width'], $options['height'], true, true, $destformat, $options['jpgQuality'], $options); + + case 'crop': + if (isset($options['x']) && isset($options['y'])) { + return (bool)$this->imgCrop($src, $options['width'], $options['height'], $options['x'], $options['y'], $destformat, $options['jpgQuality']); + } + break; + + case 'fitsquare': + return (bool)$this->imgSquareFit($src, $options['width'], $options['height'], 'center', 'middle', $options['bgcolor'], $destformat, $options['jpgQuality']); + + } + return false; + } + + /** + * Convert Video To Image by ffmpeg + * + * @param string $file video source file path + * @param array $stat file stat array + * @param object $self volume driver object + * @param int $ss start seconds + * + * @return bool + * @throws elFinderAbortException + * @author Naoki Sawada + */ + public function ffmpegToImg($file, $stat, $self, $ss = null) + { + $name = basename($file); + $path = dirname($file); + $tmp = $path . DIRECTORY_SEPARATOR . md5($name); + // register auto delete on shutdown + $GLOBALS['elFinderTempFiles'][$tmp] = true; + if (rename($file, $tmp)) { + if ($ss === null) { + // specific start time by file name (xxx^[sec].[extention] - video^3.mp4) + if (preg_match('/\^(\d+(?:\.\d+)?)\.[^.]+$/', $stat['name'], $_m)) { + $ss = $_m[1]; + } else { + $ss = $this->options['tmbVideoConvSec']; + } + } + $cmd = sprintf(ELFINDER_FFMPEG_PATH . ' -i %s -ss 00:00:%.3f -vframes 1 -f image2 -- %s', escapeshellarg($tmp), $ss, escapeshellarg($file)); + $r = ($this->procExec($cmd) === 0); + clearstatcache(); + if ($r && $ss > 0 && !file_exists($file)) { + // Retry by half of $ss + $ss = max(intval($ss / 2), 0); + rename($tmp, $file); + $r = $this->ffmpegToImg($file, $stat, $self, $ss); + } else { + unlink($tmp); + } + return $r; + } + return false; + } + + /** + * Creates a temporary file and return file pointer + * + * @return resource|boolean + */ + public function tmpfile() + { + if ($tmp = $this->getTempFile()) { + return fopen($tmp, 'wb'); + } + return false; + } + + /** + * Save error message + * + * @param array error + * + * @return boolean false + * @author Naoki Sawada + **/ + protected function setError() + { + $this->error = array(); + $this->addError(func_get_args()); + return false; + } + + /** + * Add error message + * + * @param array error + * + * @return false + * @author Dmitry(dio) Levashov + **/ + protected function addError() + { + foreach (func_get_args() as $err) { + if (is_array($err)) { + foreach($err as $er) { + $this->addError($er); + } + } else { + $this->error[] = (string)$err; + } + } + return false; + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /***************** server encoding support *******************/ + + /** + * Return parent directory path (with convert encoding) + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function dirnameCE($path) + { + $dirname = (!$this->encoding) ? $this->_dirname($path) : $this->convEncOut($this->_dirname($this->convEncIn($path))); + // check to infinite loop prevention + return ($dirname != $path) ? $dirname : ''; + } + + /** + * Return file name (with convert encoding) + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function basenameCE($path) + { + return (!$this->encoding) ? $this->_basename($path) : $this->convEncOut($this->_basename($this->convEncIn($path))); + } + + /** + * Join dir name and file name and return full path. (with convert encoding) + * Some drivers (db) use int as path - so we give to concat path to driver itself + * + * @param string $dir dir path + * @param string $name file name + * + * @return string + * @author Naoki Sawada + **/ + protected function joinPathCE($dir, $name) + { + return (!$this->encoding) ? $this->_joinPath($dir, $name) : $this->convEncOut($this->_joinPath($this->convEncIn($dir), $this->convEncIn($name))); + } + + /** + * Return normalized path (with convert encoding) + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function normpathCE($path) + { + return (!$this->encoding) ? $this->_normpath($path) : $this->convEncOut($this->_normpath($this->convEncIn($path))); + } + + /** + * Return file path related to root dir (with convert encoding) + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function relpathCE($path) + { + return (!$this->encoding) ? $this->_relpath($path) : $this->convEncOut($this->_relpath($this->convEncIn($path))); + } + + /** + * Convert path related to root dir into real path (with convert encoding) + * + * @param string $path rel file path + * + * @return string + * @author Naoki Sawada + **/ + protected function abspathCE($path) + { + return (!$this->encoding) ? $this->_abspath($path) : $this->convEncOut($this->_abspath($this->convEncIn($path))); + } + + /** + * Return true if $path is children of $parent (with convert encoding) + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Naoki Sawada + **/ + protected function inpathCE($path, $parent) + { + return (!$this->encoding) ? $this->_inpath($path, $parent) : $this->convEncOut($this->_inpath($this->convEncIn($path), $this->convEncIn($parent))); + } + + /** + * Open file and return file pointer (with convert encoding) + * + * @param string $path file path + * @param string $mode + * + * @return false|resource + * @internal param bool $write open file for writing + * @author Naoki Sawada + */ + protected function fopenCE($path, $mode = 'rb') + { + // check extra option for network stream pointer + if (func_num_args() > 2) { + $opts = func_get_arg(2); + } else { + $opts = array(); + } + return (!$this->encoding) ? $this->_fopen($path, $mode, $opts) : $this->convEncOut($this->_fopen($this->convEncIn($path), $mode, $opts)); + } + + /** + * Close opened file (with convert encoding) + * + * @param resource $fp file pointer + * @param string $path file path + * + * @return bool + * @author Naoki Sawada + **/ + protected function fcloseCE($fp, $path = '') + { + return (!$this->encoding) ? $this->_fclose($fp, $path) : $this->convEncOut($this->_fclose($fp, $this->convEncIn($path))); + } + + /** + * Create new file and write into it from file pointer. (with convert encoding) + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param string $dir target dir path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Naoki Sawada + **/ + protected function saveCE($fp, $dir, $name, $stat) + { + $res = (!$this->encoding) ? $this->_save($fp, $dir, $name, $stat) : $this->convEncOut($this->_save($fp, $this->convEncIn($dir), $this->convEncIn($name), $this->convEncIn($stat))); + if ($res !== false) { + $this->clearstatcache(); + } + return $res; + } + + /** + * Return true if path is dir and has at least one childs directory (with convert encoding) + * + * @param string $path dir path + * + * @return bool + * @author Naoki Sawada + **/ + protected function subdirsCE($path) + { + if ($this->sessionCaching['subdirs']) { + if (isset($this->sessionCache['subdirs'][$path]) && !$this->isMyReload()) { + return $this->sessionCache['subdirs'][$path]; + } + } + $hasdir = (bool)((!$this->encoding) ? $this->_subdirs($path) : $this->convEncOut($this->_subdirs($this->convEncIn($path)))); + $this->updateSubdirsCache($path, $hasdir); + return $hasdir; + } + + /** + * Return files list in directory (with convert encoding) + * + * @param string $path dir path + * + * @return array + * @author Naoki Sawada + **/ + protected function scandirCE($path) + { + return (!$this->encoding) ? $this->_scandir($path) : $this->convEncOut($this->_scandir($this->convEncIn($path))); + } + + /** + * Create symlink (with convert encoding) + * + * @param string $source file to link to + * @param string $targetDir folder to create link in + * @param string $name symlink name + * + * @return bool + * @author Naoki Sawada + **/ + protected function symlinkCE($source, $targetDir, $name) + { + return (!$this->encoding) ? $this->_symlink($source, $targetDir, $name) : $this->convEncOut($this->_symlink($this->convEncIn($source), $this->convEncIn($targetDir), $this->convEncIn($name))); + } + + /***************** paths *******************/ + + /** + * Encode path into hash + * + * @param string file path + * + * @return string + * @author Dmitry (dio) Levashov + * @author Troex Nevelin + **/ + protected function encode($path) + { + if ($path !== '') { + + // cut ROOT from $path for security reason, even if hacker decodes the path he will not know the root + $p = $this->relpathCE($path); + // if reqesting root dir $path will be empty, then assign '/' as we cannot leave it blank for crypt + if ($p === '') { + $p = $this->separator; + } + // change separator + if ($this->separatorForHash) { + $p = str_replace($this->separator, $this->separatorForHash, $p); + } + // TODO crypt path and return hash + $hash = $this->crypt($p); + // hash is used as id in HTML that means it must contain vaild chars + // make base64 html safe and append prefix in begining + $hash = strtr(base64_encode($hash), '+/=', '-_.'); + // remove dots '.' at the end, before it was '=' in base64 + $hash = rtrim($hash, '.'); + // append volume id to make hash unique + return $this->id . $hash; + } + //TODO: Add return statement here + } + + /** + * Decode path from hash + * + * @param string file hash + * + * @return string + * @author Dmitry (dio) Levashov + * @author Troex Nevelin + **/ + protected function decode($hash) + { + if (strpos($hash, $this->id) === 0) { + // cut volume id after it was prepended in encode + $h = substr($hash, strlen($this->id)); + // replace HTML safe base64 to normal + $h = base64_decode(strtr($h, '-_.', '+/=')); + // TODO uncrypt hash and return path + $path = $this->uncrypt($h); + // change separator + if ($this->separatorForHash) { + $path = str_replace($this->separatorForHash, $this->separator, $path); + } + // append ROOT to path after it was cut in encode + return $this->abspathCE($path);//$this->root.($path === $this->separator ? '' : $this->separator.$path); + } + return ''; + } + + /** + * Return crypted path + * Not implemented + * + * @param string path + * + * @return mixed + * @author Dmitry (dio) Levashov + **/ + protected function crypt($path) + { + return $path; + } + + /** + * Return uncrypted path + * Not implemented + * + * @param mixed hash + * + * @return mixed + * @author Dmitry (dio) Levashov + **/ + protected function uncrypt($hash) + { + return $hash; + } + + /** + * Validate file name based on $this->options['acceptedName'] regexp or function + * + * @param string $name file name + * @param bool $isDir + * + * @return bool + * @author Dmitry (dio) Levashov + */ + protected function nameAccepted($name, $isDir = false) + { + if (json_encode($name) === false) { + return false; + } + $nameValidator = $isDir ? $this->dirnameValidator : $this->nameValidator; + if ($nameValidator) { + if (is_callable($nameValidator)) { + $res = call_user_func($nameValidator, $name); + return $res; + } + if (preg_match($nameValidator, '') !== false) { + return preg_match($nameValidator, $name); + } + } + return true; + } + + /** + * Return session rootstat cache key + * + * @return string + */ + protected function getRootstatCachekey() + { + return md5($this->root . (isset($this->options['alias']) ? $this->options['alias'] : '')); + } + + /** + * Return new unique name based on file name and suffix + * + * @param $dir + * @param $name + * @param string $suffix suffix append to name + * @param bool $checkNum + * @param int $start + * + * @return string + * @internal param string $path file path + * @author Dmitry (dio) Levashov + */ + public function uniqueName($dir, $name, $suffix = ' copy', $checkNum = true, $start = 1) + { + static $lasts = null; + + if ($lasts === null) { + $lasts = array(); + } + + $ext = ''; + + $splits = elFinder::splitFileExtention($name); + if ($splits[1]) { + $ext = '.' . $splits[1]; + $name = $splits[0]; + } + + if ($checkNum && preg_match('/(' . preg_quote($suffix, '/') . ')(\d*)$/i', $name, $m)) { + $i = (int)$m[2]; + $name = substr($name, 0, strlen($name) - strlen($m[2])); + } else { + $i = $start; + $name .= $suffix; + } + $max = $i + 100000; + + if (isset($lasts[$name])) { + $i = max($i, $lasts[$name]); + } + + while ($i <= $max) { + $n = $name . ($i > 0 ? sprintf($this->options['uniqueNumFormat'], $i) : '') . $ext; + + if (!$this->isNameExists($this->joinPathCE($dir, $n))) { + $this->clearcache(); + $lasts[$name] = ++$i; + return $n; + } + $i++; + } + return $name . md5($dir) . $ext; + } + + /** + * Converts character encoding from UTF-8 to server's one + * + * @param mixed $var target string or array var + * @param bool $restoreLocale do retore global locale, default is false + * @param string $unknown replaces character for unknown + * + * @return mixed + * @author Naoki Sawada + */ + public function convEncIn($var = null, $restoreLocale = false, $unknown = '_') + { + return (!$this->encoding) ? $var : $this->convEnc($var, 'UTF-8', $this->encoding, $this->options['locale'], $restoreLocale, $unknown); + } + + /** + * Converts character encoding from server's one to UTF-8 + * + * @param mixed $var target string or array var + * @param bool $restoreLocale do retore global locale, default is true + * @param string $unknown replaces character for unknown + * + * @return mixed + * @author Naoki Sawada + */ + public function convEncOut($var = null, $restoreLocale = true, $unknown = '_') + { + return (!$this->encoding) ? $var : $this->convEnc($var, $this->encoding, 'UTF-8', $this->options['locale'], $restoreLocale, $unknown); + } + + /** + * Converts character encoding (base function) + * + * @param mixed $var target string or array var + * @param string $from from character encoding + * @param string $to to character encoding + * @param string $locale local locale + * @param $restoreLocale + * @param string $unknown replaces character for unknown + * + * @return mixed + */ + protected function convEnc($var, $from, $to, $locale, $restoreLocale, $unknown = '_') + { + if (strtoupper($from) !== strtoupper($to)) { + if ($locale) { + setlocale(LC_ALL, $locale); + } + if (is_array($var)) { + $_ret = array(); + foreach ($var as $_k => $_v) { + $_ret[$_k] = $this->convEnc($_v, $from, $to, '', false, $unknown = '_'); + } + $var = $_ret; + } else { + $_var = false; + if (is_string($var)) { + $_var = $var; + $errlev = error_reporting(); + error_reporting($errlev ^ E_NOTICE); + if (false !== ($_var = iconv($from, $to . '//TRANSLIT', $_var))) { + $_var = str_replace('?', $unknown, $_var); + } + error_reporting($errlev); + } + if ($_var !== false) { + $var = $_var; + } + } + if ($restoreLocale) { + setlocale(LC_ALL, elFinder::$locale); + } + } + return $var; + } + + /** + * Normalize MIME-Type by options['mimeMap'] + * + * @param string $type MIME-Type + * @param string $name Filename + * @param string $ext File extention without first dot (optional) + * + * @return string Normalized MIME-Type + */ + public function mimeTypeNormalize($type, $name, $ext = '') + { + if ($ext === '') { + $ext = (false === $pos = strrpos($name, '.')) ? '' : substr($name, $pos + 1); + } + $_checkKey = strtolower($ext . ':' . $type); + if ($type === '') { + $_keylen = strlen($_checkKey); + foreach ($this->options['mimeMap'] as $_key => $_type) { + if (substr($_key, 0, $_keylen) === $_checkKey) { + $type = $_type; + break; + } + } + } else if (isset($this->options['mimeMap'][$_checkKey])) { + $type = $this->options['mimeMap'][$_checkKey]; + } else { + $_checkKey = strtolower($ext . ':*'); + if (isset($this->options['mimeMap'][$_checkKey])) { + $type = $this->options['mimeMap'][$_checkKey]; + } else { + $_checkKey = strtolower('*:' . $type); + if (isset($this->options['mimeMap'][$_checkKey])) { + $type = $this->options['mimeMap'][$_checkKey]; + } + } + } + return $type; + } + + /*********************** util mainly for inheritance class *********************/ + + /** + * Get temporary filename. Tempfile will be removed when after script execution finishes or exit() is called. + * When needing the unique file to a path, give $path to parameter. + * + * @param string $path for get unique file to a path + * + * @return string|false + * @author Naoki Sawada + */ + protected function getTempFile($path = '') + { + static $cache = array(); + + $key = ''; + if ($path !== '') { + $key = $this->id . '#' . $path; + if (isset($cache[$key])) { + return $cache[$key]; + } + } + + if ($tmpdir = $this->getTempPath()) { + $name = tempnam($tmpdir, 'ELF'); + if ($key) { + $cache[$key] = $name; + } + // register auto delete on shutdown + $GLOBALS['elFinderTempFiles'][$name] = true; + return $name; + } + + return false; + } + + /** + * File path of local server side work file path + * + * @param string $path path need convert encoding to server encoding + * + * @return string + * @author Naoki Sawada + */ + protected function getWorkFile($path) + { + if ($wfp = $this->tmpfile()) { + if ($fp = $this->_fopen($path)) { + while (!feof($fp)) { + fwrite($wfp, fread($fp, 8192)); + } + $info = stream_get_meta_data($wfp); + fclose($wfp); + if ($info && !empty($info['uri'])) { + return $info['uri']; + } + } + } + return false; + } + + /** + * Get image size array with `dimensions` + * + * @param string $path path need convert encoding to server encoding + * @param string $mime file mime type + * + * @return array|false + * @throws ImagickException + * @throws elFinderAbortException + */ + public function getImageSize($path, $mime = '') + { + $size = false; + if ($mime === '' || strtolower(substr($mime, 0, 5)) === 'image') { + if ($work = $this->getWorkFile($path)) { + if ($size = getimagesize($work)) { + $size['dimensions'] = $size[0] . 'x' . $size[1]; + $srcfp = fopen($work, 'rb'); + $cArgs = elFinder::$currentArgs; + if (!empty($cArgs['target']) && $subImgLink = $this->getSubstituteImgLink($cArgs['target'], $size, $srcfp)) { + $size['url'] = $subImgLink; + } + } + } + is_file($work) && unlink($work); + } + return $size; + } + + /** + * Delete dirctory trees + * + * @param string $localpath path need convert encoding to server encoding + * + * @return boolean + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function delTree($localpath) + { + foreach ($this->_scandir($localpath) as $p) { + elFinder::checkAborted(); + $stat = $this->stat($this->convEncOut($p)); + $this->convEncIn(); + ($stat['mime'] === 'directory') ? $this->delTree($p) : $this->_unlink($p); + } + $res = $this->_rmdir($localpath); + $res && $this->clearstatcache(); + return $res; + } + + /** + * Copy items to a new temporary directory on the local server + * + * @param array $hashes target hashes + * @param string $dir destination directory (for recurcive) + * @param string $canLink it can use link() (for recurcive) + * + * @return string|false saved path name + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function getItemsInHand($hashes, $dir = null, $canLink = null) + { + static $banChrs = null; + static $totalSize = 0; + + if (is_null($banChrs)) { + $banChrs = DIRECTORY_SEPARATOR !== '/'? array('\\', '/', ':', '*', '?', '"', '<', '>', '|') : array('\\', '/'); + } + + if (is_null($dir)) { + $totalSize = 0; + if (!$tmpDir = $this->getTempPath()) { + return false; + } + $dir = tempnam($tmpDir, 'elf'); + if (!unlink($dir) || !mkdir($dir, 0700, true)) { + return false; + } + register_shutdown_function(array($this, 'rmdirRecursive'), $dir); + } + if (is_null($canLink)) { + $canLink = ($this instanceof elFinderVolumeLocalFileSystem); + } + elFinder::checkAborted(); + $res = true; + $files = array(); + foreach ($hashes as $hash) { + if (($file = $this->file($hash)) == false) { + continue; + } + if (!$file['read']) { + continue; + } + + $name = $file['name']; + // remove ctrl characters + $name = preg_replace('/[[:cntrl:]]+/', '', $name); + // replace ban characters + $name = str_replace($banChrs, '_', $name); + + // for call from search results + if (isset($files[$name])) { + $name = preg_replace('/^(.*?)(\..*)?$/', '$1_' . $files[$name]++ . '$2', $name); + } else { + $files[$name] = 1; + } + $target = $dir . DIRECTORY_SEPARATOR . $name; + + if ($file['mime'] === 'directory') { + $chashes = array(); + $_files = $this->scandir($hash); + foreach ($_files as $_file) { + if ($file['read']) { + $chashes[] = $_file['hash']; + } + } + if (($res = mkdir($target, 0700, true)) && $chashes) { + $res = $this->getItemsInHand($chashes, $target, $canLink); + } + if (!$res) { + break; + } + !empty($file['ts']) && touch($target, $file['ts']); + } else { + $path = $this->decode($hash); + if (!$canLink || !($canLink = $this->localFileSystemSymlink($path, $target))) { + if (file_exists($target)) { + unlink($target); + } + if ($fp = $this->fopenCE($path)) { + if ($tfp = fopen($target, 'wb')) { + $totalSize += stream_copy_to_stream($fp, $tfp); + fclose($tfp); + } + !empty($file['ts']) && touch($target, $file['ts']); + $this->fcloseCE($fp, $path); + } + } else { + $totalSize += filesize($path); + } + if ($this->options['maxArcFilesSize'] > 0 && $this->options['maxArcFilesSize'] < $totalSize) { + $res = $this->setError(elFinder::ERROR_ARC_MAXSIZE); + } + } + } + return $res ? $dir : false; + } + + /*********************** file stat *********************/ + + /** + * Check file attribute + * + * @param string $path file path + * @param string $name attribute name (read|write|locked|hidden) + * @param bool $val attribute value returned by file system + * @param bool $isDir path is directory (true: directory, false: file) + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function attr($path, $name, $val = null, $isDir = null) + { + if (!isset($this->defaults[$name])) { + return false; + } + + $relpath = $this->relpathCE($path); + if ($this->separator !== '/') { + $relpath = str_replace($this->separator, '/', $relpath); + } + $relpath = '/' . $relpath; + + $perm = null; + + if ($this->access) { + $perm = call_user_func($this->access, $name, $path, $this->options['accessControlData'], $this, $isDir, $relpath); + if ($perm !== null) { + return !!$perm; + } + } + + foreach ($this->attributes as $attrs) { + if (isset($attrs[$name]) && isset($attrs['pattern']) && preg_match($attrs['pattern'], $relpath)) { + $perm = $attrs[$name]; + break; + } + } + + return $perm === null ? (is_null($val) ? $this->defaults[$name] : $val) : !!$perm; + } + + /** + * Return true if file with given name can be created in given folder. + * + * @param string $dir parent dir path + * @param string $name new file name + * @param null $isDir + * + * @return bool + * @author Dmitry (dio) Levashov + */ + protected function allowCreate($dir, $name, $isDir = null) + { + return $this->attr($this->joinPathCE($dir, $name), 'write', true, $isDir); + } + + /** + * Return true if file MIME type can save with check uploadOrder config. + * + * @param string $mime + * + * @return boolean + */ + protected function allowPutMime($mime) + { + // logic based on http://httpd.apache.org/docs/2.2/mod/mod_authz_host.html#order + $allow = $this->mimeAccepted($mime, $this->uploadAllow, null); + $deny = $this->mimeAccepted($mime, $this->uploadDeny, null); + if (strtolower($this->uploadOrder[0]) == 'allow') { // array('allow', 'deny'), default is to 'deny' + $res = false; // default is deny + if (!$deny && ($allow === true)) { // match only allow + $res = true; + }// else (both match | no match | match only deny) { deny } + } else { // array('deny', 'allow'), default is to 'allow' - this is the default rule + $res = true; // default is allow + if (($deny === true) && !$allow) { // match only deny + $res = false; + } // else (both match | no match | match only allow) { allow } + } + return $res; + } + + /** + * Return fileinfo + * + * @param string $path file cache + * + * @return array|bool + * @author Dmitry (dio) Levashov + **/ + protected function stat($path) + { + if ($path === false || is_null($path)) { + return false; + } + $is_root = ($path == $this->root); + if ($is_root) { + $rootKey = $this->getRootstatCachekey(); + if ($this->sessionCaching['rootstat'] && !isset($this->sessionCache['rootstat'])) { + $this->sessionCache['rootstat'] = array(); + } + if (!isset($this->cache[$path]) && !$this->isMyReload()) { + // need $path as key for netmount/netunmount + if ($this->sessionCaching['rootstat'] && isset($this->sessionCache['rootstat'][$rootKey])) { + if ($ret = $this->sessionCache['rootstat'][$rootKey]) { + if ($this->options['rootRev'] === $ret['rootRev']) { + if (isset($this->options['phash'])) { + $ret['isroot'] = 1; + $ret['phash'] = $this->options['phash']; + } + return $ret; + } + } + } + } + } + $rootSessCache = false; + if (isset($this->cache[$path])) { + $ret = $this->cache[$path]; + } else { + if ($is_root && !empty($this->options['rapidRootStat']) && is_array($this->options['rapidRootStat']) && !$this->needOnline) { + $ret = $this->updateCache($path, $this->options['rapidRootStat'], true); + } else { + $ret = $this->updateCache($path, $this->convEncOut($this->_stat($this->convEncIn($path))), true); + if ($is_root && !empty($rootKey) && $this->sessionCaching['rootstat']) { + $rootSessCache = true; + } + } + } + if ($is_root) { + if ($ret) { + $this->rootModified = false; + if ($rootSessCache) { + $this->sessionCache['rootstat'][$rootKey] = $ret; + } + if (isset($this->options['phash'])) { + $ret['isroot'] = 1; + $ret['phash'] = $this->options['phash']; + } + } else if (!empty($rootKey) && $this->sessionCaching['rootstat']) { + unset($this->sessionCache['rootstat'][$rootKey]); + } + } + return $ret; + } + + /** + * Get root stat extra key values + * + * @return array stat extras + * @author Naoki Sawada + */ + protected function getRootStatExtra() + { + $stat = array(); + if ($this->rootName) { + $stat['name'] = $this->rootName; + } + $stat['rootRev'] = $this->options['rootRev']; + $stat['options'] = $this->options(null); + return $stat; + } + + /** + * Return fileinfo based on filename + * For item ID based path file system + * Please override if needed on each drivers + * + * @param string $path file cache + * + * @return array + */ + protected function isNameExists($path) + { + return $this->stat($path); + } + + /** + * Put file stat in cache and return it + * + * @param string $path file path + * @param array $stat file stat + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function updateCache($path, $stat) + { + if (empty($stat) || !is_array($stat)) { + return $this->cache[$path] = array(); + } + + if (func_num_args() > 2) { + $fromStat = func_get_arg(2); + } else { + $fromStat = false; + } + + $stat['hash'] = $this->encode($path); + + $root = $path == $this->root; + $parent = ''; + + if ($root) { + $stat = array_merge($stat, $this->getRootStatExtra()); + } else { + if (!isset($stat['name']) || $stat['name'] === '') { + $stat['name'] = $this->basenameCE($path); + } + if (empty($stat['phash'])) { + $parent = $this->dirnameCE($path); + $stat['phash'] = $this->encode($parent); + } else { + $parent = $this->decode($stat['phash']); + } + } + + // name check + if (isset($stat['name']) && !$jeName = json_encode($stat['name'])) { + return $this->cache[$path] = array(); + } + // fix name if required + if ($this->options['utf8fix'] && $this->options['utf8patterns'] && $this->options['utf8replace']) { + $stat['name'] = json_decode(str_replace($this->options['utf8patterns'], $this->options['utf8replace'], $jeName)); + } + + if (!isset($stat['size'])) { + $stat['size'] = 'unknown'; + } + + $mime = isset($stat['mime']) ? $stat['mime'] : ''; + if ($isDir = ($mime === 'directory')) { + $stat['volumeid'] = $this->id; + } else { + if (empty($stat['mime']) || $stat['size'] == 0) { + $stat['mime'] = $this->mimetype($stat['name'], true, $stat['size'], $mime); + } else { + $stat['mime'] = $this->mimeTypeNormalize($stat['mime'], $stat['name']); + } + } + + $stat['read'] = intval($this->attr($path, 'read', isset($stat['read']) ? !!$stat['read'] : null, $isDir)); + $stat['write'] = intval($this->attr($path, 'write', isset($stat['write']) ? !!$stat['write'] : null, $isDir)); + if ($root) { + $stat['locked'] = 1; + if ($this->options['type'] !== '') { + $stat['type'] = $this->options['type']; + } + } else { + // lock when parent directory is not writable + if (!isset($stat['locked'])) { + $pstat = $this->stat($parent); + if (isset($pstat['write']) && !$pstat['write']) { + $stat['locked'] = true; + } + } + if ($this->attr($path, 'locked', isset($stat['locked']) ? !!$stat['locked'] : null, $isDir)) { + $stat['locked'] = 1; + } else { + unset($stat['locked']); + } + } + + if ($root) { + unset($stat['hidden']); + } elseif ($this->attr($path, 'hidden', isset($stat['hidden']) ? !!$stat['hidden'] : null, $isDir) + || !$this->mimeAccepted($stat['mime'])) { + $stat['hidden'] = 1; + } else { + unset($stat['hidden']); + } + + if ($stat['read'] && empty($stat['hidden'])) { + + if ($isDir) { + // caching parent's subdirs + if ($parent) { + $this->updateSubdirsCache($parent, true); + } + // for dir - check for subdirs + if ($this->options['checkSubfolders']) { + if (!isset($stat['dirs']) && intval($this->options['checkSubfolders']) === -1) { + $stat['dirs'] = -1; + } + if (isset($stat['dirs'])) { + if ($stat['dirs']) { + if ($stat['dirs'] == -1) { + $stat['dirs'] = ($this->sessionCaching['subdirs'] && isset($this->sessionCache['subdirs'][$path])) ? (int)$this->sessionCache['subdirs'][$path] : -1; + } else { + $stat['dirs'] = 1; + } + } else { + unset($stat['dirs']); + } + } elseif (!empty($stat['alias']) && !empty($stat['target'])) { + $stat['dirs'] = isset($this->cache[$stat['target']]) + ? intval(isset($this->cache[$stat['target']]['dirs'])) + : $this->subdirsCE($stat['target']); + + } elseif ($this->subdirsCE($path)) { + $stat['dirs'] = 1; + } + } else { + $stat['dirs'] = 1; + } + if ($this->options['dirUrlOwn'] === true) { + // Set `null` to use the client option `commandsOptions.info.nullUrlDirLinkSelf = true` + $stat['url'] = null; + } else if ($this->options['dirUrlOwn'] === 'hide') { + // to hide link in info dialog of the elFinder client + $stat['url'] = ''; + } + } else { + // for files - check for thumbnails + $p = isset($stat['target']) ? $stat['target'] : $path; + if ($this->tmbURL && !isset($stat['tmb']) && $this->canCreateTmb($p, $stat)) { + $tmb = $this->gettmb($p, $stat); + $stat['tmb'] = $tmb ? $tmb : 1; + } + + } + if (!isset($stat['url']) && $this->URL && $this->encoding) { + $_path = str_replace($this->separator, '/', substr($path, strlen($this->root) + 1)); + $stat['url'] = rtrim($this->URL, '/') . '/' . str_replace('%2F', '/', rawurlencode((substr(PHP_OS, 0, 3) === 'WIN') ? $_path : $this->convEncIn($_path, true))); + } + } else { + if ($isDir) { + unset($stat['dirs']); + } + } + + if (!empty($stat['alias']) && !empty($stat['target'])) { + $stat['thash'] = $this->encode($stat['target']); + //$this->cache[$stat['target']] = $stat; + unset($stat['target']); + } + + $this->cache[$path] = $stat; + + if (!$fromStat && $root && $this->sessionCaching['rootstat']) { + // to update session cache + $this->stat($path); + } + + return $stat; + } + + /** + * Get stat for folder content and put in cache + * + * @param string $path + * + * @return void + * @author Dmitry (dio) Levashov + **/ + protected function cacheDir($path) + { + $this->dirsCache[$path] = array(); + $hasDir = false; + + foreach ($this->scandirCE($path) as $p) { + if (($stat = $this->stat($p)) && empty($stat['hidden'])) { + if (!$hasDir && $stat['mime'] === 'directory') { + $hasDir = true; + } + $this->dirsCache[$path][] = $p; + } + } + + $this->updateSubdirsCache($path, $hasDir); + } + + /** + * Clean cache + * + * @return void + * @author Dmitry (dio) Levashov + **/ + protected function clearcache() + { + $this->cache = $this->dirsCache = array(); + } + + /** + * Return file mimetype + * + * @param string $path file path + * @param string|bool $name + * @param integer $size + * @param string $mime was notified from the volume driver + * + * @return string + * @author Dmitry (dio) Levashov + */ + protected function mimetype($path, $name = '', $size = null, $mime = null) + { + $type = ''; + $nameCheck = false; + + if ($name === '') { + $name = $path; + } else if ($name === true) { + $name = $path; + $nameCheck = true; + } + if (!$this instanceof elFinderVolumeLocalFileSystem) { + $nameCheck = true; + } + $ext = (false === $pos = strrpos($name, '.')) ? '' : strtolower(substr($name, $pos + 1)); + if (!$nameCheck && $size === null) { + $size = file_exists($path) ? filesize($path) : -1; + } + if (!$nameCheck && is_readable($path) && $size > 0) { + // detecting by contents + if ($this->mimeDetect === 'finfo') { + $type = finfo_file($this->finfo, $path); + } else if ($this->mimeDetect === 'mime_content_type') { + $type = mime_content_type($path); + } + if ($type) { + $type = explode(';', $type); + $type = trim($type[0]); + if ($ext && preg_match('~^application/(?:octet-stream|(?:x-)?zip|xml)$~', $type)) { + // load default MIME table file "mime.types" + if (!elFinderVolumeDriver::$mimetypesLoaded) { + elFinderVolumeDriver::loadMimeTypes(); + } + if (isset(elFinderVolumeDriver::$mimetypes[$ext])) { + $type = elFinderVolumeDriver::$mimetypes[$ext]; + } + } else if ($ext === 'js' && preg_match('~^text/~', $type)) { + $type = 'text/javascript'; + } + } + } + if (!$type) { + // detecting by filename + $type = elFinderVolumeDriver::mimetypeInternalDetect($name); + if ($type === 'unknown') { + if ($mime) { + $type = $mime; + } else { + $type = ($size == 0) ? 'text/plain' : $this->options['mimeTypeUnknown']; + } + } + } + + // mime type normalization + $type = $this->mimeTypeNormalize($type, $name, $ext); + + return $type; + } + + /** + * Load file of mime.types + * + * @param string $mimeTypesFile The mime types file + */ + static protected function loadMimeTypes($mimeTypesFile = '') + { + if (!elFinderVolumeDriver::$mimetypesLoaded) { + elFinderVolumeDriver::$mimetypesLoaded = true; + $file = false; + if (!empty($mimeTypesFile) && file_exists($mimeTypesFile)) { + $file = $mimeTypesFile; + } elseif (elFinder::$defaultMimefile && file_exists(elFinder::$defaultMimefile)) { + $file = elFinder::$defaultMimefile; + } elseif (file_exists(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mime.types')) { + $file = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'mime.types'; + } elseif (file_exists(dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'mime.types')) { + $file = dirname(dirname(__FILE__)) . DIRECTORY_SEPARATOR . 'mime.types'; + } + + if ($file && file_exists($file)) { + $mimecf = file($file); + + foreach ($mimecf as $line_num => $line) { + if (!preg_match('/^\s*#/', $line)) { + $mime = preg_split('/\s+/', $line, -1, PREG_SPLIT_NO_EMPTY); + for ($i = 1, $size = count($mime); $i < $size; $i++) { + if (!isset(self::$mimetypes[$mime[$i]])) { + self::$mimetypes[$mime[$i]] = $mime[0]; + } + } + } + } + } + } + } + + /** + * Detect file mimetype using "internal" method or Loading mime.types with $path = '' + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + static protected function mimetypeInternalDetect($path = '') + { + // load default MIME table file "mime.types" + if (!elFinderVolumeDriver::$mimetypesLoaded) { + elFinderVolumeDriver::loadMimeTypes(); + } + $ext = ''; + if ($path) { + $pinfo = pathinfo($path); + $ext = isset($pinfo['extension']) ? strtolower($pinfo['extension']) : ''; + } + return ($ext && isset(elFinderVolumeDriver::$mimetypes[$ext])) ? elFinderVolumeDriver::$mimetypes[$ext] : 'unknown'; + } + + /** + * Return file/total directory size infomation + * + * @param string $path file path + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function countSize($path) + { + + elFinder::checkAborted(); + + $result = array('size' => 0, 'files' => 0, 'dirs' => 0); + $stat = $this->stat($path); + + if (empty($stat) || !$stat['read'] || !empty($stat['hidden'])) { + $result['size'] = 'unknown'; + return $result; + } + + if ($stat['mime'] !== 'directory') { + $result['size'] = intval($stat['size']); + $result['files'] = 1; + return $result; + } + + $result['dirs'] = 1; + $subdirs = $this->options['checkSubfolders']; + $this->options['checkSubfolders'] = true; + foreach ($this->getScandir($path) as $stat) { + if ($isDir = ($stat['mime'] === 'directory' && $stat['read'])) { + ++$result['dirs']; + } else { + ++$result['files']; + } + $res = $isDir + ? $this->countSize($this->decode($stat['hash'])) + : (isset($stat['size']) ? array('size' => intval($stat['size'])) : array()); + if (!empty($res['size']) && is_numeric($res['size'])) { + $result['size'] += $res['size']; + } + if (!empty($res['files']) && is_numeric($res['files'])) { + $result['files'] += $res['files']; + } + if (!empty($res['dirs']) && is_numeric($res['dirs'])) { + $result['dirs'] += $res['dirs']; + --$result['dirs']; + } + } + $this->options['checkSubfolders'] = $subdirs; + return $result; + } + + /** + * Return true if all mimes is directory or files + * + * @param string $mime1 mimetype + * @param string $mime2 mimetype + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function isSameType($mime1, $mime2) + { + return ($mime1 == 'directory' && $mime1 == $mime2) || ($mime1 != 'directory' && $mime2 != 'directory'); + } + + /** + * If file has required attr == $val - return file path, + * If dir has child with has required attr == $val - return child path + * + * @param string $path file path + * @param string $attr attribute name + * @param bool $val attribute value + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function closestByAttr($path, $attr, $val) + { + $stat = $this->stat($path); + + if (empty($stat)) { + return false; + } + + $v = isset($stat[$attr]) ? $stat[$attr] : false; + + if ($v == $val) { + return $path; + } + + return $stat['mime'] == 'directory' + ? $this->childsByAttr($path, $attr, $val) + : false; + } + + /** + * Return first found children with required attr == $val + * + * @param string $path file path + * @param string $attr attribute name + * @param bool $val attribute value + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function childsByAttr($path, $attr, $val) + { + foreach ($this->scandirCE($path) as $p) { + if (($_p = $this->closestByAttr($p, $attr, $val)) != false) { + return $_p; + } + } + return false; + } + + protected function isMyReload($target = '', $ARGtarget = '') + { + if ($this->rootModified || (!empty($this->ARGS['cmd']) && $this->ARGS['cmd'] === 'parents')) { + return true; + } + if (!empty($this->ARGS['reload'])) { + if ($ARGtarget === '') { + $ARGtarget = isset($this->ARGS['target']) ? $this->ARGS['target'] + : ((isset($this->ARGS['targets']) && is_array($this->ARGS['targets']) && count($this->ARGS['targets']) === 1) ? + $this->ARGS['targets'][0] : ''); + } + if ($ARGtarget !== '') { + $ARGtarget = strval($ARGtarget); + if ($target === '') { + return (strpos($ARGtarget, $this->id) === 0); + } else { + $target = strval($target); + return ($target === $ARGtarget); + } + } + } + return false; + } + + /** + * Update subdirs cache data + * + * @param string $path + * @param bool $subdirs + * + * @return void + */ + protected function updateSubdirsCache($path, $subdirs) + { + if (isset($this->cache[$path])) { + if ($subdirs) { + $this->cache[$path]['dirs'] = 1; + } else { + unset($this->cache[$path]['dirs']); + } + } + if ($this->sessionCaching['subdirs']) { + $this->sessionCache['subdirs'][$path] = $subdirs; + } + if ($this->sessionCaching['rootstat'] && $path == $this->root) { + unset($this->sessionCache['rootstat'][$this->getRootstatCachekey()]); + } + } + + /***************** get content *******************/ + + /** + * Return required dir's files info. + * If onlyMimes is set - return only dirs and files of required mimes + * + * @param string $path dir path + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function getScandir($path) + { + $files = array(); + + !isset($this->dirsCache[$path]) && $this->cacheDir($path); + + foreach ($this->dirsCache[$path] as $p) { + if (($stat = $this->stat($p)) && empty($stat['hidden'])) { + $files[] = $stat; + } + } + + return $files; + } + + + /** + * Return subdirs tree + * + * @param string $path parent dir path + * @param int $deep tree deep + * @param string $exclude + * + * @return array + * @author Dmitry (dio) Levashov + */ + protected function gettree($path, $deep, $exclude = '') + { + $dirs = array(); + + !isset($this->dirsCache[$path]) && $this->cacheDir($path); + + foreach ($this->dirsCache[$path] as $p) { + $stat = $this->stat($p); + + if ($stat && empty($stat['hidden']) && $p != $exclude && $stat['mime'] == 'directory') { + $dirs[] = $stat; + if ($deep > 0 && !empty($stat['dirs'])) { + $dirs = array_merge($dirs, $this->gettree($p, $deep - 1)); + } + } + } + + return $dirs; + } + + /** + * Recursive files search + * + * @param string $path dir path + * @param string $q search string + * @param array $mimes + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function doSearch($path, $q, $mimes) + { + $result = array(); + $matchMethod = empty($this->doSearchCurrentQuery['matchMethod']) ? 'searchMatchName' : $this->doSearchCurrentQuery['matchMethod']; + $timeout = $this->options['searchTimeout'] ? $this->searchStart + $this->options['searchTimeout'] : 0; + if ($timeout && $timeout < time()) { + $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->path($this->encode($path))); + return $result; + } + + foreach ($this->scandirCE($path) as $p) { + elFinder::extendTimeLimit($this->options['searchTimeout'] + 30); + + if ($timeout && ($this->error || $timeout < time())) { + !$this->error && $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->path($this->encode($path))); + break; + } + + + $stat = $this->stat($p); + + if (!$stat) { // invalid links + continue; + } + + if (!empty($stat['hidden']) || !$this->mimeAccepted($stat['mime'], $mimes)) { + continue; + } + + $name = $stat['name']; + + if ($this->doSearchCurrentQuery['excludes']) { + foreach ($this->doSearchCurrentQuery['excludes'] as $exclude) { + if ($this->stripos($name, $exclude) !== false) { + continue 2; + } + } + } + + if ((!$mimes || $stat['mime'] !== 'directory') && $this->$matchMethod($name, $q, $p) !== false) { + $stat['path'] = $this->path($stat['hash']); + if ($this->URL && !isset($stat['url'])) { + $path = str_replace($this->separator, '/', substr($p, strlen($this->root) + 1)); + if ($this->encoding) { + $path = str_replace('%2F', '/', rawurlencode($this->convEncIn($path, true))); + } else { + $path = str_replace('%2F', '/', rawurlencode($path)); + } + $stat['url'] = $this->URL . $path; + } + + $result[] = $stat; + } + if ($stat['mime'] == 'directory' && $stat['read'] && !isset($stat['alias'])) { + if (!$this->options['searchExDirReg'] || !preg_match($this->options['searchExDirReg'], $p)) { + $result = array_merge($result, $this->doSearch($p, $q, $mimes)); + } + } + } + + return $result; + } + + /********************** manuipulations ******************/ + + /** + * Copy file/recursive copy dir only in current volume. + * Return new file path or false. + * + * @param string $src source path + * @param string $dst destination dir path + * @param string $name new file name (optionaly) + * + * @return string|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function copy($src, $dst, $name) + { + + elFinder::checkAborted(); + + $srcStat = $this->stat($src); + + if (!empty($srcStat['thash'])) { + $target = $this->decode($srcStat['thash']); + if (!$this->inpathCE($target, $this->root)) { + return $this->setError(elFinder::ERROR_COPY, $this->path($srcStat['hash']), elFinder::ERROR_MKOUTLINK); + } + $stat = $this->stat($target); + $this->clearcache(); + return $stat && $this->symlinkCE($target, $dst, $name) + ? $this->joinPathCE($dst, $name) + : $this->setError(elFinder::ERROR_COPY, $this->path($srcStat['hash'])); + } + + if ($srcStat['mime'] === 'directory') { + $testStat = $this->isNameExists($this->joinPathCE($dst, $name)); + $this->clearcache(); + + if (($testStat && $testStat['mime'] !== 'directory') || (!$testStat && !$testStat = $this->mkdir($this->encode($dst), $name))) { + return $this->setError(elFinder::ERROR_COPY, $this->path($srcStat['hash'])); + } + + $dst = $this->decode($testStat['hash']); + + // start time + $stime = microtime(true); + foreach ($this->getScandir($src) as $stat) { + if (empty($stat['hidden'])) { + // current time + $ctime = microtime(true); + if (($ctime - $stime) > 2) { + $stime = $ctime; + elFinder::checkAborted(); + } + $name = $stat['name']; + $_src = $this->decode($stat['hash']); + if (!$this->copy($_src, $dst, $name)) { + $this->remove($dst, true); // fall back + return $this->setError($this->error, elFinder::ERROR_COPY, $this->_path($src)); + } + } + } + + $this->added[] = $testStat; + + return $dst; + } + + if ($this->options['copyJoin']) { + $test = $this->joinPathCE($dst, $name); + if ($this->isNameExists($test)) { + $this->remove($test); + } + } + if ($res = $this->convEncOut($this->_copy($this->convEncIn($src), $this->convEncIn($dst), $this->convEncIn($name)))) { + $path = is_string($res) ? $res : $this->joinPathCE($dst, $name); + $this->clearstatcache(); + $this->added[] = $this->stat($path); + return $path; + } + + return $this->setError(elFinder::ERROR_COPY, $this->path($srcStat['hash'])); + } + + /** + * Move file + * Return new file path or false. + * + * @param string $src source path + * @param string $dst destination dir path + * @param string $name new file name + * + * @return string|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function move($src, $dst, $name) + { + $stat = $this->stat($src); + $stat['realpath'] = $src; + $this->rmTmb($stat); // can not do rmTmb() after _move() + $this->clearcache(); + + if ($res = $this->convEncOut($this->_move($this->convEncIn($src), $this->convEncIn($dst), $this->convEncIn($name)))) { + $this->clearstatcache(); + if ($stat['mime'] === 'directory') { + $this->updateSubdirsCache($dst, true); + } + $path = is_string($res) ? $res : $this->joinPathCE($dst, $name); + $this->added[] = $this->stat($path); + $this->removed[] = $stat; + return $path; + } + + return $this->setError(elFinder::ERROR_MOVE, $this->path($stat['hash'])); + } + + /** + * Copy file from another volume. + * Return new file path or false. + * + * @param Object $volume source volume + * @param string $src source file hash + * @param string $destination destination dir path + * @param string $name file name + * + * @return string|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function copyFrom($volume, $src, $destination, $name) + { + + elFinder::checkAborted(); + + if (($source = $volume->file($src)) == false) { + return $this->addError(elFinder::ERROR_COPY, '#' . $src, $volume->error()); + } + + $srcIsDir = ($source['mime'] === 'directory'); + + $errpath = $volume->path($source['hash']); + + $errors = array(); + try { + $thash = $this->encode($destination); + elFinder::$instance->trigger('paste.copyfrom', array(&$thash, &$name, '', elFinder::$instance, $this), $errors); + } catch (elFinderTriggerException $e) { + return $this->addError(elFinder::ERROR_COPY, $name, $errors); + } + + if (!$this->nameAccepted($name, $srcIsDir)) { + return $this->addError(elFinder::ERROR_COPY, $name, $srcIsDir ? elFinder::ERROR_INVALID_DIRNAME : elFinder::ERROR_INVALID_NAME); + } + + if (!$this->allowCreate($destination, $name, $srcIsDir)) { + return $this->addError(elFinder::ERROR_COPY, $name, elFinder::ERROR_PERM_DENIED); + } + + if (!$source['read']) { + return $this->addError(elFinder::ERROR_COPY, $errpath, elFinder::ERROR_PERM_DENIED); + } + + if ($srcIsDir) { + $test = $this->isNameExists($this->joinPathCE($destination, $name)); + $this->clearcache(); + + if (($test && $test['mime'] != 'directory') || (!$test && !$test = $this->mkdir($this->encode($destination), $name))) { + return $this->addError(elFinder::ERROR_COPY, $errpath); + } + + //$path = $this->joinPathCE($destination, $name); + $path = $this->decode($test['hash']); + + foreach ($volume->scandir($src) as $entr) { + $this->copyFrom($volume, $entr['hash'], $path, $entr['name']); + } + + $this->added[] = $test; + } else { + // size check + if (!isset($source['size']) || $source['size'] > $this->uploadMaxSize) { + return $this->setError(elFinder::ERROR_UPLOAD_FILE_SIZE); + } + + // MIME check + $mimeByName = $this->mimetype($source['name'], true); + if ($source['mime'] === $mimeByName) { + $mimeByName = ''; + } + if (!$this->allowPutMime($source['mime']) || ($mimeByName && !$this->allowPutMime($mimeByName))) { + return $this->addError(elFinder::ERROR_UPLOAD_FILE_MIME, $errpath); + } + + if (strpos($source['mime'], 'image') === 0 && ($dim = $volume->dimensions($src))) { + if (is_array($dim)) { + $dim = isset($dim['dim']) ? $dim['dim'] : null; + } + if ($dim) { + $s = explode('x', $dim); + $source['width'] = $s[0]; + $source['height'] = $s[1]; + } + } + + if (($fp = $volume->open($src)) == false + || ($path = $this->saveCE($fp, $destination, $name, $source)) == false) { + $fp && $volume->close($fp, $src); + return $this->addError(elFinder::ERROR_COPY, $errpath); + } + $volume->close($fp, $src); + + $this->added[] = $this->stat($path);; + } + + return $path; + } + + /** + * Remove file/ recursive remove dir + * + * @param string $path file path + * @param bool $force try to remove even if file locked + * + * @return bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function remove($path, $force = false) + { + $stat = $this->stat($path); + + if (empty($stat)) { + return $this->setError(elFinder::ERROR_RM, $path, elFinder::ERROR_FILE_NOT_FOUND); + } + + $stat['realpath'] = $path; + $this->rmTmb($stat); + $this->clearcache(); + + if (!$force && !empty($stat['locked'])) { + return $this->setError(elFinder::ERROR_LOCKED, $this->path($stat['hash'])); + } + + if ($stat['mime'] == 'directory' && empty($stat['thash'])) { + $ret = $this->delTree($this->convEncIn($path)); + $this->convEncOut(); + if (!$ret) { + return $this->setError(elFinder::ERROR_RM, $this->path($stat['hash'])); + } + } else { + if ($this->convEncOut(!$this->_unlink($this->convEncIn($path)))) { + return $this->setError(elFinder::ERROR_RM, $this->path($stat['hash'])); + } + $this->clearstatcache(); + } + + $this->removed[] = $stat; + return true; + } + + + /************************* thumbnails **************************/ + + /** + * Return thumbnail file name for required file + * + * @param array $stat file stat + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function tmbname($stat) + { + $name = $stat['hash'] . (isset($stat['ts']) ? $stat['ts'] : '') . '.png'; + if (strlen($name) > 255) { + $name = $this->id . md5($stat['hash']) . $stat['ts'] . '.png'; + } + return $name; + } + + /** + * Return thumnbnail name if exists + * + * @param string $path file path + * @param array $stat file stat + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function gettmb($path, $stat) + { + if ($this->tmbURL && $this->tmbPath) { + // file itself thumnbnail + if (strpos($path, $this->tmbPath) === 0) { + return basename($path); + } + + $name = $this->tmbname($stat); + $tmb = $this->tmbPath . DIRECTORY_SEPARATOR . $name; + if (file_exists($tmb)) { + if ($this->options['tmbGcMaxlifeHour'] && $this->options['tmbGcPercentage'] > 0) { + touch($tmb); + } + return $name; + } + } + return false; + } + + /** + * Return true if thumnbnail for required file can be created + * + * @param string $path thumnbnail path + * @param array $stat file stat + * @param bool $checkTmbPath + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function canCreateTmb($path, $stat, $checkTmbPath = true) + { + static $gdMimes = null; + static $imgmgPS = null; + if ($gdMimes === null) { + $gdMimes = array_flip(array('image/jpeg', 'image/png', 'image/gif', 'image/x-ms-bmp','image/webp')); + $imgmgPS = array_flip(array('application/postscript', 'application/pdf')); + } + if ((!$checkTmbPath || $this->tmbPathWritable) + && (!$this->tmbPath || strpos($path, $this->tmbPath) === false) // do not create thumnbnail for thumnbnail + ) { + $mime = strtolower($stat['mime']); + list($type) = explode('/', $mime); + if (!empty($this->imgConverter)) { + if (isset($this->imgConverter[$mime])) { + return true; + } + if (isset($this->imgConverter[$type])) { + return true; + } + } + return $this->imgLib + && ( + ($type === 'image' && ($this->imgLib === 'gd' ? isset($gdMimes[$stat['mime']]) : true)) + || + (ELFINDER_IMAGEMAGICK_PS && isset($imgmgPS[$stat['mime']]) && $this->imgLib !== 'gd') + ); + } + return false; + } + + /** + * Return true if required file can be resized. + * By default - the same as canCreateTmb + * + * @param string $path thumnbnail path + * @param array $stat file stat + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function canResize($path, $stat) + { + return $this->canCreateTmb($path, $stat, false); + } + + /** + * Create thumnbnail and return it's URL on success + * + * @param string $path file path + * @param $stat + * + * @return false|string + * @internal param string $mime file mime type + * @throws elFinderAbortException + * @throws ImagickException + * @author Dmitry (dio) Levashov + */ + protected function createTmb($path, $stat) + { + if (!$stat || !$this->canCreateTmb($path, $stat)) { + return false; + } + + $name = $this->tmbname($stat); + $tmb = $this->tmbPath . DIRECTORY_SEPARATOR . $name; + + $maxlength = -1; + $imgConverter = null; + + // check imgConverter + $mime = strtolower($stat['mime']); + list($type) = explode('/', $mime); + if (isset($this->imgConverter[$mime])) { + $imgConverter = $this->imgConverter[$mime]['func']; + if (!empty($this->imgConverter[$mime]['maxlen'])) { + $maxlength = intval($this->imgConverter[$mime]['maxlen']); + } + } else if (isset($this->imgConverter[$type])) { + $imgConverter = $this->imgConverter[$type]['func']; + if (!empty($this->imgConverter[$type]['maxlen'])) { + $maxlength = intval($this->imgConverter[$type]['maxlen']); + } + } + if ($imgConverter && !is_callable($imgConverter)) { + return false; + } + + // copy image into tmbPath so some drivers does not store files on local fs + if (($src = $this->fopenCE($path, 'rb')) == false) { + return false; + } + + if (($trg = fopen($tmb, 'wb')) == false) { + $this->fcloseCE($src, $path); + return false; + } + + stream_copy_to_stream($src, $trg, $maxlength); + + $this->fcloseCE($src, $path); + fclose($trg); + + // call imgConverter + if ($imgConverter) { + if (!call_user_func_array($imgConverter, array($tmb, $stat, $this))) { + file_exists($tmb) && unlink($tmb); + return false; + } + } + + $result = false; + + $tmbSize = $this->tmbSize; + + if ($this->imgLib === 'imagick') { + try { + $imagickTest = new imagick($tmb . '[0]'); + $imagickTest->clear(); + $imagickTest = true; + } catch (Exception $e) { + $imagickTest = false; + } + } + + if (($this->imgLib === 'imagick' && !$imagickTest) || ($s = getimagesize($tmb)) === false) { + if ($this->imgLib === 'imagick') { + $bgcolor = $this->options['tmbBgColor']; + if ($bgcolor === 'transparent') { + $bgcolor = 'rgba(255, 255, 255, 0.0)'; + } + try { + $imagick = new imagick(); + $imagick->setBackgroundColor(new ImagickPixel($bgcolor)); + $imagick->readImage($this->getExtentionByMime($stat['mime'], ':') . $tmb . '[0]'); + try { + $imagick->trimImage(0); + } catch (Exception $e) { + } + $imagick->setImageFormat('png'); + $imagick->writeImage($tmb); + $imagick->clear(); + if (($s = getimagesize($tmb)) !== false) { + $result = true; + } + } catch (Exception $e) { + } + } else if ($this->imgLib === 'convert') { + $convParams = $this->imageMagickConvertPrepare($tmb, 'png', 100, array(), $stat['mime']); + $cmd = sprintf('%s -colorspace sRGB -trim -- %s %s', ELFINDER_CONVERT_PATH, $convParams['quotedPath'], $convParams['quotedDstPath']); + $result = false; + if ($this->procExec($cmd) === 0) { + if (($s = getimagesize($tmb)) !== false) { + $result = true; + } + } + } + if (!$result) { + // fallback imgLib to GD + if (function_exists('gd_info') && ($s = getimagesize($tmb))) { + $this->imgLib = 'gd'; + } else { + file_exists($tmb) && unlink($tmb); + return false; + } + } + } + + /* If image smaller or equal thumbnail size - just fitting to thumbnail square */ + if ($s[0] <= $tmbSize && $s[1] <= $tmbSize) { + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } else { + + if ($this->options['tmbCrop']) { + + $result = $tmb; + /* Resize and crop if image bigger than thumbnail */ + if (!(($s[0] > $tmbSize && $s[1] <= $tmbSize) || ($s[0] <= $tmbSize && $s[1] > $tmbSize)) || ($s[0] > $tmbSize && $s[1] > $tmbSize)) { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, false, 'png'); + } + + if ($result && ($s = getimagesize($tmb)) != false) { + $x = $s[0] > $tmbSize ? intval(($s[0] - $tmbSize) / 2) : 0; + $y = $s[1] > $tmbSize ? intval(($s[1] - $tmbSize) / 2) : 0; + $result = $this->imgCrop($result, $tmbSize, $tmbSize, $x, $y, 'png'); + } else { + $result = false; + } + + } else { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, true, 'png'); + } + + if ($result) { + if ($s = getimagesize($tmb)) { + if ($s[0] !== $tmbSize || $s[1] !== $tmbSize) { + $result = $this->imgSquareFit($result, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } + } + } + } + + if (!$result) { + unlink($tmb); + return false; + } + + return $name; + } + + /** + * Resize image + * + * @param string $path image file + * @param int $width new width + * @param int $height new height + * @param bool $keepProportions crop image + * @param bool $resizeByBiggerSide resize image based on bigger side if true + * @param string $destformat image destination format + * @param int $jpgQuality JEPG quality (1-100) + * @param array $options Other extra options + * + * @return string|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + */ + protected function imgResize($path, $width, $height, $keepProportions = false, $resizeByBiggerSide = true, $destformat = null, $jpgQuality = null, $options = array()) + { + if (($s = getimagesize($path)) == false) { + return false; + } + + if (!$jpgQuality) { + $jpgQuality = $this->options['jpgQuality']; + } + + list($orig_w, $orig_h) = array($s[0], $s[1]); + list($size_w, $size_h) = array($width, $height); + + if (empty($options['unenlarge']) || $orig_w > $size_w || $orig_h > $size_h) { + if ($keepProportions == true) { + /* Resizing by biggest side */ + if ($resizeByBiggerSide) { + if ($orig_w > $orig_h) { + $size_h = round($orig_h * $width / $orig_w); + $size_w = $width; + } else { + $size_w = round($orig_w * $height / $orig_h); + $size_h = $height; + } + } else { + if ($orig_w > $orig_h) { + $size_w = round($orig_w * $height / $orig_h); + $size_h = $height; + } else { + $size_h = round($orig_h * $width / $orig_w); + $size_w = $width; + } + } + } + } else { + $size_w = $orig_w; + $size_h = $orig_h; + } + + elFinder::extendTimeLimit(300); + switch ($this->imgLib) { + case 'imagick': + + try { + $img = new imagick($path); + } catch (Exception $e) { + return false; + } + + // Imagick::FILTER_BOX faster than FILTER_LANCZOS so use for createTmb + // resize bench: http://app-mgng.rhcloud.com/9 + // resize sample: http://www.dylanbeattie.net/magick/filters/result.html + $filter = ($destformat === 'png' /* createTmb */) ? Imagick::FILTER_BOX : Imagick::FILTER_LANCZOS; + + $ani = ($img->getNumberImages() > 1); + if ($ani && is_null($destformat)) { + $img = $img->coalesceImages(); + do { + $img->resizeImage($size_w, $size_h, $filter, 1); + } while ($img->nextImage()); + $img->optimizeImageLayers(); + $result = $img->writeImages($path, true); + } else { + if ($ani) { + $img->setFirstIterator(); + } + if (strtoupper($img->getImageFormat()) === 'JPEG') { + $img->setImageCompression(imagick::COMPRESSION_JPEG); + $img->setImageCompressionQuality($jpgQuality); + if (isset($options['preserveExif']) && !$options['preserveExif']) { + try { + $orientation = $img->getImageOrientation(); + } catch (ImagickException $e) { + $orientation = 0; + } + $img->stripImage(); + if ($orientation) { + $img->setImageOrientation($orientation); + } + } + if ($this->options['jpgProgressive']) { + $img->setInterlaceScheme(Imagick::INTERLACE_PLANE); + } + } + $img->resizeImage($size_w, $size_h, $filter, true); + if ($destformat) { + $result = $this->imagickImage($img, $path, $destformat, $jpgQuality); + } else { + $result = $img->writeImage($path); + } + } + + $img->clear(); + + return $result ? $path : false; + + break; + + case 'convert': + extract($this->imageMagickConvertPrepare($path, $destformat, $jpgQuality, $s)); + /** + * @var string $ani + * @var string $index + * @var string $coalesce + * @var string $deconstruct + * @var string $jpgQuality + * @var string $quotedPath + * @var string $quotedDstPath + * @var string $interlace + */ + $filter = ($destformat === 'png' /* createTmb */) ? '-filter Box' : '-filter Lanczos'; + $strip = (isset($options['preserveExif']) && !$options['preserveExif']) ? ' -strip' : ''; + $cmd = sprintf('%s %s%s%s%s%s %s -geometry %dx%d! %s %s', ELFINDER_CONVERT_PATH, $quotedPath, $coalesce, $jpgQuality, $strip, $interlace, $filter, $size_w, $size_h, $deconstruct, $quotedDstPath); + + $result = false; + if ($this->procExec($cmd) === 0) { + $result = true; + } + return $result ? $path : false; + + break; + + case 'gd': + elFinder::expandMemoryForGD(array($s, array($size_w, $size_h))); + $img = $this->gdImageCreate($path, $s['mime']); + + if ($img && false != ($tmp = imagecreatetruecolor($size_w, $size_h))) { + + $bgNum = false; + if ($s[2] === IMAGETYPE_GIF && (!$destformat || $destformat === 'gif')) { + $bgIdx = imagecolortransparent($img); + if ($bgIdx !== -1) { + $c = imagecolorsforindex($img, $bgIdx); + $bgNum = imagecolorallocate($tmp, $c['red'], $c['green'], $c['blue']); + imagefill($tmp, 0, 0, $bgNum); + imagecolortransparent($tmp, $bgNum); + } + } + if ($bgNum === false) { + $this->gdImageBackground($tmp, 'transparent'); + } + + if (!imagecopyresampled($tmp, $img, 0, 0, 0, 0, $size_w, $size_h, $s[0], $s[1])) { + return false; + } + + $result = $this->gdImage($tmp, $path, $destformat, $s['mime'], $jpgQuality); + + imagedestroy($img); + imagedestroy($tmp); + + return $result ? $path : false; + + } + break; + } + + return false; + } + + /** + * Crop image + * + * @param string $path image file + * @param int $width crop width + * @param int $height crop height + * @param bool $x crop left offset + * @param bool $y crop top offset + * @param string $destformat image destination format + * @param int $jpgQuality JEPG quality (1-100) + * + * @return string|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + */ + protected function imgCrop($path, $width, $height, $x, $y, $destformat = null, $jpgQuality = null) + { + if (($s = getimagesize($path)) == false) { + return false; + } + + if (!$jpgQuality) { + $jpgQuality = $this->options['jpgQuality']; + } + + elFinder::extendTimeLimit(300); + switch ($this->imgLib) { + case 'imagick': + + try { + $img = new imagick($path); + } catch (Exception $e) { + return false; + } + + $ani = ($img->getNumberImages() > 1); + if ($ani && is_null($destformat)) { + $img = $img->coalesceImages(); + do { + $img->setImagePage($s[0], $s[1], 0, 0); + $img->cropImage($width, $height, $x, $y); + $img->setImagePage($width, $height, 0, 0); + } while ($img->nextImage()); + $img->optimizeImageLayers(); + $result = $img->writeImages($path, true); + } else { + if ($ani) { + $img->setFirstIterator(); + } + $img->setImagePage($s[0], $s[1], 0, 0); + $img->cropImage($width, $height, $x, $y); + $img->setImagePage($width, $height, 0, 0); + $result = $this->imagickImage($img, $path, $destformat, $jpgQuality); + } + + $img->clear(); + + return $result ? $path : false; + + break; + + case 'convert': + extract($this->imageMagickConvertPrepare($path, $destformat, $jpgQuality, $s)); + /** + * @var string $ani + * @var string $index + * @var string $coalesce + * @var string $deconstruct + * @var string $jpgQuality + * @var string $quotedPath + * @var string $quotedDstPath + * @var string $interlace + */ + $cmd = sprintf('%s %s%s%s%s -crop %dx%d+%d+%d%s %s', ELFINDER_CONVERT_PATH, $quotedPath, $coalesce, $jpgQuality, $interlace, $width, $height, $x, $y, $deconstruct, $quotedDstPath); + + $result = false; + if ($this->procExec($cmd) === 0) { + $result = true; + } + return $result ? $path : false; + + break; + + case 'gd': + elFinder::expandMemoryForGD(array($s, array($width, $height))); + $img = $this->gdImageCreate($path, $s['mime']); + + if ($img && false != ($tmp = imagecreatetruecolor($width, $height))) { + + $bgNum = false; + if ($s[2] === IMAGETYPE_GIF && (!$destformat || $destformat === 'gif')) { + $bgIdx = imagecolortransparent($img); + if ($bgIdx !== -1) { + $c = imagecolorsforindex($img, $bgIdx); + $bgNum = imagecolorallocate($tmp, $c['red'], $c['green'], $c['blue']); + imagefill($tmp, 0, 0, $bgNum); + imagecolortransparent($tmp, $bgNum); + } + } + if ($bgNum === false) { + $this->gdImageBackground($tmp, 'transparent'); + } + + $size_w = $width; + $size_h = $height; + + if ($s[0] < $width || $s[1] < $height) { + $size_w = $s[0]; + $size_h = $s[1]; + } + + if (!imagecopy($tmp, $img, 0, 0, $x, $y, $size_w, $size_h)) { + return false; + } + + $result = $this->gdImage($tmp, $path, $destformat, $s['mime'], $jpgQuality); + + imagedestroy($img); + imagedestroy($tmp); + + return $result ? $path : false; + + } + break; + } + + return false; + } + + /** + * Put image to square + * + * @param string $path image file + * @param int $width square width + * @param int $height square height + * @param int|string $align reserved + * @param int|string $valign reserved + * @param string $bgcolor square background color in #rrggbb format + * @param string $destformat image destination format + * @param int $jpgQuality JEPG quality (1-100) + * + * @return false|string + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + */ + protected function imgSquareFit($path, $width, $height, $align = 'center', $valign = 'middle', $bgcolor = '#0000ff', $destformat = null, $jpgQuality = null) + { + if (($s = getimagesize($path)) == false) { + return false; + } + + $result = false; + + /* Coordinates for image over square aligning */ + $y = ceil(abs($height - $s[1]) / 2); + $x = ceil(abs($width - $s[0]) / 2); + + if (!$jpgQuality) { + $jpgQuality = $this->options['jpgQuality']; + } + + elFinder::extendTimeLimit(300); + switch ($this->imgLib) { + case 'imagick': + try { + $img = new imagick($path); + } catch (Exception $e) { + return false; + } + + if ($bgcolor === 'transparent') { + $bgcolor = 'rgba(255, 255, 255, 0.0)'; + } + $ani = ($img->getNumberImages() > 1); + if ($ani && is_null($destformat)) { + $img1 = new Imagick(); + $img1->setFormat('gif'); + $img = $img->coalesceImages(); + do { + $gif = new Imagick(); + $gif->newImage($width, $height, new ImagickPixel($bgcolor)); + $gif->setImageColorspace($img->getImageColorspace()); + $gif->setImageFormat('gif'); + $gif->compositeImage($img, imagick::COMPOSITE_OVER, $x, $y); + $gif->setImageDelay($img->getImageDelay()); + $gif->setImageIterations($img->getImageIterations()); + $img1->addImage($gif); + $gif->clear(); + } while ($img->nextImage()); + $img1->optimizeImageLayers(); + $result = $img1->writeImages($path, true); + } else { + if ($ani) { + $img->setFirstIterator(); + } + $img1 = new Imagick(); + $img1->newImage($width, $height, new ImagickPixel($bgcolor)); + $img1->setImageColorspace($img->getImageColorspace()); + $img1->compositeImage($img, imagick::COMPOSITE_OVER, $x, $y); + $result = $this->imagickImage($img1, $path, $destformat, $jpgQuality); + } + + $img1->clear(); + $img->clear(); + return $result ? $path : false; + + break; + + case 'convert': + extract($this->imageMagickConvertPrepare($path, $destformat, $jpgQuality, $s)); + /** + * @var string $ani + * @var string $index + * @var string $coalesce + * @var string $deconstruct + * @var string $jpgQuality + * @var string $quotedPath + * @var string $quotedDstPath + * @var string $interlace + */ + if ($bgcolor === 'transparent') { + $bgcolor = 'rgba(255, 255, 255, 0.0)'; + } + $cmd = sprintf('%s -size %dx%d "xc:%s" png:- | convert%s%s%s png:- %s -geometry +%d+%d -compose over -composite%s %s', ELFINDER_CONVERT_PATH, $width, $height, $bgcolor, $coalesce, $jpgQuality, $interlace, $quotedPath, $x, $y, $deconstruct, $quotedDstPath); + + $result = false; + if ($this->procExec($cmd) === 0) { + $result = true; + } + return $result ? $path : false; + + break; + + case 'gd': + elFinder::expandMemoryForGD(array($s, array($width, $height))); + $img = $this->gdImageCreate($path, $s['mime']); + + if ($img && false != ($tmp = imagecreatetruecolor($width, $height))) { + + $this->gdImageBackground($tmp, $bgcolor); + if ($bgcolor === 'transparent' && ($destformat === 'png' || $s[2] === IMAGETYPE_PNG)) { + $bgNum = imagecolorallocatealpha($tmp, 255, 255, 255, 127); + imagefill($tmp, 0, 0, $bgNum); + } + + if (!imagecopy($tmp, $img, $x, $y, 0, 0, $s[0], $s[1])) { + return false; + } + + $result = $this->gdImage($tmp, $path, $destformat, $s['mime'], $jpgQuality); + + imagedestroy($img); + imagedestroy($tmp); + + return $result ? $path : false; + } + break; + } + + return false; + } + + /** + * Rotate image + * + * @param string $path image file + * @param int $degree rotete degrees + * @param string $bgcolor square background color in #rrggbb format + * @param string $destformat image destination format + * @param int $jpgQuality JEPG quality (1-100) + * + * @return string|false + * @throws elFinderAbortException + * @author nao-pon + * @author Troex Nevelin + */ + protected function imgRotate($path, $degree, $bgcolor = '#ffffff', $destformat = null, $jpgQuality = null) + { + if (($s = getimagesize($path)) == false || $degree % 360 === 0) { + return false; + } + + $result = false; + + // try lossless rotate + if ($degree % 90 === 0 && in_array($s[2], array(IMAGETYPE_JPEG, IMAGETYPE_JPEG2000))) { + $count = ($degree / 90) % 4; + $exiftran = array( + 1 => '-9', + 2 => '-1', + 3 => '-2' + ); + $jpegtran = array( + 1 => '90', + 2 => '180', + 3 => '270' + ); + $quotedPath = escapeshellarg($path); + $cmds = array(); + if ($this->procExec(ELFINDER_EXIFTRAN_PATH . ' -h') === 0) { + $cmds[] = ELFINDER_EXIFTRAN_PATH . ' -i ' . $exiftran[$count] . ' -- ' . $quotedPath; + } + if ($this->procExec(ELFINDER_JPEGTRAN_PATH . ' -version') === 0) { + $cmds[] = ELFINDER_JPEGTRAN_PATH . ' -rotate ' . $jpegtran[$count] . ' -copy all -outfile ' . $quotedPath . ' -- ' . $quotedPath; + } + foreach ($cmds as $cmd) { + if ($this->procExec($cmd) === 0) { + $result = true; + break; + } + } + if ($result) { + return $path; + } + } + + if (!$jpgQuality) { + $jpgQuality = $this->options['jpgQuality']; + } + + elFinder::extendTimeLimit(300); + switch ($this->imgLib) { + case 'imagick': + try { + $img = new imagick($path); + } catch (Exception $e) { + return false; + } + + if ($s[2] === IMAGETYPE_GIF || $s[2] === IMAGETYPE_PNG) { + $bgcolor = 'rgba(255, 255, 255, 0.0)'; + } + if ($img->getNumberImages() > 1) { + $img = $img->coalesceImages(); + do { + $img->rotateImage(new ImagickPixel($bgcolor), $degree); + } while ($img->nextImage()); + $img->optimizeImageLayers(); + $result = $img->writeImages($path, true); + } else { + $img->rotateImage(new ImagickPixel($bgcolor), $degree); + $result = $this->imagickImage($img, $path, $destformat, $jpgQuality); + } + $img->clear(); + return $result ? $path : false; + + break; + + case 'convert': + extract($this->imageMagickConvertPrepare($path, $destformat, $jpgQuality, $s)); + /** + * @var string $ani + * @var string $index + * @var string $coalesce + * @var string $deconstruct + * @var string $jpgQuality + * @var string $quotedPath + * @var string $quotedDstPath + * @var string $interlace + */ + if ($s[2] === IMAGETYPE_GIF || $s[2] === IMAGETYPE_PNG) { + $bgcolor = 'rgba(255, 255, 255, 0.0)'; + } + $cmd = sprintf('%s%s%s%s -background "%s" -rotate %d%s -- %s %s', ELFINDER_CONVERT_PATH, $coalesce, $jpgQuality, $interlace, $bgcolor, $degree, $deconstruct, $quotedPath, $quotedDstPath); + + $result = false; + if ($this->procExec($cmd) === 0) { + $result = true; + } + return $result ? $path : false; + + break; + + case 'gd': + elFinder::expandMemoryForGD(array($s, $s)); + $img = $this->gdImageCreate($path, $s['mime']); + + $degree = 360 - $degree; + + $bgNum = -1; + $bgIdx = false; + if ($s[2] === IMAGETYPE_GIF) { + $bgIdx = imagecolortransparent($img); + if ($bgIdx !== -1) { + $c = imagecolorsforindex($img, $bgIdx); + $w = imagesx($img); + $h = imagesy($img); + $newImg = imagecreatetruecolor($w, $h); + imagepalettecopy($newImg, $img); + $bgNum = imagecolorallocate($newImg, $c['red'], $c['green'], $c['blue']); + imagefill($newImg, 0, 0, $bgNum); + imagecolortransparent($newImg, $bgNum); + imagecopy($newImg, $img, 0, 0, 0, 0, $w, $h); + imagedestroy($img); + $img = $newImg; + $newImg = null; + } + } else if ($s[2] === IMAGETYPE_PNG) { + $bgNum = imagecolorallocatealpha($img, 255, 255, 255, 127); + } + if ($bgNum === -1) { + list($r, $g, $b) = sscanf($bgcolor, "#%02x%02x%02x"); + $bgNum = imagecolorallocate($img, $r, $g, $b); + } + + $tmp = imageRotate($img, $degree, $bgNum); + if ($bgIdx !== -1) { + imagecolortransparent($tmp, $bgNum); + } + + $result = $this->gdImage($tmp, $path, $destformat, $s['mime'], $jpgQuality); + + imageDestroy($img); + imageDestroy($tmp); + + return $result ? $path : false; + + break; + } + + return false; + } + + /** + * Execute shell command + * + * @param string $command command line + * @param string $output stdout strings + * @param int $return_var process exit code + * @param string $error_output stderr strings + * + * @return int exit code + * @throws elFinderAbortException + * @author Alexey Sukhotin + */ + protected function procExec($command, &$output = '', &$return_var = -1, &$error_output = '') + { + return elFinder::procExec($command, $output, $return_var, $error_output); + } + + /** + * Remove thumbnail, also remove recursively if stat is directory + * + * @param array $stat file stat + * + * @return void + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + * @author Troex Nevelin + */ + protected function rmTmb($stat) + { + if ($this->tmbPathWritable) { + if ($stat['mime'] === 'directory') { + foreach ($this->scandirCE($this->decode($stat['hash'])) as $p) { + elFinder::extendTimeLimit(30); + $name = $this->basenameCE($p); + $name != '.' && $name != '..' && $this->rmTmb($this->stat($p)); + } + } else if (!empty($stat['tmb']) && $stat['tmb'] != "1") { + $tmb = $this->tmbPath . DIRECTORY_SEPARATOR . rawurldecode($stat['tmb']); + file_exists($tmb) && unlink($tmb); + clearstatcache(); + } + } + } + + /** + * Create an gd image according to the specified mime type + * + * @param string $path image file + * @param string $mime + * + * @return resource|false GD image resource identifier + */ + protected function gdImageCreate($path, $mime) + { + switch ($mime) { + case 'image/jpeg': + return imagecreatefromjpeg($path); + + case 'image/png': + return imagecreatefrompng($path); + + case 'image/gif': + return imagecreatefromgif($path); + + case 'image/webp': + return function_exists('imagecreatefromwebp') ? imagecreatefromwebp($path) : false; + + case 'image/x-ms-bmp': + if (!function_exists('imagecreatefrombmp')) { + include_once dirname(__FILE__) . '/libs/GdBmp.php'; + } + return imagecreatefrombmp($path); + + case 'image/xbm': + return imagecreatefromxbm($path); + + case 'image/xpm': + return imagecreatefromxpm($path); + } + return false; + } + + /** + * Output gd image to file + * + * @param resource $image gd image resource + * @param string $filename The path to save the file to. + * @param string $destformat The Image type to use for $filename + * @param string $mime The original image mime type + * @param int $jpgQuality JEPG quality (1-100) + * + * @return bool + */ + protected function gdImage($image, $filename, $destformat, $mime, $jpgQuality = null) + { + + if (!$jpgQuality) { + $jpgQuality = $this->options['jpgQuality']; + } + if ($destformat) { + switch ($destformat) { + case 'jpg': + $mime = 'image/jpeg'; + break; + case 'gif': + $mime = 'image/gif'; + break; + case 'webp': + $mime = 'image/webp'; + break; + case 'png': + default: + $mime = 'image/png'; + break; + } + } + switch ($mime) { + case 'image/gif': + return imagegif($image, $filename); + case 'image/jpeg': + if ($this->options['jpgProgressive']) { + imageinterlace($image, true); + } + return imagejpeg($image, $filename, $jpgQuality); + case 'image/wbmp': + return imagewbmp($image, $filename); + case 'image/webp': + return function_exists('imagewebp') ? imagewebp($image, $filename, $jpgQuality) : false; + case 'image/png': + default: + return imagepng($image, $filename); + } + } + + /** + * Output imagick image to file + * + * @param imagick $img imagick image resource + * @param string $filename The path to save the file to. + * @param string $destformat The Image type to use for $filename + * @param int $jpgQuality JEPG quality (1-100) + * + * @return bool + */ + protected function imagickImage($img, $filename, $destformat, $jpgQuality = null) + { + + if (!$jpgQuality) { + $jpgQuality = $this->options['jpgQuality']; + } + + try { + if ($destformat) { + if ($destformat === 'gif') { + $img->setImageFormat('gif'); + } else if ($destformat === 'png') { + $img->setImageFormat('png'); + } else if ($destformat === 'jpg') { + $img->setImageFormat('jpeg'); + } + } + if (strtoupper($img->getImageFormat()) === 'JPEG') { + $img->setImageCompression(imagick::COMPRESSION_JPEG); + $img->setImageCompressionQuality($jpgQuality); + if ($this->options['jpgProgressive']) { + $img->setInterlaceScheme(Imagick::INTERLACE_PLANE); + } + try { + $orientation = $img->getImageOrientation(); + } catch (ImagickException $e) { + $orientation = 0; + } + $img->stripImage(); + if ($orientation) { + $img->setImageOrientation($orientation); + } + } + $result = $img->writeImage($filename); + } catch (Exception $e) { + $result = false; + } + + return $result; + } + + /** + * Assign the proper background to a gd image + * + * @param resource $image gd image resource + * @param string $bgcolor background color in #rrggbb format + */ + protected function gdImageBackground($image, $bgcolor) + { + + if ($bgcolor === 'transparent') { + imagealphablending($image, false); + imagesavealpha($image, true); + } else { + list($r, $g, $b) = sscanf($bgcolor, "#%02x%02x%02x"); + $bgcolor1 = imagecolorallocate($image, $r, $g, $b); + imagefill($image, 0, 0, $bgcolor1); + } + } + + /** + * Prepare variables for exec convert of ImageMagick + * + * @param string $path + * @param string $destformat + * @param int $jpgQuality + * @param array $imageSize + * @param null $mime + * + * @return array + * @throws elFinderAbortException + */ + protected function imageMagickConvertPrepare($path, $destformat, $jpgQuality, $imageSize = null, $mime = null) + { + if (is_null($imageSize)) { + $imageSize = getimagesize($path); + } + if (is_null($mime)) { + $mime = $this->mimetype($path); + } + $srcType = $this->getExtentionByMime($mime, ':'); + $ani = false; + if (preg_match('/^(?:gif|png|ico)/', $srcType)) { + $cmd = ELFINDER_IDENTIFY_PATH . ' -- ' . escapeshellarg($srcType . $path); + if ($this->procExec($cmd, $o) === 0) { + $ani = preg_split('/(?:\r\n|\n|\r)/', trim($o)); + if (count($ani) < 2) { + $ani = false; + } + } + } + $coalesce = $index = $interlace = ''; + $deconstruct = ' +repage'; + if ($ani && $destformat !== 'png'/* not createTmb */) { + if (is_null($destformat)) { + $coalesce = ' -coalesce -repage 0x0'; + $deconstruct = ' +repage -deconstruct -layers optimize'; + } else if ($imageSize) { + if ($srcType === 'ico:') { + $index = '[0]'; + foreach ($ani as $_i => $_info) { + if (preg_match('/ (\d+)x(\d+) /', $_info, $m)) { + if ($m[1] == $imageSize[0] && $m[2] == $imageSize[1]) { + $index = '[' . $_i . ']'; + break; + } + } + } + } + } + } else { + $index = '[0]'; + } + if ($imageSize && ($imageSize[2] === IMAGETYPE_JPEG || $imageSize[2] === IMAGETYPE_JPEG2000)) { + $jpgQuality = ' -quality ' . $jpgQuality; + if ($this->options['jpgProgressive']) { + $interlace = ' -interlace Plane'; + } + } else { + $jpgQuality = ''; + } + $quotedPath = escapeshellarg($srcType . $path . $index); + $quotedDstPath = escapeshellarg(($destformat ? ($destformat . ':') : $srcType) . $path); + return compact('ani', 'index', 'coalesce', 'deconstruct', 'jpgQuality', 'quotedPath', 'quotedDstPath', 'interlace'); + } + + /*********************** misc *************************/ + + /** + * Find position of first occurrence of string in a string with multibyte support + * + * @param string $haystack The string being checked. + * @param string $needle The string to find in haystack. + * @param int $offset The search offset. If it is not specified, 0 is used. + * + * @return int|bool + * @author Alexey Sukhotin + **/ + protected function stripos($haystack, $needle, $offset = 0) + { + if (function_exists('mb_stripos')) { + return mb_stripos($haystack, $needle, $offset, 'UTF-8'); + } else if (function_exists('mb_strtolower') && function_exists('mb_strpos')) { + return mb_strpos(mb_strtolower($haystack, 'UTF-8'), mb_strtolower($needle, 'UTF-8'), $offset); + } + return stripos($haystack, $needle, $offset); + } + + /** + * Default serach match method (name match) + * + * @param String $name Item name + * @param String $query Query word + * @param String $path Item path + * + * @return bool @return bool + */ + protected function searchMatchName($name, $query, $path) + { + return $this->stripos($name, $query) !== false; + } + + /** + * Get server side available archivers + * + * @param bool $use_cache + * + * @return array + * @throws elFinderAbortException + */ + protected function getArchivers($use_cache = true) + { + $sessionKey = 'archivers'; + if ($use_cache) { + if (isset($this->options['archivers']) && is_array($this->options['archivers']) && $this->options['archivers']) { + $cache = $this->options['archivers']; + } else { + $cache = elFinder::$archivers; + } + if ($cache) { + return $cache; + } else { + if ($cache = $this->session->get($sessionKey, array())) { + return elFinder::$archivers = $cache; + } + } + } + + $arcs = array( + 'create' => array(), + 'extract' => array() + ); + + if ($this->procExec('') === 0) { + + $this->procExec(ELFINDER_TAR_PATH . ' --version', $o, $ctar); + + if ($ctar == 0) { + $arcs['create']['application/x-tar'] = array('cmd' => ELFINDER_TAR_PATH, 'argc' => '-chf', 'ext' => 'tar'); + $arcs['extract']['application/x-tar'] = array('cmd' => ELFINDER_TAR_PATH, 'argc' => '-xf', 'ext' => 'tar', 'toSpec' => '-C ', 'getsize' => array('argc' => '-xvf', 'toSpec' => '--to-stdout|wc -c', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]*([0-9]+)[^\r\n]*$/s', 'replace' => '$1')); + unset($o); + $this->procExec(ELFINDER_GZIP_PATH . ' --version', $o, $c); + if ($c == 0) { + $arcs['create']['application/x-gzip'] = array('cmd' => ELFINDER_TAR_PATH, 'argc' => '-czhf', 'ext' => 'tgz'); + $arcs['extract']['application/x-gzip'] = array('cmd' => ELFINDER_TAR_PATH, 'argc' => '-xzf', 'ext' => 'tgz', 'toSpec' => '-C ', 'getsize' => array('argc' => '-xvf', 'toSpec' => '--to-stdout|wc -c', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]*([0-9]+)[^\r\n]*$/s', 'replace' => '$1')); + } + unset($o); + $this->procExec(ELFINDER_BZIP2_PATH . ' --version', $o, $c); + if ($c == 0) { + $arcs['create']['application/x-bzip2'] = array('cmd' => ELFINDER_TAR_PATH, 'argc' => '-cjhf', 'ext' => 'tbz'); + $arcs['extract']['application/x-bzip2'] = array('cmd' => ELFINDER_TAR_PATH, 'argc' => '-xjf', 'ext' => 'tbz', 'toSpec' => '-C ', 'getsize' => array('argc' => '-xvf', 'toSpec' => '--to-stdout|wc -c', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]*([0-9]+)[^\r\n]*$/s', 'replace' => '$1')); + } + unset($o); + $this->procExec(ELFINDER_XZ_PATH . ' --version', $o, $c); + if ($c == 0) { + $arcs['create']['application/x-xz'] = array('cmd' => ELFINDER_TAR_PATH, 'argc' => '-cJhf', 'ext' => 'xz'); + $arcs['extract']['application/x-xz'] = array('cmd' => ELFINDER_TAR_PATH, 'argc' => '-xJf', 'ext' => 'xz', 'toSpec' => '-C ', 'getsize' => array('argc' => '-xvf', 'toSpec' => '--to-stdout|wc -c', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]*([0-9]+)[^\r\n]*$/s', 'replace' => '$1')); + } + } + unset($o); + $this->procExec(ELFINDER_ZIP_PATH . ' -h', $o, $c); + if ($c == 0) { + $arcs['create']['application/zip'] = array('cmd' => ELFINDER_ZIP_PATH, 'argc' => '-r9 -q', 'ext' => 'zip'); + } + unset($o); + $this->procExec(ELFINDER_UNZIP_PATH . ' --help', $o, $c); + if ($c == 0) { + $arcs['extract']['application/zip'] = array('cmd' => ELFINDER_UNZIP_PATH, 'argc' => '-q', 'ext' => 'zip', 'toSpec' => '-d ', 'getsize' => array('argc' => '-Z -t', 'regex' => '/^.+?,\s?([0-9]+).+$/', 'replace' => '$1')); + } + unset($o); + $this->procExec(ELFINDER_RAR_PATH . ' --version', $o, $c); + if ($c == 0 || $c == 7) { + $arcs['create']['application/x-rar'] = array('cmd' => ELFINDER_RAR_PATH, 'argc' => 'a -inul', 'ext' => 'rar'); + } + unset($o); + $this->procExec(ELFINDER_UNRAR_PATH, $o, $c); + if ($c == 0 || $c == 7) { + $arcs['extract']['application/x-rar'] = array('cmd' => ELFINDER_UNRAR_PATH, 'argc' => 'x -y', 'ext' => 'rar', 'toSpec' => '', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]+[0-9]+[^\r\n0-9]+([0-9]+)[^\r\n]+$/s', 'replace' => '$1')); + } + unset($o); + $this->procExec(ELFINDER_7Z_PATH, $o, $c); + if ($c == 0) { + $arcs['create']['application/x-7z-compressed'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a', 'ext' => '7z'); + $arcs['extract']['application/x-7z-compressed'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'x -y', 'ext' => '7z', 'toSpec' => '-o', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]+([0-9]+)[^\r\n]+$/s', 'replace' => '$1')); + + if (empty($arcs['create']['application/zip'])) { + $arcs['create']['application/zip'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -tzip', 'ext' => 'zip'); + } + if (empty($arcs['extract']['application/zip'])) { + $arcs['extract']['application/zip'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'x -tzip -y', 'ext' => 'zip', 'toSpec' => '-o', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]+([0-9]+)[^\r\n]+$/s', 'replace' => '$1')); + } + if (empty($arcs['create']['application/x-tar'])) { + $arcs['create']['application/x-tar'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'a -ttar', 'ext' => 'tar'); + } + if (empty($arcs['extract']['application/x-tar'])) { + $arcs['extract']['application/x-tar'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'x -ttar -y', 'ext' => 'tar', 'toSpec' => '-o', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]+([0-9]+)[^\r\n]+$/s', 'replace' => '$1')); + } + if (substr(PHP_OS, 0, 3) === 'WIN' && empty($arcs['extract']['application/x-rar'])) { + $arcs['extract']['application/x-rar'] = array('cmd' => ELFINDER_7Z_PATH, 'argc' => 'x -trar -y', 'ext' => 'rar', 'toSpec' => '-o', 'getsize' => array('argc' => 'l', 'regex' => '/^.+(?:\r\n|\n|\r)[^\r\n0-9]+([0-9]+)[^\r\n]+$/s', 'replace' => '$1')); + } + } + + } + + // Use PHP ZipArchive Class + if (class_exists('ZipArchive', false)) { + if (empty($arcs['create']['application/zip'])) { + $arcs['create']['application/zip'] = array('cmd' => 'phpfunction', 'argc' => array('self', 'zipArchiveZip'), 'ext' => 'zip'); + } + if (empty($arcs['extract']['application/zip'])) { + $arcs['extract']['application/zip'] = array('cmd' => 'phpfunction', 'argc' => array('self', 'zipArchiveUnzip'), 'ext' => 'zip'); + } + } + + $this->session->set($sessionKey, $arcs); + return elFinder::$archivers = $arcs; + } + + /** + * Resolve relative / (Unix-like)absolute path + * + * @param string $path target path + * @param string $base base path + * + * @return string + */ + protected function getFullPath($path, $base) + { + $separator = $this->separator; + $systemroot = $this->systemRoot; + $base = (string)$base; + + if ($base[0] === $separator && substr($base, 0, strlen($systemroot)) !== $systemroot) { + $base = $systemroot . substr($base, 1); + } + if ($base !== $systemroot) { + $base = rtrim($base, $separator); + } + + // 'Here' + if ($path === '' || $path === '.' . $separator) return $base; + + $sepquoted = preg_quote($separator, '#'); + + if (substr($path, 0, 3) === '..' . $separator) { + $path = $base . $separator . $path; + } + // normalize `/../` + $normreg = '#(' . $sepquoted . ')[^' . $sepquoted . ']+' . $sepquoted . '\.\.' . $sepquoted . '#'; // '#(/)[^\/]+/\.\./#' + while (preg_match($normreg, $path)) { + $path = preg_replace($normreg, '$1', $path, 1); + } + if ($path !== $systemroot) { + $path = rtrim($path, $separator); + } + + // Absolute path + if ($path[0] === $separator || strpos($path, $systemroot) === 0) { + return $path; + } + + $preg_separator = '#' . $sepquoted . '#'; + + // Relative path from 'Here' + if (substr($path, 0, 2) === '.' . $separator || $path[0] !== '.') { + $arrn = preg_split($preg_separator, $path, -1, PREG_SPLIT_NO_EMPTY); + if ($arrn[0] !== '.') { + array_unshift($arrn, '.'); + } + $arrn[0] = rtrim($base, $separator); + return join($separator, $arrn); + } + + return $path; + } + + /** + * Remove directory recursive on local file system + * + * @param string $dir Target dirctory path + * + * @return boolean + * @throws elFinderAbortException + * @author Naoki Sawada + */ + public function rmdirRecursive($dir) + { + return self::localRmdirRecursive($dir); + } + + /** + * Create archive and return its path + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + * @author Naoki Sawada + */ + protected function makeArchive($dir, $files, $name, $arc) + { + if ($arc['cmd'] === 'phpfunction') { + if (is_callable($arc['argc'])) { + call_user_func_array($arc['argc'], array($dir, $files, $name)); + } + } else { + $cwd = getcwd(); + if (chdir($dir)) { + foreach ($files as $i => $file) { + $files[$i] = '.' . DIRECTORY_SEPARATOR . basename($file); + } + $files = array_map('escapeshellarg', $files); + + $cmd = $arc['cmd'] . ' ' . $arc['argc'] . ' ' . escapeshellarg($name) . ' ' . implode(' ', $files); + $this->procExec($cmd, $o, $c); + chdir($cwd); + } else { + return false; + } + } + $path = $dir . DIRECTORY_SEPARATOR . $name; + return file_exists($path) ? $path : false; + } + + /** + * Unpack archive + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * @param bool|string $mode bool: remove archive ( unlink($path) ) | string: extract to directory + * + * @return void + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + * @author Naoki Sawada + */ + protected function unpackArchive($path, $arc, $mode = true) + { + if (is_string($mode)) { + $dir = $mode; + $chdir = null; + $remove = false; + } else { + $dir = dirname($path); + $chdir = $dir; + $remove = $mode; + } + $dir = realpath($dir); + $path = realpath($path); + if ($arc['cmd'] === 'phpfunction') { + if (is_callable($arc['argc'])) { + call_user_func_array($arc['argc'], array($path, $dir)); + } + } else { + $cwd = getcwd(); + if (!$chdir || chdir($dir)) { + if (!empty($arc['getsize'])) { + // Check total file size after extraction + $getsize = $arc['getsize']; + if (is_array($getsize) && !empty($getsize['regex']) && !empty($getsize['replace'])) { + $cmd = $arc['cmd'] . ' ' . $getsize['argc'] . ' ' . escapeshellarg($path) . (!empty($getsize['toSpec'])? (' ' . $getsize['toSpec']): ''); + $this->procExec($cmd, $o, $c); + if ($o) { + $size = preg_replace($getsize['regex'], $getsize['replace'], trim($o)); + $comp = function_exists('bccomp')? 'bccomp' : 'strnatcmp'; + if (!empty($this->options['maxArcFilesSize'])) { + if ($comp($size, (string)$this->options['maxArcFilesSize']) > 0) { + throw new Exception(elFinder::ERROR_ARC_MAXSIZE); + } + } + } + unset($o, $c); + } + } + if ($chdir) { + $cmd = $arc['cmd'] . ' ' . $arc['argc'] . ' ' . escapeshellarg(basename($path)); + } else { + $cmd = $arc['cmd'] . ' ' . $arc['argc'] . ' ' . escapeshellarg($path) . ' ' . $arc['toSpec'] . escapeshellarg($dir); + } + $this->procExec($cmd, $o, $c); + $chdir && chdir($cwd); + } + } + $remove && unlink($path); + } + + /** + * Check and filter the extracted items + * + * @param string $path target local path + * @param array $checks types to check default: ['symlink', 'name', 'writable', 'mime'] + * + * @return array ['symlinks' => [], 'names' => [], 'writables' => [], 'mimes' => [], 'rmNames' => [], 'totalSize' => 0] + * @throws elFinderAbortException + * @throws Exception + * @author Naoki Sawada + */ + protected function checkExtractItems($path, $checks = null) + { + if (is_null($checks) || !is_array($checks)) { + $checks = array('symlink', 'name', 'writable', 'mime'); + } + $chkSymlink = in_array('symlink', $checks); + $chkName = in_array('name', $checks); + $chkWritable = in_array('writable', $checks); + $chkMime = in_array('mime', $checks); + + $res = array( + 'symlinks' => array(), + 'names' => array(), + 'writables' => array(), + 'mimes' => array(), + 'rmNames' => array(), + 'totalSize' => 0 + ); + + if (is_dir($path)) { + $files = self::localScandir($path); + } else { + $files = array(basename($path)); + $path = dirname($path); + } + + foreach ($files as $name) { + $p = $path . DIRECTORY_SEPARATOR . $name; + $utf8Name = elFinder::$instance->utf8Encode($name); + if ($name !== $utf8Name) { + $fsSame = false; + if ($this->encoding) { + // test as fs encoding + $_utf8 = @iconv($this->encoding, 'utf-8//IGNORE', $name); + if (@iconv('utf-8', $this->encoding.'//IGNORE', $_utf8) === $name) { + $fsSame = true; + $utf8Name = $_utf8; + } else { + $_name = $this->convEncIn($utf8Name, true); + } + } else { + $_name = $utf8Name; + } + if (!$fsSame && rename($p, $path . DIRECTORY_SEPARATOR . $_name)) { + $name = $_name; + $p = $path . DIRECTORY_SEPARATOR . $name; + } + } + if (!is_readable($p)) { + // Perhaps a symbolic link to open_basedir restricted location + self::localRmdirRecursive($p); + $res['symlinks'][] = $p; + $res['rmNames'][] = $utf8Name; + continue; + } + if ($chkSymlink && is_link($p)) { + self::localRmdirRecursive($p); + $res['symlinks'][] = $p; + $res['rmNames'][] = $utf8Name; + continue; + } + $isDir = is_dir($p); + if ($chkName && !$this->nameAccepted($name, $isDir)) { + self::localRmdirRecursive($p); + $res['names'][] = $p; + $res['rmNames'][] = $utf8Name; + continue; + } + if ($chkWritable && !$this->attr($p, 'write', null, $isDir)) { + self::localRmdirRecursive($p); + $res['writables'][] = $p; + $res['rmNames'][] = $utf8Name; + continue; + } + if ($isDir) { + $cRes = $this->checkExtractItems($p, $checks); + foreach ($cRes as $k => $v) { + if (is_array($v)) { + $res[$k] = array_merge($res[$k], $cRes[$k]); + } else { + $res[$k] += $cRes[$k]; + } + } + } else { + if ($chkMime && ($mimeByName = elFinderVolumeDriver::mimetypeInternalDetect($name)) && !$this->allowPutMime($mimeByName)) { + self::localRmdirRecursive($p); + $res['mimes'][] = $p; + $res['rmNames'][] = $utf8Name; + continue; + } + $res['totalSize'] += (int)sprintf('%u', filesize($p)); + } + } + $res['rmNames'] = array_unique($res['rmNames']); + + return $res; + } + + /** + * Return files of target directory that is dotfiles excludes. + * + * @param string $dir target directory path + * + * @return array + * @throws Exception + * @author Naoki Sawada + */ + protected static function localScandir($dir) + { + // PHP function scandir() is not work well in specific environment. I dont know why. + // ref. https://github.com/Studio-42/elFinder/issues/1248 + $files = array(); + if ($dh = opendir($dir)) { + while (false !== ($file = readdir($dh))) { + if ($file !== '.' && $file !== '..') { + $files[] = $file; + } + } + closedir($dh); + } else { + throw new Exception('Can not open local directory.'); + } + return $files; + } + + /** + * Remove directory recursive on local file system + * + * @param string $dir Target dirctory path + * + * @return boolean + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected static function localRmdirRecursive($dir) + { + // try system command + if (is_callable('exec')) { + $o = ''; + $r = 1; + if (substr(PHP_OS, 0, 3) === 'WIN') { + if (!is_link($dir) && is_dir($dir)) { + exec('rd /S /Q ' . escapeshellarg($dir), $o, $r); + } else { + exec('del /F /Q ' . escapeshellarg($dir), $o, $r); + } + } else { + exec('rm -rf ' . escapeshellarg($dir), $o, $r); + } + if ($r === 0) { + return true; + } + } + if (!is_link($dir) && is_dir($dir)) { + chmod($dir, 0777); + if ($handle = opendir($dir)) { + while (false !== ($file = readdir($handle))) { + if ($file === '.' || $file === '..') { + continue; + } + elFinder::extendTimeLimit(30); + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (!is_link($dir) && is_dir($path)) { + self::localRmdirRecursive($path); + } else { + chmod($path, 0666); + unlink($path); + } + } + closedir($handle); + } + return rmdir($dir); + } else { + chmod($dir, 0666); + return unlink($dir); + } + } + + /** + * Move item recursive on local file system + * + * @param string $src + * @param string $target + * @param bool $overWrite + * @param bool $copyJoin + * + * @return boolean + * @throws elFinderAbortException + * @throws Exception + * @author Naoki Sawada + */ + protected static function localMoveRecursive($src, $target, $overWrite = true, $copyJoin = true) + { + $res = false; + if (!file_exists($target)) { + return rename($src, $target); + } + if (!$copyJoin || !is_dir($target)) { + if ($overWrite) { + if (is_dir($target)) { + $del = self::localRmdirRecursive($target); + } else { + $del = unlink($target); + } + if ($del) { + return rename($src, $target); + } + } + } else { + foreach (self::localScandir($src) as $item) { + $res |= self::localMoveRecursive($src . DIRECTORY_SEPARATOR . $item, $target . DIRECTORY_SEPARATOR . $item, $overWrite, $copyJoin); + } + } + return (bool)$res; + } + + /** + * Create Zip archive using PHP class ZipArchive + * + * @param string $dir target dir + * @param array $files files names list + * @param string|object $zipPath Zip archive name + * + * @return bool + * @author Naoki Sawada + */ + protected static function zipArchiveZip($dir, $files, $zipPath) + { + try { + if ($start = is_string($zipPath)) { + $zip = new ZipArchive(); + if ($zip->open($dir . DIRECTORY_SEPARATOR . $zipPath, ZipArchive::CREATE) !== true) { + $zip = false; + } + } else { + $zip = $zipPath; + } + if ($zip) { + foreach ($files as $file) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (is_dir($path)) { + $zip->addEmptyDir($file); + $_files = array(); + if ($handle = opendir($path)) { + while (false !== ($entry = readdir($handle))) { + if ($entry !== "." && $entry !== "..") { + $_files[] = $file . DIRECTORY_SEPARATOR . $entry; + } + } + closedir($handle); + } + if ($_files) { + self::zipArchiveZip($dir, $_files, $zip); + } + } else { + $zip->addFile($path, $file); + } + } + $start && $zip->close(); + } + } catch (Exception $e) { + return false; + } + return true; + } + + /** + * Unpack Zip archive using PHP class ZipArchive + * + * @param string $zipPath Zip archive name + * @param string $toDir Extract to path + * + * @return bool + * @author Naoki Sawada + */ + protected static function zipArchiveUnzip($zipPath, $toDir) + { + try { + $zip = new ZipArchive(); + if ($zip->open($zipPath) === true) { + // Check total file size after extraction + $num = $zip->numFiles; + $size = 0; + $maxSize = empty(self::$maxArcFilesSize)? '' : (string)self::$maxArcFilesSize; + $comp = function_exists('bccomp')? 'bccomp' : 'strnatcmp'; + for ($i = 0; $i < $num; $i++) { + $stat = $zip->statIndex($i); + $size += $stat['size']; + if (strpos((string)$size, 'E') !== false) { + // Cannot handle values exceeding PHP_INT_MAX + throw new Exception(elFinder::ERROR_ARC_MAXSIZE); + } + if (!$maxSize) { + if ($comp($size, $maxSize) > 0) { + throw new Exception(elFinder::ERROR_ARC_MAXSIZE); + } + } + } + // do extract + $zip->extractTo($toDir); + $zip->close(); + } + } catch (Exception $e) { + throw $e; + } + return true; + } + + /** + * Recursive symlinks search + * + * @param string $path file/dir path + * + * @return bool + * @throws Exception + * @author Dmitry (dio) Levashov + */ + protected static function localFindSymlinks($path) + { + if (is_link($path)) { + return true; + } + + if (is_dir($path)) { + foreach (self::localScandir($path) as $name) { + $p = $path . DIRECTORY_SEPARATOR . $name; + if (is_link($p)) { + return true; + } + if (is_dir($p) && self::localFindSymlinks($p)) { + return true; + } + } + } + + return false; + } + + /**==================================* abstract methods *====================================**/ + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _dirname($path); + + /** + * Return file name + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _basename($path); + + /** + * Join dir name and file name and return full path. + * Some drivers (db) use int as path - so we give to concat path to driver itself + * + * @param string $dir dir path + * @param string $name file name + * + * @return string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _joinPath($dir, $name); + + /** + * Return normalized path + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _normpath($path); + + /** + * Return file path related to root dir + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _relpath($path); + + /** + * Convert path related to root dir into real path + * + * @param string $path rel file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _abspath($path); + + /** + * Return fake path started from root dir. + * Required to show path on client side. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _path($path); + + /** + * Return true if $path is children of $parent + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _inpath($path, $parent); + + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + abstract protected function _stat($path); + + + /***************** file stat ********************/ + + + /** + * Return true if path is dir and has at least one childs directory + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _subdirs($path); + + /** + * Return object width and height + * Ususaly used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * + * @return string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _dimensions($path, $mime); + + /******************** file/dir content *********************/ + + /** + * Return files list in directory + * + * @param string $path dir path + * + * @return array + * @author Dmitry (dio) Levashov + **/ + abstract protected function _scandir($path); + + /** + * Open file and return file pointer + * + * @param string $path file path + * @param string $mode open mode + * + * @return resource|false + * @author Dmitry (dio) Levashov + **/ + abstract protected function _fopen($path, $mode = "rb"); + + /** + * Close opened file + * + * @param resource $fp file pointer + * @param string $path file path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _fclose($fp, $path = ''); + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed + * + * @param string $path parent dir path + * @param string $name new directory name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _mkdir($path, $name); + + /** + * Create file and return it's path or false on failed + * + * @param string $path parent dir path + * @param string $name new file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _mkfile($path, $name); + + /** + * Create symlink + * + * @param string $source file to link to + * @param string $targetDir folder to create link in + * @param string $name symlink name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _symlink($source, $targetDir, $name); + + /** + * Copy file into another file (only inside one volume) + * + * @param string $source source file path + * @param $targetDir + * @param string $name file name + * + * @return bool|string + * @internal param string $target target dir path + * @author Dmitry (dio) Levashov + */ + abstract protected function _copy($source, $targetDir, $name); + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param $targetDir + * @param string $name file name + * + * @return bool|string + * @internal param string $target target dir path + * @author Dmitry (dio) Levashov + */ + abstract protected function _move($source, $targetDir, $name); + + /** + * Remove file + * + * @param string $path file path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _unlink($path); + + /** + * Remove dir + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _rmdir($path); + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param string $dir target dir path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Dmitry (dio) Levashov + **/ + abstract protected function _save($fp, $dir, $name, $stat); + + /** + * Get file contents + * + * @param string $path file path + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + abstract protected function _getContents($path); + + /** + * Write a string to a file + * + * @param string $path file path + * @param string $content new file content + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + abstract protected function _filePutContents($path, $content); + + /** + * Extract files from archive + * + * @param string $path file path + * @param array $arc archiver options + * + * @return bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + abstract protected function _extract($path, $arc); + + /** + * Create archive and return its path + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + abstract protected function _archive($dir, $files, $name, $arc); + + /** + * Detect available archivers + * + * @return void + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + abstract protected function _checkArchivers(); + + /** + * Change file mode (chmod) + * + * @param string $path file path + * @param string $mode octal string such as '0755' + * + * @return bool + * @author David Bartle, + **/ + abstract protected function _chmod($path, $mode); + + +} // END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDropbox.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDropbox.class.php new file mode 100644 index 0000000..0a0d138 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDropbox.class.php @@ -0,0 +1,1464 @@ +dropbox_phpFound = class_exists('Dropbox_API'); + + if (! $this->dropbox_phpFound) { + // check with pear + if (include_once 'Dropbox/autoload.php') { + $this->dropbox_phpFound = in_array('Dropbox_autoload', spl_autoload_functions()); + } + } + + $opts = array( + 'consumerKey' => '', + 'consumerSecret' => '', + 'accessToken' => '', + 'accessTokenSecret' => '', + 'dropboxUid' => '', + 'root' => 'dropbox', + 'path' => '/', + 'separator' => '/', + 'PDO_DSN' => '', // if empty use 'sqlite:(metaCachePath|tmbPath)/elFinder_dropbox_db_(hash:dropboxUid+consumerSecret)' + 'PDO_User' => '', + 'PDO_Pass' => '', + 'PDO_Options' => array(), + 'PDO_DBName' => 'dropbox', + 'treeDeep' => 0, + 'tmbPath' => '', + 'tmbURL' => '', + 'tmpPath' => '', + 'getTmbSize' => 'large', // small: 32x32, medium or s: 64x64, large or m: 128x128, l: 640x480, xl: 1024x768 + 'metaCachePath' => '', + 'metaCacheTime' => '600', // 10m + 'acceptedName' => '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#', + 'rootCssClass' => 'elfinder-navbar-root-dropbox' + ); + $this->options = array_merge($this->options, $opts); + $this->options['mimeDetect'] = 'internal'; + } + + /** + * Prepare + * Call from elFinder::netmout() before volume->mount() + * + * @param $options + * @return Array + * @author Naoki Sawada + */ + public function netmountPrepare($options) { + if (empty($options['consumerKey']) && defined('ELFINDER_DROPBOX_CONSUMERKEY')) $options['consumerKey'] = ELFINDER_DROPBOX_CONSUMERKEY; + if (empty($options['consumerSecret']) && defined('ELFINDER_DROPBOX_CONSUMERSECRET')) $options['consumerSecret'] = ELFINDER_DROPBOX_CONSUMERSECRET; + + if ($options['user'] === 'init') { + + if (! $this->dropbox_phpFound || empty($options['consumerKey']) || empty($options['consumerSecret']) || !class_exists('PDO', false)) { + return array('exit' => true, 'body' => '{msg:errNetMountNoDriver}'); + } + + if (defined('ELFINDER_DROPBOX_USE_CURL_PUT')) { + $this->oauth = new Dropbox_OAuth_Curl($options['consumerKey'], $options['consumerSecret']); + } else { + if (class_exists('OAuth', false)) { + $this->oauth = new Dropbox_OAuth_PHP($options['consumerKey'], $options['consumerSecret']); + } else { + if (! class_exists('HTTP_OAuth_Consumer')) { + // We're going to try to load in manually + include 'HTTP/OAuth/Consumer.php'; + } + if (class_exists('HTTP_OAuth_Consumer', false)) { + $this->oauth = new Dropbox_OAuth_PEAR($options['consumerKey'], $options['consumerSecret']); + } + } + } + + if (! $this->oauth) { + return array('exit' => true, 'body' => '{msg:errNetMountNoDriver}'); + } + + if ($options['pass'] === 'init') { + $html = ''; + if ($sessionToken = $this->session->get('DropboxTokens')) { + // token check + try { + list(, $accessToken, $accessTokenSecret) = $sessionToken; + $this->oauth->setToken($accessToken, $accessTokenSecret); + $this->dropbox = new Dropbox_API($this->oauth, $this->options['root']); + $this->dropbox->getAccountInfo(); + $script = ''; + $html = $script; + } catch (Dropbox_Exception $e) { + $this->session->remove('DropboxTokens'); + } + } + if (! $html) { + // get customdata + $cdata = ''; + $innerKeys = array('cmd', 'host', 'options', 'pass', 'protocol', 'user'); + $this->ARGS = $_SERVER['REQUEST_METHOD'] === 'POST'? $_POST : $_GET; + foreach($this->ARGS as $k => $v) { + if (! in_array($k, $innerKeys)) { + $cdata .= '&' . $k . '=' . rawurlencode($v); + } + } + if (strpos($options['url'], 'http') !== 0 ) { + $options['url'] = elFinder::getConnectorUrl(); + } + $callback = $options['url'] + . '?cmd=netmount&protocol=dropbox&host=dropbox.com&user=init&pass=return&node='.$options['id'].$cdata; + + try { + $tokens = $this->oauth->getRequestToken(); + $url= $this->oauth->getAuthorizeUrl(rawurlencode($callback)); + } catch (Dropbox_Exception $e) { + return array('exit' => true, 'body' => '{msg:errAccess}'); + } + + $this->session->set('DropboxAuthTokens', $tokens); + $html = ''; + $html .= ''; + } + return array('exit' => true, 'body' => $html); + } else { + $this->oauth->setToken($this->session->get('DropboxAuthTokens')); + $this->session->remove('DropboxAuthTokens'); + $tokens = $this->oauth->getAccessToken(); + $this->session->set('DropboxTokens', array($_GET['uid'], $tokens['token'], $tokens['token_secret'])); + + $out = array( + 'node' => $_GET['node'], + 'json' => '{"protocol": "dropbox", "mode": "done"}', + 'bind' => 'netmount' + ); + + return array('exit' => 'callback', 'out' => $out); + } + } + if ($sessionToken = $this->session->get('DropboxTokens')) { + list($options['dropboxUid'], $options['accessToken'], $options['accessTokenSecret']) = $sessionToken; + } + unset($options['user'], $options['pass']); + return $options; + } + + /** + * process of on netunmount + * Drop table `dropbox` & rm thumbs + * + * @param $netVolumes + * @param $key + * @return bool + * @internal param array $options + */ + public function netunmount($netVolumes, $key) { + $count = 0; + $dropboxUid = ''; + if (isset($netVolumes[$key])) { + $dropboxUid = $netVolumes[$key]['dropboxUid']; + } + foreach($netVolumes as $volume) { + if ($volume['host'] === 'dropbox' && $volume['dropboxUid'] === $dropboxUid) { + $count++; + } + } + if ($count === 1) { + $this->DB->exec('drop table '.$this->DB_TableName); + foreach(glob(rtrim($this->options['tmbPath'], '\\/').DIRECTORY_SEPARATOR.$this->tmbPrefix.'*.png') as $tmb) { + unlink($tmb); + } + } + return true; + } + + /*********************************************************************/ + /* INIT AND CONFIGURE */ + /*********************************************************************/ + + /** + * Prepare FTP connection + * Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn + * + * @return bool + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + **/ + protected function init() { + if (!class_exists('PDO', false)) { + return $this->setError('PHP PDO class is require.'); + } + + if (!$this->options['consumerKey'] + || !$this->options['consumerSecret'] + || !$this->options['accessToken'] + || !$this->options['accessTokenSecret']) { + return $this->setError('Required options undefined.'); + } + + if (empty($this->options['metaCachePath']) && defined('ELFINDER_DROPBOX_META_CACHE_PATH')) { + $this->options['metaCachePath'] = ELFINDER_DROPBOX_META_CACHE_PATH; + } + + // make net mount key + $this->netMountKey = md5(join('-', array('dropbox', $this->options['path']))); + + if (! $this->oauth) { + if (defined('ELFINDER_DROPBOX_USE_CURL_PUT')) { + $this->oauth = new Dropbox_OAuth_Curl($this->options['consumerKey'], $this->options['consumerSecret']); + } else { + if (class_exists('OAuth', false)) { + $this->oauth = new Dropbox_OAuth_PHP($this->options['consumerKey'], $this->options['consumerSecret']); + } else { + if (! class_exists('HTTP_OAuth_Consumer')) { + // We're going to try to load in manually + include 'HTTP/OAuth/Consumer.php'; + } + if (class_exists('HTTP_OAuth_Consumer', false)) { + $this->oauth = new Dropbox_OAuth_PEAR($this->options['consumerKey'], $this->options['consumerSecret']); + } + } + } + } + + if (! $this->oauth) { + return $this->setError('OAuth extension not loaded.'); + } + + // normalize root path + $this->root = $this->options['path'] = $this->_normpath($this->options['path']); + + if (empty($this->options['alias'])) { + $this->options['alias'] = ($this->options['path'] === '/')? 'Dropbox.com' : 'Dropbox'.$this->options['path']; + } + + $this->rootName = $this->options['alias']; + + try { + $this->oauth->setToken($this->options['accessToken'], $this->options['accessTokenSecret']); + $this->dropbox = new Dropbox_API($this->oauth, $this->options['root']); + } catch (Dropbox_Exception $e) { + $this->session->remove('DropboxTokens'); + return $this->setError('Dropbox error: '.$e->getMessage()); + } + + // user + if (empty($this->options['dropboxUid'])) { + try { + $res = $this->dropbox->getAccountInfo(); + $this->options['dropboxUid'] = $res['uid']; + } catch (Dropbox_Exception $e) { + $this->session->remove('DropboxTokens'); + return $this->setError('Dropbox error: '.$e->getMessage()); + } + } + + $this->dropboxUid = $this->options['dropboxUid']; + $this->tmbPrefix = 'dropbox'.base_convert($this->dropboxUid, 10, 32); + + if (!empty($this->options['tmpPath'])) { + if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($this->options['tmpPath'])) { + $this->tmp = $this->options['tmpPath']; + } + } + if (!$this->tmp && is_writable($this->options['tmbPath'])) { + $this->tmp = $this->options['tmbPath']; + } + if (!$this->tmp && ($tmp = elFinder::getStaticVar('commonTempPath'))) { + $this->tmp = $tmp; + } + + if (!empty($this->options['metaCachePath'])) { + if ((is_dir($this->options['metaCachePath']) || mkdir($this->options['metaCachePath'])) && is_writable($this->options['metaCachePath'])) { + $this->metaCache = $this->options['metaCachePath']; + } + } + if (!$this->metaCache && $this->tmp) { + $this->metaCache = $this->tmp; + } + + if (!$this->metaCache) { + return $this->setError('Cache dirctory (metaCachePath or tmp) is require.'); + } + + // setup PDO + if (! $this->options['PDO_DSN']) { + $this->options['PDO_DSN'] = 'sqlite:'.$this->metaCache.DIRECTORY_SEPARATOR.'.elFinder_dropbox_db_'.md5($this->dropboxUid.$this->options['consumerSecret']); + } + // DataBase table name + $this->DB_TableName = $this->options['PDO_DBName']; + // DataBase check or make table + try { + $this->DB = new PDO($this->options['PDO_DSN'], $this->options['PDO_User'], $this->options['PDO_Pass'], $this->options['PDO_Options']); + if (! $this->checkDB()) { + return $this->setError('Can not make DB table'); + } + } catch (PDOException $e) { + return $this->setError('PDO connection failed: '.$e->getMessage()); + } + + $res = $this->deltaCheck($this->isMyReload()); + if ($res !== true) { + if (is_string($res)) { + return $this->setError($res); + } else { + return $this->setError('Could not check API "delta"'); + } + } + + if (is_null($this->options['syncChkAsTs'])) { + $this->options['syncChkAsTs'] = true; + } + if ($this->options['syncChkAsTs']) { + // 'tsPlSleep' minmum 5 sec + $this->options['tsPlSleep'] = max(5, $this->options['tsPlSleep']); + } else { + // 'lsPlSleep' minmum 10 sec + $this->options['lsPlSleep'] = max(10, $this->options['lsPlSleep']); + } + + return true; + } + + + /** + * Configure after successful mount. + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function configure() { + parent::configure(); + + $this->disabled[] = 'archive'; + $this->disabled[] = 'extract'; + } + + /** + * Check DB for delta cache + * + * @return bool + */ + private function checkDB() { + $res = $this->query('SELECT * FROM sqlite_master WHERE type=\'table\' AND name=\''.$this->DB_TableName.'\''); + if ($res && isset($_REQUEST['init'])) { + // check is index(nameidx) UNIQUE? + $chk = $this->query('SELECT sql FROM sqlite_master WHERE type=\'index\' and name=\'nameidx\''); + if (!$chk || strpos(strtoupper($chk[0]), 'UNIQUE') === false) { + // remake + $this->DB->exec('DROP TABLE '.$this->DB_TableName); + $res = false; + } + } + if (! $res) { + try { + $this->DB->exec('CREATE TABLE '.$this->DB_TableName.'(path text, fname text, dat blob, isdir integer);'); + $this->DB->exec('CREATE UNIQUE INDEX nameidx ON '.$this->DB_TableName.'(path, fname)'); + $this->DB->exec('CREATE INDEX isdiridx ON '.$this->DB_TableName.'(isdir)'); + } catch (PDOException $e) { + return $this->setError($e->getMessage()); + } + } + return true; + } + + /** + * DB query and fetchAll + * + * @param string $sql + * @return boolean|array + */ + private function query($sql) { + if ($sth = $this->DB->query($sql)) { + $res = $sth->fetchAll(PDO::FETCH_COLUMN); + } else { + $res = false; + } + return $res; + } + + /** + * Get dat(dropbox metadata) from DB + * + * @param string $path + * @return array dropbox metadata + */ + private function getDBdat($path) { + if ($res = $this->query('select dat from '.$this->DB_TableName.' where path='.$this->DB->quote(strtolower($this->_dirname($path))).' and fname='.$this->DB->quote(strtolower($this->_basename($path))).' limit 1')) { + return unserialize($res[0]); + } else { + return array(); + } + } + + /** + * Update DB dat(dropbox metadata) + * + * @param string $path + * @param array $dat + * @return bool|array + */ + private function updateDBdat($path, $dat) { + return $this->query('update '.$this->DB_TableName.' set dat='.$this->DB->quote(serialize($dat)) + . ', isdir=' . ($dat['is_dir']? 1 : 0) + . ' where path='.$this->DB->quote(strtolower($this->_dirname($path))).' and fname='.$this->DB->quote(strtolower($this->_basename($path)))); + } + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /** + * Close opened connection + * + * @return void + * @author Dmitry (dio) Levashov + **/ + public function umount() { + + } + + /** + * Get delta data and DB update + * + * @param boolean $refresh force refresh + * @return true|string error message + */ + protected function deltaCheck($refresh = true) { + $chk = false; + if (! $refresh && $chk = $this->query('select dat from '.$this->DB_TableName.' where path=\'\' and fname=\'\' limit 1')) { + $chk = unserialize($chk[0]); + } + if ($chk && ($chk['mtime'] + $this->options['metaCacheTime']) > $_SERVER['REQUEST_TIME']) { + return true; + } + + try { + $more = true; + $this->DB->beginTransaction(); + + if ($res = $this->query('select dat from '.$this->DB_TableName.' where path=\'\' and fname=\'\' limit 1')) { + $res = unserialize($res[0]); + $cursor = $res['cursor']; + } else { + $cursor = ''; + } + $delete = false; + $reset = false; + $ptimes = array(); + $now = time(); + do { + ini_set('max_execution_time', 120); + $_info = $this->dropbox->delta($cursor); + if (! empty($_info['reset'])) { + $this->DB->exec('TRUNCATE table '.$this->DB_TableName); + $this->DB->exec('insert into '.$this->DB_TableName.' values(\'\', \'\', \''.serialize(array('cursor' => '', 'mtime' => 0)).'\', 0);'); + $this->DB->exec('insert into '.$this->DB_TableName.' values(\'/\', \'\', \''.serialize(array( + 'path' => '/', + 'is_dir' => 1, + 'mime_type' => '', + 'bytes' => 0 + )).'\', 0);'); + $reset = true; + } + $cursor = $_info['cursor']; + + foreach($_info['entries'] as $entry) { + $key = strtolower($entry[0]); + $pkey = strtolower($this->_dirname($key)); + + $path = $this->DB->quote($pkey); + $fname = $this->DB->quote(strtolower($this->_basename($key))); + $where = 'where path='.$path.' and fname='.$fname; + + if (empty($entry[1])) { + $ptimes[$pkey] = isset($ptimes[$pkey])? max(array($now, $ptimes[$pkey])) : $now; + $this->DB->exec('delete from '.$this->DB_TableName.' '.$where); + ! $delete && $delete = true; + continue; + } + + $_itemTime = strtotime(isset($entry[1]['client_mtime'])? $entry[1]['client_mtime'] : $entry[1]['modified']); + $ptimes[$pkey] = isset($ptimes[$pkey])? max(array($_itemTime, $ptimes[$pkey])) : $_itemTime; + $sql = 'select path from '.$this->DB_TableName.' '.$where.' limit 1'; + if (! $reset && $this->query($sql)) { + $this->DB->exec('update '.$this->DB_TableName.' set dat='.$this->DB->quote(serialize($entry[1])).', isdir='.($entry[1]['is_dir']? 1 : 0).' ' .$where); + } else { + $this->DB->exec('insert into '.$this->DB_TableName.' values ('.$path.', '.$fname.', '.$this->DB->quote(serialize($entry[1])).', '.(int)$entry[1]['is_dir'].')'); + } + } + } while (! empty($_info['has_more'])); + + // update time stamp of parent holder + foreach ($ptimes as $_p => $_t) { + if ($praw = $this->getDBdat($_p)) { + $_update = false; + if (isset($praw['client_mtime']) && $_t > strtotime($praw['client_mtime'])) { + $praw['client_mtime'] = date('r', $_t); + $_update = true; + } + if (isset($praw['modified']) && $_t > strtotime($praw['modified'])) { + $praw['modified'] = date('r', $_t); + $_update = true; + } + if ($_update) { + $pwhere = 'where path='.$this->DB->quote(strtolower($this->_dirname($_p))).' and fname='.$this->DB->quote(strtolower($this->_basename($_p))); + $this->DB->exec('update '.$this->DB_TableName.' set dat='.$this->DB->quote(serialize($praw)).' '.$pwhere); + } + } + } + + $this->DB->exec('update '.$this->DB_TableName.' set dat='.$this->DB->quote(serialize(array('cursor'=>$cursor, 'mtime'=>$_SERVER['REQUEST_TIME']))).' where path=\'\' and fname=\'\''); + if (! $this->DB->commit()) { + $e = $this->DB->errorInfo(); + return $e[2]; + } + if ($delete) { + $this->DB->exec('vacuum'); + } + } catch(Dropbox_Exception $e) { + return $e->getMessage(); + } + return true; + } + + /** + * Parse line from dropbox metadata output and return file stat (array) + * + * @param string $raw line from ftp_rawlist() output + * @return array + * @author Dmitry Levashov + **/ + protected function parseRaw($raw) { + $stat = array(); + + $stat['rev'] = isset($raw['rev'])? $raw['rev'] : 'root'; + $stat['name'] = $this->_basename($raw['path']); + $stat['mime'] = $raw['is_dir']? 'directory' : $raw['mime_type']; + $stat['size'] = $stat['mime'] == 'directory' ? 0 : $raw['bytes']; + $stat['ts'] = isset($raw['client_mtime'])? strtotime($raw['client_mtime']) : + (isset($raw['modified'])? strtotime($raw['modified']) : $_SERVER['REQUEST_TIME']); + $stat['dirs'] = 0; + if ($raw['is_dir']) { + $stat['dirs'] = (int)(bool)$this->query('select path from '.$this->DB_TableName.' where isdir=1 and path='.$this->DB->quote(strtolower($raw['path']))); + } + + if (!empty($raw['url'])) { + $stat['url'] = $raw['url']; + } else if (! $this->disabledGetUrl) { + $stat['url'] = '1'; + } + if (isset($raw['width'])) $stat['width'] = $raw['width']; + if (isset($raw['height'])) $stat['height'] = $raw['height']; + + return $stat; + } + + /** + * Cache dir contents + * + * @param string $path dir path + * @return string + * @author Dmitry Levashov + **/ + protected function cacheDir($path) { + $this->dirsCache[$path] = array(); + $hasDir = false; + + $res = $this->query('select dat from '.$this->DB_TableName.' where path='.$this->DB->quote(strtolower($path))); + + if ($res) { + foreach($res as $raw) { + $raw = unserialize($raw); + if ($stat = $this->parseRaw($raw)) { + $stat = $this->updateCache($raw['path'], $stat); + if (empty($stat['hidden']) && $path !== $raw['path']) { + if (! $hasDir && $stat['mime'] === 'directory') { + $hasDir = true; + } + $this->dirsCache[$path][] = $raw['path']; + } + } + } + } + + if (isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'][$path] = $hasDir; + } + + return $this->dirsCache[$path]; + } + + /** + * Recursive files search + * + * @param string $path dir path + * @param string $q search string + * @param array $mimes + * @return array + * @author Naoki Sawada + **/ + protected function doSearch($path, $q, $mimes) { + $result = array(); + $sth = $this->DB->prepare('select dat from '.$this->DB_TableName.' WHERE path LIKE ? AND fname LIKE ?'); + $sth->execute(array((($path === '/')? '' : strtolower($path)).'%', '%'.strtolower($q).'%')); + $res = $sth->fetchAll(PDO::FETCH_COLUMN); + $timeout = $this->options['searchTimeout']? $this->searchStart + $this->options['searchTimeout'] : 0; + + if ($res) { + foreach($res as $raw) { + if ($timeout && $timeout < time()) { + $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->path($this->encode($path))); + break; + } + + $raw = unserialize($raw); + if ($stat = $this->parseRaw($raw)) { + if (!isset($this->cache[$raw['path']])) { + $stat = $this->updateCache($raw['path'], $stat); + } + if (!empty($stat['hidden']) || ($mimes && $stat['mime'] === 'directory') || !$this->mimeAccepted($stat['mime'], $mimes)) { + continue; + } + $stat = $this->stat($raw['path']); + $stat['path'] = $this->path($stat['hash']); + $result[] = $stat; + } + } + } + return $result; + } + + /** + * Copy file/recursive copy dir only in current volume. + * Return new file path or false. + * + * @param string $src source path + * @param string $dst destination dir path + * @param string $name new file name (optionaly) + * @return string|false + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + **/ + protected function copy($src, $dst, $name) { + + $this->clearcache(); + + return $this->_copy($src, $dst, $name) + ? $this->_joinPath($dst, $name) + : $this->setError(elFinder::ERROR_COPY, $this->_path($src)); + } + + /** + * Remove file/ recursive remove dir + * + * @param string $path file path + * @param bool $force try to remove even if file locked + * @param bool $recursive + * @return bool + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function remove($path, $force = false, $recursive = false) { + $stat = $this->stat($path); + $stat['realpath'] = $path; + $this->rmTmb($stat); + $this->clearcache(); + + if (empty($stat)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path), elFinder::ERROR_FILE_NOT_FOUND); + } + + if (!$force && !empty($stat['locked'])) { + return $this->setError(elFinder::ERROR_LOCKED, $this->_path($path)); + } + + if ($stat['mime'] == 'directory') { + if (!$recursive && !$this->_rmdir($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } else { + if (!$recursive && !$this->_unlink($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } + + $this->removed[] = $stat; + return true; + } + + /** + * Create thumnbnail and return it's URL on success + * + * @param string $path file path + * @param $stat + * @return false|string + * @internal param string $mime file mime type + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function createTmb($path, $stat) { + if (!$stat || !$this->canCreateTmb($path, $stat)) { + return false; + } + + $name = $this->tmbname($stat); + $tmb = $this->tmbPath.DIRECTORY_SEPARATOR.$name; + + // copy image into tmbPath so some drivers does not store files on local fs + if (! $data = $this->getThumbnail($path, $this->options['getTmbSize'])) { + return false; + } + if (! file_put_contents($tmb, $data)) { + return false; + } + + $result = false; + + $tmbSize = $this->tmbSize; + + if (($s = getimagesize($tmb)) == false) { + return false; + } + + /* If image smaller or equal thumbnail size - just fitting to thumbnail square */ + if ($s[0] <= $tmbSize && $s[1] <= $tmbSize) { + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png' ); + + } else { + + if ($this->options['tmbCrop']) { + + /* Resize and crop if image bigger than thumbnail */ + if (!(($s[0] > $tmbSize && $s[1] <= $tmbSize) || ($s[0] <= $tmbSize && $s[1] > $tmbSize) ) || ($s[0] > $tmbSize && $s[1] > $tmbSize)) { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, false, 'png'); + } + + if (($s = getimagesize($tmb)) != false) { + $x = $s[0] > $tmbSize ? intval(($s[0] - $tmbSize)/2) : 0; + $y = $s[1] > $tmbSize ? intval(($s[1] - $tmbSize)/2) : 0; + $result = $this->imgCrop($tmb, $tmbSize, $tmbSize, $x, $y, 'png'); + } + + } else { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, true, 'png'); + } + + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png' ); + } + + if (!$result) { + unlink($tmb); + return false; + } + + return $name; + } + + /** + * Return thumbnail file name for required file + * + * @param array $stat file stat + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function tmbname($stat) { + return $this->tmbPrefix.$stat['rev'].'.png'; + } + + /** + * Get thumbnail from dropbox.com + * @param string $path + * @param string $size + * @return string | boolean + */ + protected function getThumbnail($path, $size = 'small') { + try { + return $this->dropbox->getThumbnail($path, $size); + } catch (Dropbox_Exception $e) { + return false; + } + } + + /** + * Return content URL + * + * @param string $hash file hash + * @param array $options options + * @return array + * @author Naoki Sawada + **/ + public function getContentUrl($hash, $options = array()) { + if (($file = $this->file($hash)) == false || !$file['url'] || $file['url'] == 1) { + $path = $this->decode($hash); + $cache = $this->getDBdat($path); + $url = ''; + if (isset($cache['share']) && strpos($cache['share'], $this->dropbox_dlhost) !== false) { + $res = $this->getHttpResponseHeader($cache['share']); + if (preg_match("/^HTTP\/[01\.]+ ([0-9]{3})/", $res, $match)) { + if ($match[1] < 400) { + $url = $cache['share']; + } + } + } + if (! $url) { + try { + $res = $this->dropbox->share($path, null, false); + $url = $res['url']; + if (strpos($url, 'www.dropbox.com') === false) { + $res = $this->getHttpResponseHeader($url); + if (preg_match('/^location:\s*(http[^\s]+)/im', $res, $match)) { + $url = $match[1]; + } + } + list($url) = explode('?', $url); + $url = str_replace('www.dropbox.com', $this->dropbox_dlhost, $url); + if (! isset($cache['share']) || $cache['share'] !== $url) { + $cache['share'] = $url; + $this->updateDBdat($path, $cache); + } + } catch (Dropbox_Exception $e) { + return false; + } + } + return $url; + } + return $file['url']; + } + + /** + * Get HTTP request response header string + * + * @param string $url target URL + * @return string + * @author Naoki Sawada + */ + private function getHttpResponseHeader($url) { + if (function_exists('curl_exec')) { + + $c = curl_init(); + curl_setopt( $c, CURLOPT_RETURNTRANSFER, true ); + curl_setopt( $c, CURLOPT_CUSTOMREQUEST, 'HEAD' ); + curl_setopt( $c, CURLOPT_HEADER, 1 ); + curl_setopt( $c, CURLOPT_NOBODY, true ); + curl_setopt( $c, CURLOPT_URL, $url ); + $res = curl_exec( $c ); + + } else { + + require_once 'HTTP/Request2.php'; + try { + $request2 = new HTTP_Request2(); + $request2->setConfig(array( + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false + )); + $request2->setUrl($url); + $request2->setMethod(HTTP_Request2::METHOD_HEAD); + $result = $request2->send(); + $res = array(); + $res[] = 'HTTP/'.$result->getVersion().' '.$result->getStatus().' '.$result->getReasonPhrase(); + foreach($result->getHeader() as $key => $val) { + $res[] = $key . ': ' . $val; + } + $res = join("\r\n", $res); + } catch( HTTP_Request2_Exception $e ){ + $res = ''; + } catch (Exception $e){ + $res = ''; + } + + } + return $res; + } + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path + * + * @param string $path file path + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dirname($path) { + return $this->_normpath(substr($path, 0, strrpos($path, '/'))); + } + + /** + * Return file name + * + * @param string $path file path + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _basename($path) { + return substr($path, strrpos($path, '/') + 1); + } + + /** + * Join dir name and file name and retur full path + * + * @param string $dir + * @param string $name + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _joinPath($dir, $name) { + return $this->_normpath($dir.'/'.$name); + } + + /** + * Return normalized path, this works the same as os.path.normpath() in Python + * + * @param string $path path + * @return string + * @author Troex Nevelin + **/ + protected function _normpath($path) { + $path = '/' . ltrim($path, '/'); + return $path; + } + + /** + * Return file path related to root dir + * + * @param string $path file path + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _relpath($path) { + return $path; + } + + /** + * Convert path related to root dir into real path + * + * @param string $path file path + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _abspath($path) { + return $path; + } + + /** + * Return fake path started from root dir + * + * @param string $path file path + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _path($path) { + return $this->rootName . $this->_normpath(substr($path, strlen($this->root))); + } + + /** + * Return true if $path is children of $parent + * + * @param string $path path to check + * @param string $parent parent path + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _inpath($path, $parent) { + return $path == $parent || strpos($path, $parent.'/') === 0; + } + + /***************** file stat ********************/ + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally + * + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * @return array|false + * @author Dmitry (dio) Levashov + **/ + protected function _stat($path) { + //if (!empty($this->ARGS['reload']) && isset($this->ARGS['target']) && strpos($this->ARGS['target'], $this->id) === 0) { + if ($this->isMyReload()) { + $this->deltaCheck(); + } + if ($raw = $this->getDBdat($path)) { + return $this->parseRaw($raw); + } + return false; + } + + /** + * Return true if path is dir and has at least one childs directory + * + * @param string $path dir path + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _subdirs($path) { + return ($stat = $this->stat($path)) && isset($stat['dirs']) ? $stat['dirs'] : false; + } + + /** + * Return object width and height + * Ususaly used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dimensions($path, $mime) { + if (strpos($mime, 'image') !== 0) return ''; + $cache = $this->getDBdat($path); + if (isset($cache['width']) && isset($cache['height'])) { + return $cache['width'].'x'.$cache['height']; + } + $ret = ''; + if ($work = $this->getWorkFile($path)) { + if ($size = getimagesize($work)) { + $cache['width'] = $size[0]; + $cache['height'] = $size[1]; + $this->updateDBdat($path, $cache); + $ret = $size[0].'x'.$size[1]; + } + } + is_file($work) && unlink($work); + return $ret; + } + + /******************** file/dir content *********************/ + + /** + * Return files list in directory. + * + * @param string $path dir path + * @return array + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + **/ + protected function _scandir($path) { + return isset($this->dirsCache[$path]) + ? $this->dirsCache[$path] + : $this->cacheDir($path); + } + + /** + * Open file and return file pointer + * + * @param string $path file path + * @param string $mode + * @return false|resource + * @internal param bool $write open file for writing + * @author Dmitry (dio) Levashov + */ + protected function _fopen($path, $mode='rb') { + + if (($mode == 'rb' || $mode == 'r')) { + try { + $res = $this->dropbox->media($path); + $url = parse_url($res['url']); + $fp = stream_socket_client('ssl://'.$url['host'].':443'); + fputs($fp, "GET {$url['path']} HTTP/1.0\r\n"); + fputs($fp, "Host: {$url['host']}\r\n"); + fputs($fp, "\r\n"); + while(trim(fgets($fp)) !== ''){}; + return $fp; + } catch (Dropbox_Exception $e) { + return false; + } + } + + if ($this->tmp) { + $contents = $this->_getContents($path); + + if ($contents === false) { + return false; + } + + if ($local = $this->getTempFile($path)) { + if (file_put_contents($local, $contents, LOCK_EX) !== false) { + return fopen($local, $mode); + } + } + } + + return false; + } + + /** + * Close opened file + * + * @param resource $fp file pointer + * @param string $path + * @return bool + * @author Dmitry (dio) Levashov + */ + protected function _fclose($fp, $path='') { + fclose($fp); + if ($path) { + unlink($this->getTempFile($path)); + } + } + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed + * + * @param string $path parent dir path + * @param string $name new directory name + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkdir($path, $name) { + $path = $this->_normpath($path.'/'.$name); + try { + $this->dropbox->createFolder($path); + } catch (Dropbox_Exception $e) { + $this->deltaCheck(); + if ($this->dir($this->encode($path))) { + return $path; + } + return $this->setError('Dropbox error: '.$e->getMessage()); + } + $this->deltaCheck(); + return $path; + } + + /** + * Create file and return it's path or false on failed + * + * @param string $path parent dir path + * @param string $name new file name + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkfile($path, $name) { + return $this->_filePutContents($path.'/'.$name, ''); + } + + /** + * Create symlink. FTP driver does not support symlinks. + * + * @param string $target link target + * @param string $path symlink path + * @param string $name + * @return bool + * @author Dmitry (dio) Levashov + */ + protected function _symlink($target, $path, $name) { + return false; + } + + /** + * Copy file into another file + * + * @param string $source source file path + * @param string $targetDir target directory path + * @param string $name new file name + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _copy($source, $targetDir, $name) { + $path = $this->_normpath($targetDir.'/'.$name); + try { + $this->dropbox->copy($source, $path); + } catch (Dropbox_Exception $e) { + return $this->setError('Dropbox error: '.$e->getMessage()); + } + $this->deltaCheck(); + return true; + } + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param $targetDir + * @param string $name file name + * @return bool|string + * @internal param string $target target dir path + * @author Dmitry (dio) Levashov + */ + protected function _move($source, $targetDir, $name) { + $target = $this->_normpath($targetDir.'/'.$name); + try { + $this->dropbox->move($source, $target); + } catch (Dropbox_Exception $e) { + return $this->setError('Dropbox error: '.$e->getMessage()); + } + $this->deltaCheck(); + return $target; + } + + /** + * Remove file + * + * @param string $path file path + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _unlink($path) { + try { + $this->dropbox->delete($path); + } catch (Dropbox_Exception $e) { + return $this->setError('Dropbox error: '.$e->getMessage()); + } + $this->deltaCheck(); + return true; + } + + /** + * Remove dir + * + * @param string $path dir path + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _rmdir($path) { + return $this->_unlink($path); + } + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param string $path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * @return bool|string + * @internal param string $dir target dir path + * @author Dmitry (dio) Levashov + */ + protected function _save($fp, $path, $name, $stat) { + if ($name) $path .= '/'.$name; + $path = $this->_normpath($path); + try { + $this->dropbox->putFile($path, $fp); + } catch (Dropbox_Exception $e) { + return $this->setError('Dropbox error: '.$e->getMessage()); + } + $this->deltaCheck(); + if (is_array($stat)) { + $raw = $this->getDBdat($path); + if (isset($stat['width'])) $raw['width'] = $stat['width']; + if (isset($stat['height'])) $raw['height'] = $stat['height']; + $this->updateDBdat($path, $raw); + } + return $path; + } + + /** + * Get file contents + * + * @param string $path file path + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function _getContents($path) { + $contents = ''; + try { + $contents = $this->dropbox->getFile($path); + } catch (Dropbox_Exception $e) { + return $this->setError('Dropbox error: '.$e->getMessage()); + } + return $contents; + } + + /** + * Write a string to a file + * + * @param string $path file path + * @param string $content new file content + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _filePutContents($path, $content) { + $res = false; + + if ($local = $this->getTempFile($path)) { + if (file_put_contents($local, $content, LOCK_EX) !== false + && ($fp = fopen($local, 'rb'))) { + clearstatcache(); + $res = $this->_save($fp, $path, '', array()); + fclose($fp); + } + file_exists($local) && unlink($local); + } + + return $res; + } + + /** + * Detect available archivers + * + * @return array + **/ + protected function _checkArchivers() { + // die('Not yet implemented. (_checkArchivers)'); + return array(); + } + + /** + * chmod implementation + * + * @param string $path + * @param string $mode + * @return bool + */ + protected function _chmod($path, $mode) { + return false; + } + + /** + * Unpack archive + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * @return true + * @return void + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + **/ + protected function _unpack($path, $arc) { + die('Not yet implemented. (_unpack)'); + } + + /** + * Recursive symlinks search + * + * @param string $path file/dir path + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _findSymlinks($path) { + die('Not yet implemented. (_findSymlinks)'); + } + + /** + * Extract files from archive + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * @return true + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _extract($path, $arc) { + die('Not yet implemented. (_extract)'); + + } + + /** + * Create archive and return its path + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * @return string|bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _archive($dir, $files, $name, $arc) { + die('Not yet implemented. (_archive)'); + } + +} // END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDropbox2.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDropbox2.class.php new file mode 100644 index 0000000..f235517 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeDropbox2.class.php @@ -0,0 +1,1516 @@ + '', + 'app_secret' => '', + 'access_token' => '', + 'aliasFormat' => '%s@Dropbox', + 'path' => '/', + 'separator' => '/', + 'acceptedName' => '#^[^\\\/]+$#', + 'rootCssClass' => 'elfinder-navbar-root-dropbox', + 'publishPermission' => [ + 'requested_visibility' => 'public', + //'link_password' => '', + //'expires' => '', + ], + 'getThumbSize' => 'medium', // Available sizes: 'thumb', 'small', 'medium', 'large', 'huge' + ]; + $this->options = array_merge($this->options, $opts); + $this->options['mimeDetect'] = 'internal'; + } + + /*********************************************************************/ + /* ORIGINAL FUNCTIONS */ + /*********************************************************************/ + + /** + * Get Parent ID, Item ID, Parent Path as an array from path. + * + * @param string $path + * + * @return array + */ + protected function _db_splitPath($path) + { + $path = trim($path, '/'); + if ($path === '') { + $dirname = '/'; + $basename = ''; + } else { + $pos = strrpos($path, '/'); + if ($pos === false) { + $dirname = '/'; + $basename = $path; + } else { + $dirname = '/' . substr($path, 0, $pos); + $basename = substr($path, $pos + 1); + } + } + + return [$dirname, $basename]; + } + + /** + * Get dat(Dropbox metadata) from Dropbox. + * + * @param string $path + * + * @return boolean|object Dropbox metadata + */ + private function _db_getFile($path) + { + if ($path === '/') { + return true; + } + + $res = false; + try { + $file = $this->service->getMetadata($path, $this->FETCH_OPTIONS); + if ($file instanceof FolderMetadata || $file instanceof FileMetadata) { + $res = $file; + } + + return $res; + } catch (DropboxClientException $e) { + return false; + } + } + + /** + * Parse line from Dropbox metadata output and return file stat (array). + * + * @param object $raw line from ftp_rawlist() output + * + * @return array + * @author Naoki Sawada + **/ + protected function _db_parseRaw($raw) + { + $stat = []; + $isFolder = false; + if ($raw === true) { + // root folder + $isFolder = true; + $stat['name'] = ''; + $stat['iid'] = '0'; + } + + $data = []; + if (is_object($raw)) { + $isFolder = $raw instanceof FolderMetadata; + $data = $raw->getData(); + } elseif (is_array($raw)) { + $isFolder = $raw['.tag'] === 'folder'; + $data = $raw; + } + + if (isset($data['path_lower'])) { + $stat['path'] = $data['path_lower']; + } + + if (isset($data['name'])) { + $stat['name'] = $data['name']; + } + + if (isset($data['id'])) { + $stat['iid'] = substr($data['id'], 3); + } + + if ($isFolder) { + $stat['mime'] = 'directory'; + $stat['size'] = 0; + $stat['ts'] = 0; + $stat['dirs'] = -1; + } else { + $stat['size'] = isset($data['size']) ? (int)$data['size'] : 0; + if (isset($data['server_modified'])) { + $stat['ts'] = strtotime($data['server_modified']); + } elseif (isset($data['client_modified'])) { + $stat['ts'] = strtotime($data['client_modified']); + } else { + $stat['ts'] = 0; + } + $stat['url'] = '1'; + } + + return $stat; + } + + /** + * Get thumbnail from Dropbox. + * + * @param string $path + * @param string $size + * + * @return string | boolean + */ + protected function _db_getThumbnail($path) + { + try { + return $this->service->getThumbnail($path, $this->options['getThumbSize'])->getContents(); + } catch (DropboxClientException $e) { + return false; + } + } + + /** + * Join dir name and file name(display name) and retur full path. + * + * @param string $dir + * @param string $displayName + * + * @return string + */ + protected function _db_joinName($dir, $displayName) + { + return rtrim($dir, '/') . '/' . $displayName; + } + + /** + * Get OAuth2 access token form OAuth1 tokens. + * + * @param string $app_key + * @param string $app_secret + * @param string $oauth1_token + * @param string $oauth1_secret + * + * @return string|false + */ + public static function getTokenFromOauth1($app_key, $app_secret, $oauth1_token, $oauth1_secret) + { + $data = [ + 'oauth1_token' => $oauth1_token, + 'oauth1_token_secret' => $oauth1_secret, + ]; + $auth = base64_encode($app_key . ':' . $app_secret); + + $ch = curl_init('https://api.dropboxapi.com/2/auth/token/from_oauth1'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Basic ' . $auth, + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + $result = curl_exec($ch); + curl_close($ch); + + $res = $result ? json_decode($result, true) : []; + + return isset($res['oauth2_token']) ? $res['oauth2_token'] : false; + } + + /*********************************************************************/ + /* EXTENDED FUNCTIONS */ + /*********************************************************************/ + + /** + * Prepare + * Call from elFinder::netmout() before volume->mount(). + * + * @return array + * @author Naoki Sawada + **/ + public function netmountPrepare($options) + { + if (empty($options['app_key']) && defined('ELFINDER_DROPBOX_APPKEY')) { + $options['app_key'] = ELFINDER_DROPBOX_APPKEY; + } + if (empty($options['app_secret']) && defined('ELFINDER_DROPBOX_APPSECRET')) { + $options['app_secret'] = ELFINDER_DROPBOX_APPSECRET; + } + + if (!isset($options['pass'])) { + $options['pass'] = ''; + } + + try { + $app = new DropboxApp($options['app_key'], $options['app_secret']); + $dropbox = new Dropbox($app); + $authHelper = $dropbox->getAuthHelper(); + + if ($options['pass'] === 'reauth') { + $options['pass'] = ''; + $this->session->set('Dropbox2AuthParams', [])->set('Dropbox2Tokens', []); + } elseif ($options['pass'] === 'dropbox2') { + $options['pass'] = ''; + } + + $options = array_merge($this->session->get('Dropbox2AuthParams', []), $options); + + if (!isset($options['tokens'])) { + $options['tokens'] = $this->session->get('Dropbox2Tokens', []); + $this->session->remove('Dropbox2Tokens'); + } + $aToken = $options['tokens']; + if (!is_array($aToken) || !isset($aToken['access_token'])) { + $aToken = []; + } + + $service = null; + if ($aToken) { + try { + $dropbox->setAccessToken($aToken['access_token']); + $this->session->set('Dropbox2AuthParams', $options); + } catch (DropboxClientException $e) { + $aToken = []; + $options['tokens'] = []; + if ($options['user'] !== 'init') { + $this->session->set('Dropbox2AuthParams', $options); + + return ['exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE]; + } + } + } + + if ((isset($options['user']) && $options['user'] === 'init') || (isset($_GET['host']) && $_GET['host'] == '1')) { + if (empty($options['url'])) { + $options['url'] = elFinder::getConnectorUrl(); + } + + if (!empty($options['id'])) { + $callback = $options['url'] + . (strpos($options['url'], '?') !== false? '&' : '?') . 'cmd=netmount&protocol=dropbox2&host=' . ($options['id'] === 'elfinder'? '1' : $options['id']); + } + + $itpCare = isset($options['code']); + $code = $itpCare? $options['code'] : (isset($_GET['code'])? $_GET['code'] : ''); + $state = $itpCare? $options['state'] : (isset($_GET['state'])? $_GET['state'] : ''); + if (!$aToken && empty($code)) { + $url = $authHelper->getAuthUrl($callback); + + $html = ''; + $html .= ''; + if (empty($options['pass']) && $options['host'] !== '1') { + $options['pass'] = 'return'; + $this->session->set('Dropbox2AuthParams', $options); + + return ['exit' => true, 'body' => $html]; + } else { + $out = [ + 'node' => $options['id'], + 'json' => '{"protocol": "dropbox2", "mode": "makebtn", "body" : "' . str_replace($html, '"', '\\"') . '", "error" : "' . elFinder::ERROR_ACCESS_DENIED . '"}', + 'bind' => 'netmount', + ]; + + return ['exit' => 'callback', 'out' => $out]; + } + } else { + if ($code && $state) { + if (!empty($options['id'])) { + // see https://github.com/kunalvarma05/dropbox-php-sdk/issues/115 + $authHelper->getPersistentDataStore()->set('state', filter_var($state, FILTER_SANITIZE_STRING)); + $tokenObj = $authHelper->getAccessToken($code, $state, $callback); + $options['tokens'] = [ + 'access_token' => $tokenObj->getToken(), + 'uid' => $tokenObj->getUid(), + ]; + unset($options['code'], $options['state']); + $this->session->set('Dropbox2Tokens', $options['tokens'])->set('Dropbox2AuthParams', $options); + $out = [ + 'node' => $options['id'], + 'json' => '{"protocol": "dropbox2", "mode": "done", "reset": 1}', + 'bind' => 'netmount', + ]; + } else { + $nodeid = ($_GET['host'] === '1')? 'elfinder' : $_GET['host']; + $out = [ + 'node' => $nodeid, + 'json' => json_encode(array( + 'protocol' => 'dropbox2', + 'host' => $nodeid, + 'mode' => 'redirect', + 'options' => array( + 'id' => $nodeid, + 'code' => $code, + 'state' => $state + ) + )), + 'bind' => 'netmount' + ]; + } + if (!$itpCare) { + return array('exit' => 'callback', 'out' => $out); + } else { + return array('exit' => true, 'body' => $out['json']); + } + } + $path = $options['path']; + $folders = []; + $listFolderContents = $dropbox->listFolder($path); + $items = $listFolderContents->getItems(); + foreach ($items as $item) { + $data = $item->getData(); + if ($data['.tag'] === 'folder') { + $folders[$data['path_lower']] = $data['name']; + } + } + natcasesort($folders); + + if ($options['pass'] === 'folders') { + return ['exit' => true, 'folders' => $folders]; + } + + $folders = ['/' => '/'] + $folders; + $folders = json_encode($folders); + $json = '{"protocol": "dropbox2", "mode": "done", "folders": ' . $folders . '}'; + $options['pass'] = 'return'; + $html = 'Dropbox.com'; + $html .= ''; + $this->session->set('Dropbox2AuthParams', $options); + + return ['exit' => true, 'body' => $html]; + } + } + } catch (DropboxClientException $e) { + $this->session->remove('Dropbox2AuthParams')->remove('Dropbox2Tokens'); + if (empty($options['pass'])) { + return ['exit' => true, 'body' => '{msg:' . elFinder::ERROR_ACCESS_DENIED . '}' . ' ' . $e->getMessage()]; + } else { + return ['exit' => true, 'error' => [elFinder::ERROR_ACCESS_DENIED, $e->getMessage()]]; + } + } + + if (!$aToken) { + return ['exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE]; + } + + if ($options['path'] === 'root') { + $options['path'] = '/'; + } + + try { + if ($options['path'] !== '/') { + $file = $dropbox->getMetadata($options['path']); + $name = $file->getName(); + } else { + $name = 'root'; + } + $options['alias'] = sprintf($this->options['aliasFormat'], $name); + } catch (DropboxClientException $e) { + return ['exit' => true, 'error' => $e->getMessage()]; + } + + foreach (['host', 'user', 'pass', 'id', 'offline'] as $key) { + unset($options[$key]); + } + + return $options; + } + + /** + * process of on netunmount + * Drop `Dropbox` & rm thumbs. + * + * @param array $options + * + * @return bool + */ + public function netunmount($netVolumes, $key) + { + if ($tmbs = glob(rtrim($this->options['tmbPath'], '\\/') . DIRECTORY_SEPARATOR . $this->driverId . '_' . $this->options['tokens']['uid'] . '*.png')) { + foreach ($tmbs as $file) { + unlink($file); + } + } + + return true; + } + + /*********************************************************************/ + /* INIT AND CONFIGURE */ + /*********************************************************************/ + + /** + * Prepare Dropbox connection + * Connect to remote server and check if credentials are correct, if so, store the connection id in $this->service. + * + * @return bool + * @author Naoki Sawada + **/ + protected function init() + { + if (empty($this->options['app_key'])) { + if (defined('ELFINDER_DROPBOX_APPKEY') && ELFINDER_DROPBOX_APPKEY) { + $this->options['app_key'] = ELFINDER_DROPBOX_APPKEY; + } else { + return $this->setError('Required option "app_key" is undefined.'); + } + } + if (empty($this->options['app_secret'])) { + if (defined('ELFINDER_DROPBOX_APPSECRET') && ELFINDER_DROPBOX_APPSECRET) { + $this->options['app_secret'] = ELFINDER_DROPBOX_APPSECRET; + } else { + return $this->setError('Required option "app_secret" is undefined.'); + } + } + if (isset($this->options['tokens']) && is_array($this->options['tokens']) && !empty($this->options['tokens']['access_token'])) { + $this->options['access_token'] = $this->options['tokens']['access_token']; + } + if (!$this->options['access_token']) { + return $this->setError('Required option "access_token" or "refresh_token" is undefined.'); + } + + try { + // make net mount key for network mount + $aToken = $this->options['access_token']; + $this->netMountKey = md5($aToken . '-' . $this->options['path']); + + $errors = []; + if ($this->needOnline && !$this->service) { + $app = new DropboxApp($this->options['app_key'], $this->options['app_secret'], $aToken); + $this->service = new Dropbox($app); + // to check access_token + $this->service->getCurrentAccount(); + } + } catch (DropboxClientException $e) { + $errors[] = 'Dropbox error: ' . $e->getMessage(); + } catch (Exception $e) { + $errors[] = $e->getMessage(); + } + + if ($this->needOnline && !$this->service) { + $errors[] = 'Dropbox Service could not be loaded.'; + } + + if ($errors) { + return $this->setError($errors); + } + + // normalize root path + $this->options['path'] = strtolower($this->options['path']); + if ($this->options['path'] == 'root') { + $this->options['path'] = '/'; + } + $this->root = $this->options['path'] = $this->_normpath($this->options['path']); + + if (empty($this->options['alias'])) { + $this->options['alias'] = sprintf($this->options['aliasFormat'], ($this->options['path'] === '/') ? 'Root' : $this->_basename($this->options['path'])); + if (!empty($this->options['netkey'])) { + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']); + } + } + + $this->rootName = $this->options['alias']; + + if (!empty($this->options['tmpPath'])) { + if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($this->options['tmpPath'])) { + $this->tmp = $this->options['tmpPath']; + } + } + + if (!$this->tmp && ($tmp = elFinder::getStaticVar('commonTempPath'))) { + $this->tmp = $tmp; + } + + // This driver dose not support `syncChkAsTs` + $this->options['syncChkAsTs'] = false; + + // 'lsPlSleep' minmum 10 sec + $this->options['lsPlSleep'] = max(10, $this->options['lsPlSleep']); + + // enable command archive + $this->options['useRemoteArchive'] = true; + + return true; + } + + /** + * Configure after successfull mount. + * + * @author Naoki Sawada + * @throws elFinderAbortException + */ + protected function configure() + { + parent::configure(); + + // fallback of $this->tmp + if (!$this->tmp && $this->tmbPathWritable) { + $this->tmp = $this->tmbPath; + } + + if ($this->isMyReload()) { + //$this->_db_getDirectoryData(false); + } + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /** + * Close opened connection. + **/ + public function umount() + { + } + + /** + * Cache dir contents. + * + * @param string $path dir path + * + * @return + * @author Naoki Sawada + */ + protected function cacheDir($path) + { + $this->dirsCache[$path] = []; + $hasDir = false; + + $res = $this->service->listFolder($path, $this->FETCH_OPTIONS); + + if ($res) { + $items = $res->getItems()->all(); + foreach ($items as $raw) { + if ($stat = $this->_db_parseRaw($raw)) { + $mountPath = $this->_joinPath($path, $stat['name']); + $stat = $this->updateCache($mountPath, $stat); + if (empty($stat['hidden']) && $path !== $mountPath) { + if (!$hasDir && $stat['mime'] === 'directory') { + $hasDir = true; + } + $this->dirsCache[$path][] = $mountPath; + } + } + } + } + + if (isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'][$path] = $hasDir; + } + + return $this->dirsCache[$path]; + } + + /** + * Recursive files search. + * + * @param string $path dir path + * @param string $q search string + * @param array $mimes + * + * @return array + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function doSearch($path, $q, $mimes) + { + if (!empty($this->doSearchCurrentQuery['matchMethod']) || $mimes) { + // has custom match method or mimes, use elFinderVolumeDriver::doSearch() + return parent::doSearch($path, $q, $mimes); + } + + $timeout = $this->options['searchTimeout'] ? $this->searchStart + $this->options['searchTimeout'] : 0; + + $searchRes = $this->service->search($path, $q, ['start' => 0, 'max_results' => 1000]); + $items = $searchRes->getItems(); + $more = $searchRes->hasMoreItems(); + while ($more) { + if ($timeout && $timeout < time()) { + $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->_path($path)); + break; + } + $searchRes = $this->service->search($path, $q, ['start' => $searchRes->getCursor(), 'max_results' => 1000]); + $more = $searchRes->hasMoreItems(); + $items = $items->merge($searchRes->getItems()); + } + + $result = []; + foreach ($items as $raw) { + if ($stat = $this->_db_parseRaw($raw->getMetadata())) { + $stat = $this->updateCache($stat['path'], $stat); + if (empty($stat['hidden'])) { + $result[] = $stat; + } + } + } + + return $result; + } + + /** + * Copy file/recursive copy dir only in current volume. + * Return new file path or false. + * + * @param string $src source path + * @param string $dst destination dir path + * @param string $name new file name (optionaly) + * + * @return string|false + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function copy($src, $dst, $name) + { + $srcStat = $this->stat($src); + $target = $this->_joinPath($dst, $name); + $tgtStat = $this->stat($target); + if ($tgtStat) { + if ($srcStat['mime'] === 'directory') { + return parent::copy($src, $dst, $name); + } else { + $this->_unlink($target); + } + } + $this->clearcache(); + if ($res = $this->_copy($src, $dst, $name)) { + $this->added[] = $this->stat($target); + $res = $target; + } + + return $res; + } + + /** + * Remove file/ recursive remove dir. + * + * @param string $path file path + * @param bool $force try to remove even if file locked + * @param bool $recursive + * + * @return bool + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function remove($path, $force = false, $recursive = false) + { + $stat = $this->stat($path); + $stat['realpath'] = $path; + $this->rmTmb($stat); + $this->clearcache(); + + if (empty($stat)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path), elFinder::ERROR_FILE_NOT_FOUND); + } + + if (!$force && !empty($stat['locked'])) { + return $this->setError(elFinder::ERROR_LOCKED, $this->_path($path)); + } + + if ($stat['mime'] == 'directory') { + if (!$recursive && !$this->_rmdir($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } else { + if (!$recursive && !$this->_unlink($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } + + $this->removed[] = $stat; + + return true; + } + + /** + * Create thumnbnail and return it's URL on success. + * + * @param string $path file path + * @param $stat + * + * @return string|false + * @throws ImagickException + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function createTmb($path, $stat) + { + if (!$stat || !$this->canCreateTmb($path, $stat)) { + return false; + } + + $name = $this->tmbname($stat); + $tmb = $this->tmbPath . DIRECTORY_SEPARATOR . $name; + + // copy image into tmbPath so some drivers does not store files on local fs + if (!$data = $this->_db_getThumbnail($path)) { + return false; + } + if (!file_put_contents($tmb, $data)) { + return false; + } + + $tmbSize = $this->tmbSize; + + if (($s = getimagesize($tmb)) == false) { + return false; + } + + $result = true; + + /* If image smaller or equal thumbnail size - just fitting to thumbnail square */ + if ($s[0] <= $tmbSize && $s[1] <= $tmbSize) { + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } else { + if ($this->options['tmbCrop']) { + + /* Resize and crop if image bigger than thumbnail */ + if (!(($s[0] > $tmbSize && $s[1] <= $tmbSize) || ($s[0] <= $tmbSize && $s[1] > $tmbSize)) || ($s[0] > $tmbSize && $s[1] > $tmbSize)) { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, false, 'png'); + } + + if ($result && ($s = getimagesize($tmb)) != false) { + $x = $s[0] > $tmbSize ? intval(($s[0] - $tmbSize) / 2) : 0; + $y = $s[1] > $tmbSize ? intval(($s[1] - $tmbSize) / 2) : 0; + $result = $this->imgCrop($tmb, $tmbSize, $tmbSize, $x, $y, 'png'); + } + } else { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, true, 'png'); + } + + if ($result) { + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + + } + } + + if (!$result) { + unlink($tmb); + + return false; + } + + return $name; + } + + /** + * Return thumbnail file name for required file. + * + * @param array $stat file stat + * + * @return string + * @author Naoki Sawada + **/ + protected function tmbname($stat) + { + $name = $this->driverId . '_'; + if (isset($this->options['tokens']) && is_array($this->options['tokens'])) { + $name .= $this->options['tokens']['uid']; + } + + return $name . md5($stat['iid']) . $stat['ts'] . '.png'; + } + + /** + * Return content URL (for netmout volume driver) + * If file.url == 1 requests from JavaScript client with XHR. + * + * @param string $hash file hash + * @param array $options options array + * + * @return bool|string + * @author Naoki Sawada + */ + public function getContentUrl($hash, $options = []) + { + if (!empty($options['onetime']) && $this->options['onetimeUrl']) { + return parent::getContentUrl($hash, $options); + } + if (!empty($options['temporary'])) { + // try make temporary file + $url = parent::getContentUrl($hash, $options); + if ($url) { + return $url; + } + } + $file = $this->file($hash); + if (($file = $this->file($hash)) !== false && (!$file['url'] || $file['url'] == 1)) { + $path = $this->decode($hash); + $url = ''; + try { + $res = $this->service->postToAPI('/sharing/list_shared_links', ['path' => $path, 'direct_only' => true])->getDecodedBody(); + if ($res && !empty($res['links'])) { + foreach ($res['links'] as $link) { + if (isset($link['link_permissions']) + && isset($link['link_permissions']['requested_visibility']) + && $link['link_permissions']['requested_visibility']['.tag'] === $this->options['publishPermission']['requested_visibility']) { + $url = $link['url']; + break; + } + } + } + if (!$url) { + $res = $this->service->postToAPI('/sharing/create_shared_link_with_settings', ['path' => $path, 'settings' => $this->options['publishPermission']])->getDecodedBody(); + if (isset($res['url'])) { + $url = $res['url']; + } + } + if ($url) { + $url = str_replace('www.dropbox.com', 'dl.dropboxusercontent.com', $url); + $url = str_replace('?dl=0', '', $url); + + return $url; + } + } catch (DropboxClientException $e) { + return $this->setError('Dropbox error: ' . $e->getMessage()); + } + } + + return false; + } + + /** + * Return debug info for client. + * + * @return array + **/ + public function debug() + { + $res = parent::debug(); + if (!empty($this->options['netkey']) && isset($this->options['tokens']) && !empty($this->options['tokens']['uid'])) { + $res['Dropbox uid'] = $this->options['tokens']['uid']; + $res['access_token'] = $this->options['tokens']['access_token']; + } + + return $res; + } + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path. + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function _dirname($path) + { + list($dirname) = $this->_db_splitPath($path); + + return $dirname; + } + + /** + * Return file name. + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function _basename($path) + { + list(, $basename) = $this->_db_splitPath($path); + + return $basename; + } + + /** + * Join dir name and file name and retur full path. + * + * @param string $dir + * @param string $name + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _joinPath($dir, $name) + { + return rtrim($dir, '/') . '/' . strtolower($name); + } + + /** + * Return normalized path, this works the same as os.path.normpath() in Python. + * + * @param string $path path + * + * @return string + * @author Naoki Sawada + **/ + protected function _normpath($path) + { + return '/' . ltrim($path, '/'); + } + + /** + * Return file path related to root dir. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _relpath($path) + { + if ($path === $this->root) { + return ''; + } else { + return ltrim(substr($path, strlen($this->root)), '/'); + } + } + + /** + * Convert path related to root dir into real path. + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function _abspath($path) + { + if ($path === '/') { + return $this->root; + } else { + return $this->_joinPath($this->root, $path); + } + } + + /** + * Return fake path started from root dir. + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function _path($path) + { + $path = $this->_normpath(substr($path, strlen($this->root))); + + return $path; + } + + /** + * Return true if $path is children of $parent. + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Naoki Sawada + **/ + protected function _inpath($path, $parent) + { + return $path == $parent || strpos($path, $parent . '/') === 0; + } + + /***************** file stat ********************/ + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally. + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + protected function _stat($path) + { + if ($raw = $this->_db_getFile($path)) { + return $this->_db_parseRaw($raw); + } + + return false; + } + + /** + * Return true if path is dir and has at least one childs directory. + * + * @param string $path dir path + * + * @return bool + * @author Naoki Sawada + **/ + protected function _subdirs($path) + { + $hasdir = false; + + try { + $res = $this->service->listFolder($path); + if ($res) { + $items = $res->getItems(); + foreach ($items as $raw) { + if ($raw instanceof FolderMetadata) { + $hasdir = true; + break; + } + } + } + } catch (DropboxClientException $e) { + $this->setError('Dropbox error: ' . $e->getMessage()); + } + + return $hasdir; + } + + /** + * Return object width and height + * Ususaly used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * + * @return string + * @throws ImagickException + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function _dimensions($path, $mime) + { + if (strpos($mime, 'image') !== 0) { + return ''; + } + $ret = ''; + + if ($data = $this->_getContents($path)) { + $tmp = $this->getTempFile(); + file_put_contents($tmp, $data); + $size = getimagesize($tmp); + if ($size) { + $ret = array('dim' => $size[0] . 'x' . $size[1]); + $srcfp = fopen($tmp, 'rb'); + $target = isset(elFinder::$currentArgs['target'])? elFinder::$currentArgs['target'] : ''; + if ($subImgLink = $this->getSubstituteImgLink($target, $size, $srcfp)) { + $ret['url'] = $subImgLink; + } + } + } + + return $ret; + } + + /******************** file/dir content *********************/ + + /** + * Return files list in directory. + * + * @param string $path dir path + * + * @return array + * @author Naoki Sawada + **/ + protected function _scandir($path) + { + return isset($this->dirsCache[$path]) + ? $this->dirsCache[$path] + : $this->cacheDir($path); + } + + /** + * Open file and return file pointer. + * + * @param string $path file path + * @param bool $write open file for writing + * + * @return resource|false + * @author Naoki Sawada + **/ + protected function _fopen($path, $mode = 'rb') + { + if ($mode === 'rb' || $mode === 'r') { + if ($link = $this->service->getTemporaryLink($path)) { + $access_token = $this->service->getAccessToken(); + if ($access_token) { + $data = array( + 'target' => $link->getLink(), + 'headers' => array('Authorization: Bearer ' . $access_token), + ); + + // to support range request + if (func_num_args() > 2) { + $opts = func_get_arg(2); + } else { + $opts = array(); + } + if (!empty($opts['httpheaders'])) { + $data['headers'] = array_merge($opts['httpheaders'], $data['headers']); + } + + return elFinder::getStreamByUrl($data); + } + } + } + + return false; + } + + /** + * Close opened file. + * + * @param resource $fp file pointer + * + * @return bool + * @author Naoki Sawada + **/ + protected function _fclose($fp, $path = '') + { + is_resource($fp) && fclose($fp); + } + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed. + * + * @param string $path parent dir path + * @param string $name new directory name + * + * @return string|bool + * @author Naoki Sawada + **/ + protected function _mkdir($path, $name) + { + try { + return $this->service->createFolder($this->_db_joinName($path, $name))->getPathLower(); + } catch (DropboxClientException $e) { + return $this->setError('Dropbox error: ' . $e->getMessage()); + } + } + + /** + * Create file and return it's path or false on failed. + * + * @param string $path parent dir path + * @param string $name new file name + * + * @return string|bool + * @author Naoki Sawada + **/ + protected function _mkfile($path, $name) + { + return $this->_save($this->tmpfile(), $path, $name, []); + } + + /** + * Create symlink. FTP driver does not support symlinks. + * + * @param string $target link target + * @param string $path symlink path + * + * @return bool + * @author Naoki Sawada + **/ + protected function _symlink($target, $path, $name) + { + return false; + } + + /** + * Copy file into another file. + * + * @param string $source source file path + * @param string $targetDir target directory path + * @param string $name new file name + * + * @return bool + * @author Naoki Sawada + **/ + protected function _copy($source, $targetDir, $name) + { + try { + $this->service->copy($source, $this->_db_joinName($targetDir, $name))->getPathLower(); + } catch (DropboxClientException $e) { + return $this->setError('Dropbox error: ' . $e->getMessage()); + } + + return true; + } + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param string $target target dir path + * @param string $name file name + * + * @return string|bool + * @author Naoki Sawada + **/ + protected function _move($source, $targetDir, $name) + { + try { + return $this->service->move($source, $this->_db_joinName($targetDir, $name))->getPathLower(); + } catch (DropboxClientException $e) { + return $this->setError('Dropbox error: ' . $e->getMessage()); + } + } + + /** + * Remove file. + * + * @param string $path file path + * + * @return bool + * @author Naoki Sawada + **/ + protected function _unlink($path) + { + try { + $this->service->delete($path); + + return true; + } catch (DropboxClientException $e) { + return $this->setError('Dropbox error: ' . $e->getMessage()); + } + + return true; + } + + /** + * Remove dir. + * + * @param string $path dir path + * + * @return bool + * @author Naoki Sawada + **/ + protected function _rmdir($path) + { + return $this->_unlink($path); + } + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param string $dir target dir path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Naoki Sawada + **/ + protected function _save($fp, $path, $name, $stat) + { + try { + $info = stream_get_meta_data($fp); + if (empty($info['uri']) || preg_match('#^[a-z0-9.-]+://#', $info['uri'])) { + if ($filepath = $this->getTempFile()) { + $_fp = fopen($filepath, 'wb'); + stream_copy_to_stream($fp, $_fp); + fclose($_fp); + } + } else { + $filepath = $info['uri']; + } + $dropboxFile = new DropboxFile($filepath); + if ($name === '') { + $fullpath = $path; + } else { + $fullpath = $this->_db_joinName($path, $name); + } + + return $this->service->upload($dropboxFile, $fullpath, ['mode' => 'overwrite'])->getPathLower(); + } catch (DropboxClientException $e) { + return $this->setError('Dropbox error: ' . $e->getMessage()); + } + } + + /** + * Get file contents. + * + * @param string $path file path + * + * @return string|false + * @author Naoki Sawada + **/ + protected function _getContents($path) + { + $contents = ''; + + try { + $file = $this->service->download($path); + $contents = $file->getContents(); + } catch (Exception $e) { + return $this->setError('Dropbox error: ' . $e->getMessage()); + } + + return $contents; + } + + /** + * Write a string to a file. + * + * @param string $path file path + * @param string $content new file content + * + * @return bool + * @author Naoki Sawada + **/ + protected function _filePutContents($path, $content) + { + $res = false; + + if ($local = $this->getTempFile($path)) { + if (file_put_contents($local, $content, LOCK_EX) !== false + && ($fp = fopen($local, 'rb'))) { + clearstatcache(); + $name = ''; + $stat = $this->stat($path); + if ($stat) { + // keep real name + $path = $this->_dirname($path); + $name = $stat['name']; + } + $res = $this->_save($fp, $path, $name, []); + fclose($fp); + } + file_exists($local) && unlink($local); + } + + return $res; + } + + /** + * Detect available archivers. + **/ + protected function _checkArchivers() + { + // die('Not yet implemented. (_checkArchivers)'); + return []; + } + + /** + * chmod implementation. + * + * @return bool + **/ + protected function _chmod($path, $mode) + { + return false; + } + + /** + * Unpack archive. + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return true + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + **/ + protected function _unpack($path, $arc) + { + die('Not yet implemented. (_unpack)'); + //return false; + } + + /** + * Recursive symlinks search. + * + * @param string $path file/dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _findSymlinks($path) + { + die('Not yet implemented. (_findSymlinks)'); + } + + /** + * Extract files from archive. + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return true + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _extract($path, $arc) + { + die('Not yet implemented. (_extract)'); + } + + /** + * Create archive and return its path. + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _archive($dir, $files, $name, $arc) + { + die('Not yet implemented. (_archive)'); + } +} // END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeFTP.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeFTP.class.php new file mode 100644 index 0000000..23d5d04 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeFTP.class.php @@ -0,0 +1,1810 @@ + 'localhost', + 'user' => '', + 'pass' => '', + 'port' => 21, + 'mode' => 'passive', + 'ssl' => false, + 'path' => '/', + 'timeout' => 20, + 'owner' => true, + 'tmbPath' => '', + 'tmpPath' => '', + 'separator' => '/', + 'checkSubfolders' => -1, + 'dirMode' => 0755, + 'fileMode' => 0644, + 'rootCssClass' => 'elfinder-navbar-root-ftp', + 'ftpListOption' => '-al', + ); + $this->options = array_merge($this->options, $opts); + $this->options['mimeDetect'] = 'internal'; + } + + /** + * Prepare + * Call from elFinder::netmout() before volume->mount() + * + * @param $options + * + * @return array volume root options + * @author Naoki Sawada + */ + public function netmountPrepare($options) + { + if (!empty($_REQUEST['encoding']) && iconv('UTF-8', $_REQUEST['encoding'], '') !== false) { + $options['encoding'] = $_REQUEST['encoding']; + if (!empty($_REQUEST['locale']) && setlocale(LC_ALL, $_REQUEST['locale'])) { + setlocale(LC_ALL, elFinder::$locale); + $options['locale'] = $_REQUEST['locale']; + } + } + if (!empty($_REQUEST['FTPS'])) { + $options['ssl'] = true; + } + $options['statOwner'] = true; + $options['allowChmodReadOnly'] = true; + $options['acceptedName'] = '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#'; + return $options; + } + + /*********************************************************************/ + /* INIT AND CONFIGURE */ + /*********************************************************************/ + + /** + * Prepare FTP connection + * Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn + * + * @return bool + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + **/ + protected function init() + { + if (!$this->options['host'] + || !$this->options['port']) { + return $this->setError('Required options undefined.'); + } + + if (!$this->options['user']) { + $this->options['user'] = 'anonymous'; + $this->options['pass'] = ''; + } + if (!$this->options['path']) { + $this->options['path'] = '/'; + } + + // make ney mount key + $this->netMountKey = md5(join('-', array('ftp', $this->options['host'], $this->options['port'], $this->options['path'], $this->options['user']))); + + if (!function_exists('ftp_connect')) { + return $this->setError('FTP extension not loaded.'); + } + + // remove protocol from host + $scheme = parse_url($this->options['host'], PHP_URL_SCHEME); + + if ($scheme) { + $this->options['host'] = substr($this->options['host'], strlen($scheme) + 3); + } + + // normalize root path + $this->root = $this->options['path'] = $this->_normpath($this->options['path']); + + if (empty($this->options['alias'])) { + $this->options['alias'] = $this->options['user'] . '@' . $this->options['host']; + if (!empty($this->options['netkey'])) { + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']); + } + } + + $this->rootName = $this->options['alias']; + $this->options['separator'] = '/'; + + if (is_null($this->options['syncChkAsTs'])) { + $this->options['syncChkAsTs'] = true; + } + + if (isset($this->options['ftpListOption'])) { + $this->ftpListOption = $this->options['ftpListOption']; + } + + return $this->needOnline? $this->connect() : true; + + } + + + /** + * Configure after successfull mount. + * + * @return void + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function configure() + { + parent::configure(); + + if (!empty($this->options['tmpPath'])) { + if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'], 0755, true)) && is_writable($this->options['tmpPath'])) { + $this->tmp = $this->options['tmpPath']; + } + } + if (!$this->tmp && ($tmp = elFinder::getStaticVar('commonTempPath'))) { + $this->tmp = $tmp; + } + + // fallback of $this->tmp + if (!$this->tmp && $this->tmbPathWritable) { + $this->tmp = $this->tmbPath; + } + + if (!$this->tmp) { + $this->disabled[] = 'mkfile'; + $this->disabled[] = 'paste'; + $this->disabled[] = 'duplicate'; + $this->disabled[] = 'upload'; + $this->disabled[] = 'edit'; + $this->disabled[] = 'archive'; + $this->disabled[] = 'extract'; + } + + // echo $this->tmp; + + } + + /** + * Connect to ftp server + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function connect() + { + $withSSL = empty($this->options['ssl']) ? '' : ' with SSL'; + if ($withSSL) { + if (!function_exists('ftp_ssl_connect') || !($this->connect = ftp_ssl_connect($this->options['host'], $this->options['port'], $this->options['timeout']))) { + return $this->setError('Unable to connect to FTP server ' . $this->options['host'] . $withSSL); + } + $this->isFTPS = true; + } else { + if (!($this->connect = ftp_connect($this->options['host'], $this->options['port'], $this->options['timeout']))) { + return $this->setError('Unable to connect to FTP server ' . $this->options['host']); + } + } + if (!ftp_login($this->connect, $this->options['user'], $this->options['pass'])) { + $this->umount(); + return $this->setError('Unable to login into ' . $this->options['host'] . $withSSL); + } + + // try switch utf8 mode + if ($this->encoding) { + ftp_raw($this->connect, 'OPTS UTF8 OFF'); + } else { + ftp_raw($this->connect, 'OPTS UTF8 ON'); + } + + $help = ftp_raw($this->connect, 'HELP'); + $this->isPureFtpd = stripos(implode(' ', $help), 'Pure-FTPd') !== false; + + if (!$this->isPureFtpd) { + // switch off extended passive mode - may be usefull for some servers + // this command, for pure-ftpd, doesn't work and takes a timeout in some pure-ftpd versions + ftp_raw($this->connect, 'epsv4 off'); + } + // enter passive mode if required + $pasv = ($this->options['mode'] == 'passive'); + if (!ftp_pasv($this->connect, $pasv)) { + if ($pasv) { + $this->options['mode'] = 'active'; + } + } + + // enter root folder + if (!ftp_chdir($this->connect, $this->root) + || $this->root != ftp_pwd($this->connect)) { + $this->umount(); + return $this->setError('Unable to open root folder.'); + } + + // check for MLST support + $features = ftp_raw($this->connect, 'FEAT'); + if (!is_array($features)) { + $this->umount(); + return $this->setError('Server does not support command FEAT.'); + } + + foreach ($features as $feat) { + if (strpos(trim($feat), 'MLST') === 0) { + $this->MLSTsupprt = true; + break; + } + } + + return true; + } + + /** + * Call ftp_rawlist with option prefix + * + * @param string $path + * + * @return array + */ + protected function ftpRawList($path) + { + if ($this->isPureFtpd) { + $path = str_replace(' ', '\ ', $path); + } + if ($this->ftpListOption) { + $path = $this->ftpListOption . ' ' . $path; + } + $res = ftp_rawlist($this->connect, $path); + if ($res === false) { + $res = array(); + } + return $res; + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /** + * Close opened connection + * + * @return void + * @author Dmitry (dio) Levashov + **/ + public function umount() + { + $this->connect && ftp_close($this->connect); + } + + + /** + * Parse line from ftp_rawlist() output and return file stat (array) + * + * @param string $raw line from ftp_rawlist() output + * @param $base + * @param bool $nameOnly + * + * @return array + * @author Dmitry Levashov + */ + protected function parseRaw($raw, $base, $nameOnly = false) + { + static $now; + static $lastyear; + + if (!$now) { + $now = time(); + $lastyear = date('Y') - 1; + } + + $info = preg_split("/\s+/", $raw, 8); + if (isset($info[7])) { + list($info[7], $info[8]) = explode(' ', $info[7], 2); + } + $stat = array(); + + if (!isset($this->ftpOsUnix)) { + $this->ftpOsUnix = !preg_match('/\d/', substr($info[0], 0, 1)); + } + if (!$this->ftpOsUnix) { + $info = $this->normalizeRawWindows($raw); + } + + if (count($info) < 9 || $info[8] == '.' || $info[8] == '..') { + return false; + } + + $name = $info[8]; + + if (preg_match('|(.+)\-\>(.+)|', $name, $m)) { + $name = trim($m[1]); + // check recursive processing + if ($this->cacheDirTarget && $this->_joinPath($base, $name) !== $this->cacheDirTarget) { + return array(); + } + if (!$nameOnly) { + $target = trim($m[2]); + if (substr($target, 0, 1) !== $this->separator) { + $target = $this->getFullPath($target, $base); + } + $target = $this->_normpath($target); + $stat['name'] = $name; + $stat['target'] = $target; + return $stat; + } + } + + if ($nameOnly) { + return array('name' => $name); + } + + if (is_numeric($info[5]) && !$info[6] && !$info[7]) { + // by normalizeRawWindows() + $stat['ts'] = $info[5]; + } else { + $stat['ts'] = strtotime($info[5] . ' ' . $info[6] . ' ' . $info[7]); + if ($stat['ts'] && $stat['ts'] > $now && strpos($info[7], ':') !== false) { + $stat['ts'] = strtotime($info[5] . ' ' . $info[6] . ' ' . $lastyear . ' ' . $info[7]); + } + if (empty($stat['ts'])) { + $stat['ts'] = strtotime($info[6] . ' ' . $info[5] . ' ' . $info[7]); + if ($stat['ts'] && $stat['ts'] > $now && strpos($info[7], ':') !== false) { + $stat['ts'] = strtotime($info[6] . ' ' . $info[5] . ' ' . $lastyear . ' ' . $info[7]); + } + } + } + + if ($this->options['statOwner']) { + $stat['owner'] = $info[2]; + $stat['group'] = $info[3]; + $stat['perm'] = substr($info[0], 1); + // + // if not exists owner in LS ftp ==> isowner = true + // if is defined as option : 'owner' => true isowner = true + // + // if exist owner in LS ftp and 'owner' => False isowner = result of owner(file) == user(logged with ftp) + // + $stat['isowner'] = isset($stat['owner']) ? ($this->options['owner'] ? true : ($stat['owner'] == $this->options['user'])) : true; + } + + $owner_computed = isset($stat['isowner']) ? $stat['isowner'] : $this->options['owner']; + $perm = $this->parsePermissions($info[0], $owner_computed); + $stat['name'] = $name; + $stat['mime'] = substr(strtolower($info[0]), 0, 1) == 'd' ? 'directory' : $this->mimetype($stat['name'], true); + $stat['size'] = $stat['mime'] == 'directory' ? 0 : $info[4]; + $stat['read'] = $perm['read']; + $stat['write'] = $perm['write']; + + return $stat; + } + + /** + * Normalize MS-DOS style FTP LIST Raw line + * + * @param string $raw line from FTP LIST (MS-DOS style) + * + * @return array + * @author Naoki Sawada + **/ + protected function normalizeRawWindows($raw) + { + $info = array_pad(array(), 9, ''); + $item = preg_replace('#\s+#', ' ', trim($raw), 3); + list($date, $time, $size, $name) = explode(' ', $item, 4); + $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i'; + $dateObj = DateTime::createFromFormat($format, $date . $time); + $info[5] = strtotime($dateObj->format('Y-m-d H:i')); + $info[8] = $name; + if ($size === '') { + $info[4] = 0; + $info[0] = 'drwxr-xr-x'; + } else { + $info[4] = (int)$size; + $info[0] = '-rw-r--r--'; + } + return $info; + } + + /** + * Parse permissions string. Return array(read => true/false, write => true/false) + * + * @param string $perm permissions string 'rwx' + 'rwx' + 'rwx' + * ^ ^ ^ + * | | +-> others + * | +---------> group + * +-----------------> owner + * The isowner parameter is computed by the caller. + * If the owner parameter in the options is true, the user is the actual owner of all objects even if che user used in the ftp Login + * is different from the file owner id. + * If the owner parameter is false to understand if the user is the file owner we compare the ftp user with the file owner id. + * @param Boolean $isowner . Tell if the current user is the owner of the object. + * + * @return array + * @author Dmitry (dio) Levashov + * @author Ugo Vierucci + */ + protected function parsePermissions($perm, $isowner = true) + { + $res = array(); + $parts = array(); + for ($i = 0, $l = strlen($perm); $i < $l; $i++) { + $parts[] = substr($perm, $i, 1); + } + + $read = ($isowner && $parts[1] == 'r') || $parts[4] == 'r' || $parts[7] == 'r'; + + return array( + 'read' => $parts[0] == 'd' ? $read && (($isowner && $parts[3] == 'x') || $parts[6] == 'x' || $parts[9] == 'x') : $read, + 'write' => ($isowner && $parts[2] == 'w') || $parts[5] == 'w' || $parts[8] == 'w' + ); + } + + /** + * Cache dir contents + * + * @param string $path dir path + * + * @return void + * @author Dmitry Levashov + **/ + protected function cacheDir($path) + { + $this->dirsCache[$path] = array(); + $hasDir = false; + + $list = array(); + $encPath = $this->convEncIn($path); + foreach ($this->ftpRawList($encPath) as $raw) { + if (($stat = $this->parseRaw($raw, $encPath))) { + $list[] = $stat; + } + } + $list = $this->convEncOut($list); + $prefix = ($path === $this->separator) ? $this->separator : $path . $this->separator; + $targets = array(); + foreach ($list as $stat) { + $p = $prefix . $stat['name']; + if (isset($stat['target'])) { + // stat later + $targets[$stat['name']] = $stat['target']; + } else { + $stat = $this->updateCache($p, $stat); + if (empty($stat['hidden'])) { + if (!$hasDir && $stat['mime'] === 'directory') { + $hasDir = true; + } + $this->dirsCache[$path][] = $p; + } + } + } + // stat link targets + foreach ($targets as $name => $target) { + $stat = array(); + $stat['name'] = $name; + $p = $prefix . $name; + $cacheDirTarget = $this->cacheDirTarget; + $this->cacheDirTarget = $this->convEncIn($target, true); + if ($tstat = $this->stat($target)) { + $stat['size'] = $tstat['size']; + $stat['alias'] = $target; + $stat['thash'] = $tstat['hash']; + $stat['mime'] = $tstat['mime']; + $stat['read'] = $tstat['read']; + $stat['write'] = $tstat['write']; + + if (isset($tstat['ts'])) { + $stat['ts'] = $tstat['ts']; + } + if (isset($tstat['owner'])) { + $stat['owner'] = $tstat['owner']; + } + if (isset($tstat['group'])) { + $stat['group'] = $tstat['group']; + } + if (isset($tstat['perm'])) { + $stat['perm'] = $tstat['perm']; + } + if (isset($tstat['isowner'])) { + $stat['isowner'] = $tstat['isowner']; + } + } else { + + $stat['mime'] = 'symlink-broken'; + $stat['read'] = false; + $stat['write'] = false; + $stat['size'] = 0; + + } + $this->cacheDirTarget = $cacheDirTarget; + $stat = $this->updateCache($p, $stat); + if (empty($stat['hidden'])) { + if (!$hasDir && $stat['mime'] === 'directory') { + $hasDir = true; + } + $this->dirsCache[$path][] = $p; + } + } + + if (isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'][$path] = $hasDir; + } + } + + /** + * Return ftp transfer mode for file + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function ftpMode($path) + { + return strpos($this->mimetype($path), 'text/') === 0 ? FTP_ASCII : FTP_BINARY; + } + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function _dirname($path) + { + $parts = explode($this->separator, trim($path, $this->separator)); + array_pop($parts); + return $this->separator . join($this->separator, $parts); + } + + /** + * Return file name + * + * @param string $path file path + * + * @return string + * @author Naoki Sawada + **/ + protected function _basename($path) + { + $parts = explode($this->separator, trim($path, $this->separator)); + return array_pop($parts); + } + + /** + * Join dir name and file name and retur full path + * + * @param string $dir + * @param string $name + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _joinPath($dir, $name) + { + return rtrim($dir, $this->separator) . $this->separator . $name; + } + + /** + * Return normalized path, this works the same as os.path.normpath() in Python + * + * @param string $path path + * + * @return string + * @author Troex Nevelin + **/ + protected function _normpath($path) + { + if (empty($path)) { + $path = '.'; + } + // path must be start with / + $path = preg_replace('|^\.\/?|', $this->separator, $path); + $path = preg_replace('/^([^\/])/', "/$1", $path); + + if ($path[0] === $this->separator) { + $initial_slashes = true; + } else { + $initial_slashes = false; + } + + if (($initial_slashes) + && (strpos($path, '//') === 0) + && (strpos($path, '///') === false)) { + $initial_slashes = 2; + } + + $initial_slashes = (int)$initial_slashes; + + $comps = explode($this->separator, $path); + $new_comps = array(); + foreach ($comps as $comp) { + if (in_array($comp, array('', '.'))) { + continue; + } + + if (($comp != '..') + || (!$initial_slashes && !$new_comps) + || ($new_comps && (end($new_comps) == '..'))) { + array_push($new_comps, $comp); + } elseif ($new_comps) { + array_pop($new_comps); + } + } + $comps = $new_comps; + $path = implode($this->separator, $comps); + if ($initial_slashes) { + $path = str_repeat($this->separator, $initial_slashes) . $path; + } + + return $path ? $path : '.'; + } + + /** + * Return file path related to root dir + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _relpath($path) + { + if ($path === $this->root) { + return ''; + } else { + if (strpos($path, $this->root) === 0) { + return ltrim(substr($path, strlen($this->root)), $this->separator); + } else { + // for link + return $path; + } + } + } + + /** + * Convert path related to root dir into real path + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _abspath($path) + { + if ($path === $this->separator) { + return $this->root; + } else { + if ($path[0] === $this->separator) { + // for link + return $path; + } else { + return $this->_joinPath($this->root, $path); + } + } + } + + /** + * Return fake path started from root dir + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _path($path) + { + return $this->rootName . ($path == $this->root ? '' : $this->separator . $this->_relpath($path)); + } + + /** + * Return true if $path is children of $parent + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _inpath($path, $parent) + { + return $path == $parent || strpos($path, rtrim($parent, $this->separator) . $this->separator) === 0; + } + + /***************** file stat ********************/ + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + protected function _stat($path) + { + $outPath = $this->convEncOut($path); + if (isset($this->cache[$outPath])) { + return $this->convEncIn($this->cache[$outPath]); + } else { + $this->convEncIn(); + } + if (!$this->MLSTsupprt) { + if ($path === $this->root) { + $res = array( + 'name' => $this->root, + 'mime' => 'directory', + 'dirs' => -1 + ); + if ($this->needOnline && (($this->ARGS['cmd'] === 'open' && $this->ARGS['target'] === $this->encode($this->root)) || $this->isMyReload())) { + $check = array( + 'ts' => true, + 'dirs' => true, + ); + $ts = 0; + foreach ($this->ftpRawList($path) as $str) { + $info = preg_split('/\s+/', $str, 9); + if ($info[8] === '.') { + $info[8] = 'root'; + if ($stat = $this->parseRaw(join(' ', $info), $path)) { + unset($stat['name']); + $res = array_merge($res, $stat); + if ($res['ts']) { + $ts = 0; + unset($check['ts']); + } + } + } + if ($check && ($stat = $this->parseRaw($str, $path))) { + if (isset($stat['ts']) && !empty($stat['ts'])) { + $ts = max($ts, $stat['ts']); + } + if (isset($stat['dirs']) && $stat['mime'] === 'directory') { + $res['dirs'] = 1; + unset($stat['dirs']); + } + if (!$check) { + break; + } + } + } + if ($ts) { + $res['ts'] = $ts; + } + $this->cache[$outPath] = $res; + } + return $res; + } + + $pPath = $this->_dirname($path); + if ($this->_inPath($pPath, $this->root)) { + $outPPpath = $this->convEncOut($pPath); + if (!isset($this->dirsCache[$outPPpath])) { + $parentSubdirs = null; + if (isset($this->sessionCache['subdirs']) && isset($this->sessionCache['subdirs'][$outPPpath])) { + $parentSubdirs = $this->sessionCache['subdirs'][$outPPpath]; + } + $this->cacheDir($outPPpath); + if ($parentSubdirs) { + $this->sessionCache['subdirs'][$outPPpath] = $parentSubdirs; + } + } + } + + $stat = $this->convEncIn(isset($this->cache[$outPath]) ? $this->cache[$outPath] : array()); + if (!$this->mounted) { + // dispose incomplete cache made by calling `stat` by 'startPath' option + $this->cache = array(); + } + return $stat; + } + $raw = ftp_raw($this->connect, 'MLST ' . $path); + if (is_array($raw) && count($raw) > 1 && substr(trim($raw[0]), 0, 1) == 2) { + $parts = explode(';', trim($raw[1])); + array_pop($parts); + $parts = array_map('strtolower', $parts); + $stat = array(); + $mode = ''; + foreach ($parts as $part) { + + list($key, $val) = explode('=', $part, 2); + + switch ($key) { + case 'type': + if (strpos($val, 'dir') !== false) { + $stat['mime'] = 'directory'; + } else if (strpos($val, 'link') !== false) { + $stat['mime'] = 'symlink'; + break(2); + } else { + $stat['mime'] = $this->mimetype($path); + } + break; + + case 'size': + $stat['size'] = $val; + break; + + case 'modify': + $ts = mktime(intval(substr($val, 8, 2)), intval(substr($val, 10, 2)), intval(substr($val, 12, 2)), intval(substr($val, 4, 2)), intval(substr($val, 6, 2)), substr($val, 0, 4)); + $stat['ts'] = $ts; + break; + + case 'unix.mode': + $mode = strval($val); + break; + + case 'unix.uid': + $stat['owner'] = $val; + break; + + case 'unix.gid': + $stat['group'] = $val; + break; + + case 'perm': + $val = strtolower($val); + $stat['read'] = (int)preg_match('/e|l|r/', $val); + $stat['write'] = (int)preg_match('/w|m|c/', $val); + if (!preg_match('/f|d/', $val)) { + $stat['locked'] = 1; + } + break; + } + } + + if (empty($stat['mime'])) { + return array(); + } + + // do not use MLST to get stat of symlink + if ($stat['mime'] === 'symlink') { + $this->MLSTsupprt = false; + $res = $this->_stat($path); + $this->MLSTsupprt = true; + return $res; + } + + if ($stat['mime'] === 'directory') { + $stat['size'] = 0; + } + + if ($mode) { + $stat['perm'] = ''; + if ($mode[0] === '0') { + $mode = substr($mode, 1); + } + + $perm = array(); + for ($i = 0; $i <= 2; $i++) { + $perm[$i] = array(false, false, false); + $n = isset($mode[$i]) ? $mode[$i] : 0; + + if ($n - 4 >= 0) { + $perm[$i][0] = true; + $n = $n - 4; + $stat['perm'] .= 'r'; + } else { + $stat['perm'] .= '-'; + } + + if ($n - 2 >= 0) { + $perm[$i][1] = true; + $n = $n - 2; + $stat['perm'] .= 'w'; + } else { + $stat['perm'] .= '-'; + } + + if ($n - 1 == 0) { + $perm[$i][2] = true; + $stat['perm'] .= 'x'; + } else { + $stat['perm'] .= '-'; + } + } + + $stat['perm'] = trim($stat['perm']); + // + // if not exists owner in LS ftp ==> isowner = true + // if is defined as option : 'owner' => true isowner = true + // + // if exist owner in LS ftp and 'owner' => False isowner = result of owner(file) == user(logged with ftp) + + $owner_computed = isset($stat['owner']) ? ($this->options['owner'] ? true : ($stat['owner'] == $this->options['user'])) : true; + + $read = ($owner_computed && $perm[0][0]) || $perm[1][0] || $perm[2][0]; + + $stat['read'] = $stat['mime'] == 'directory' ? $read && (($owner_computed && $perm[0][2]) || $perm[1][2] || $perm[2][2]) : $read; + $stat['write'] = ($owner_computed && $perm[0][1]) || $perm[1][1] || $perm[2][1]; + + if ($this->options['statOwner']) { + $stat['isowner'] = $owner_computed; + } else { + unset($stat['owner'], $stat['group'], $stat['perm']); + } + } + + return $stat; + + } + + return array(); + } + + /** + * Return true if path is dir and has at least one childs directory + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _subdirs($path) + { + + foreach ($this->ftpRawList($path) as $str) { + $info = preg_split('/\s+/', $str, 9); + if (!isset($this->ftpOsUnix)) { + $this->ftpOsUnix = !preg_match('/\d/', substr($info[0], 0, 1)); + } + if (!$this->ftpOsUnix) { + $info = $this->normalizeRawWindows($str); + } + $name = isset($info[8]) ? trim($info[8]) : ''; + if ($name && $name !== '.' && $name !== '..' && substr(strtolower($info[0]), 0, 1) === 'd') { + return true; + } + } + return false; + } + + /** + * Return object width and height + * Ususaly used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * + * @return string|false + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function _dimensions($path, $mime) + { + $ret = false; + if ($imgsize = $this->getImageSize($path, $mime)) { + $ret = array('dim' => $imgsize['dimensions']); + if (!empty($imgsize['url'])) { + $ret['url'] = $imgsize['url']; + } + } + return $ret; + } + + /******************** file/dir content *********************/ + + /** + * Return files list in directory. + * + * @param string $path dir path + * + * @return array + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + **/ + protected function _scandir($path) + { + $files = array(); + + foreach ($this->ftpRawList($path) as $str) { + if (($stat = $this->parseRaw($str, $path, true))) { + $files[] = $this->_joinPath($path, $stat['name']); + } + } + + return $files; + } + + /** + * Open file and return file pointer + * + * @param string $path file path + * @param string $mode + * + * @return false|resource + * @throws elFinderAbortException + * @internal param bool $write open file for writing + * @author Dmitry (dio) Levashov + */ + protected function _fopen($path, $mode = 'rb') + { + // try ftp stream wrapper + if ($this->options['mode'] === 'passive' && ini_get('allow_url_fopen')) { + $url = ($this->isFTPS ? 'ftps' : 'ftp') . '://' . $this->options['user'] . ':' . $this->options['pass'] . '@' . $this->options['host'] . ':' . $this->options['port'] . $path; + if (strtolower($mode[0]) === 'w') { + $context = stream_context_create(array('ftp' => array('overwrite' => true))); + $fp = fopen($url, $mode, false, $context); + } else { + $fp = fopen($url, $mode); + } + if ($fp) { + return $fp; + } + } + + if ($this->tmp) { + $local = $this->getTempFile($path); + $fp = fopen($local, 'wb'); + $ret = ftp_nb_fget($this->connect, $fp, $path, FTP_BINARY); + while ($ret === FTP_MOREDATA) { + elFinder::extendTimeLimit(); + $ret = ftp_nb_continue($this->connect); + } + if ($ret === FTP_FINISHED) { + fclose($fp); + $fp = fopen($local, $mode); + return $fp; + } + fclose($fp); + is_file($local) && unlink($local); + } + + return false; + } + + /** + * Close opened file + * + * @param resource $fp file pointer + * @param string $path + * + * @return void + * @author Dmitry (dio) Levashov + */ + protected function _fclose($fp, $path = '') + { + is_resource($fp) && fclose($fp); + if ($path) { + unlink($this->getTempFile($path)); + } + } + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed + * + * @param string $path parent dir path + * @param string $name new directory name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkdir($path, $name) + { + $path = $this->_joinPath($path, $name); + if (ftp_mkdir($this->connect, $path) === false) { + return false; + } + + $this->options['dirMode'] && ftp_chmod($this->connect, $this->options['dirMode'], $path); + return $path; + } + + /** + * Create file and return it's path or false on failed + * + * @param string $path parent dir path + * @param string $name new file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkfile($path, $name) + { + if ($this->tmp) { + $path = $this->_joinPath($path, $name); + $local = $this->getTempFile(); + $res = touch($local) && ftp_put($this->connect, $path, $local, FTP_ASCII); + unlink($local); + return $res ? $path : false; + } + return false; + } + + /** + * Create symlink. FTP driver does not support symlinks. + * + * @param string $target link target + * @param string $path symlink path + * @param string $name + * + * @return bool + * @author Dmitry (dio) Levashov + */ + protected function _symlink($target, $path, $name) + { + return false; + } + + /** + * Copy file into another file + * + * @param string $source source file path + * @param string $targetDir target directory path + * @param string $name new file name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _copy($source, $targetDir, $name) + { + $res = false; + + if ($this->tmp) { + $local = $this->getTempFile(); + $target = $this->_joinPath($targetDir, $name); + + if (ftp_get($this->connect, $local, $source, FTP_BINARY) + && ftp_put($this->connect, $target, $local, $this->ftpMode($target))) { + $res = $target; + } + unlink($local); + } + + return $res; + } + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param $targetDir + * @param string $name file name + * + * @return bool|string + * @internal param string $target target dir path + * @author Dmitry (dio) Levashov + */ + protected function _move($source, $targetDir, $name) + { + $target = $this->_joinPath($targetDir, $name); + return ftp_rename($this->connect, $source, $target) ? $target : false; + } + + /** + * Remove file + * + * @param string $path file path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _unlink($path) + { + return ftp_delete($this->connect, $path); + } + + /** + * Remove dir + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _rmdir($path) + { + return ftp_rmdir($this->connect, $path); + } + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param string $dir target dir path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Dmitry (dio) Levashov + **/ + protected function _save($fp, $dir, $name, $stat) + { + $path = $this->_joinPath($dir, $name); + return ftp_fput($this->connect, $path, $fp, $this->ftpMode($path)) + ? $path + : false; + } + + /** + * Get file contents + * + * @param string $path file path + * + * @return string|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function _getContents($path) + { + $contents = ''; + if (($fp = $this->_fopen($path))) { + while (!feof($fp)) { + $contents .= fread($fp, 8192); + } + $this->_fclose($fp, $path); + return $contents; + } + return false; + } + + /** + * Write a string to a file + * + * @param string $path file path + * @param string $content new file content + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _filePutContents($path, $content) + { + $res = false; + + if ($this->tmp) { + $local = $this->getTempFile(); + + if (file_put_contents($local, $content, LOCK_EX) !== false + && ($fp = fopen($local, 'rb'))) { + $file = $this->stat($this->convEncOut($path, false)); + if (!empty($file['thash'])) { + $path = $this->decode($file['thash']); + } + clearstatcache(); + $res = ftp_fput($this->connect, $path, $fp, $this->ftpMode($path)); + fclose($fp); + } + file_exists($local) && unlink($local); + } + + return $res; + } + + /** + * Detect available archivers + * + * @return void + * @throws elFinderAbortException + */ + protected function _checkArchivers() + { + $this->archivers = $this->getArchivers(); + return; + } + + /** + * chmod availability + * + * @param string $path + * @param string $mode + * + * @return bool + */ + protected function _chmod($path, $mode) + { + $modeOct = is_string($mode) ? octdec($mode) : octdec(sprintf("%04o", $mode)); + return ftp_chmod($this->connect, $modeOct, $path); + } + + /** + * Extract files from archive + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return true + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + */ + protected function _extract($path, $arc) + { + $dir = $this->tempDir(); + if (!$dir) { + return false; + } + + $basename = $this->_basename($path); + $localPath = $dir . DIRECTORY_SEPARATOR . $basename; + + if (!ftp_get($this->connect, $localPath, $path, FTP_BINARY)) { + //cleanup + $this->rmdirRecursive($dir); + return false; + } + + $this->unpackArchive($localPath, $arc); + + $this->archiveSize = 0; + + // find symlinks and check extracted items + $checkRes = $this->checkExtractItems($dir); + if ($checkRes['symlinks']) { + $this->rmdirRecursive($dir); + return $this->setError(array_merge($this->error, array(elFinder::ERROR_ARC_SYMLINKS))); + } + $this->archiveSize = $checkRes['totalSize']; + if ($checkRes['rmNames']) { + foreach ($checkRes['rmNames'] as $name) { + $this->addError(elFinder::ERROR_SAVE, $name); + } + } + + $filesToProcess = self::listFilesInDirectory($dir, true); + + // no files - extract error ? + if (empty($filesToProcess)) { + $this->rmdirRecursive($dir); + return false; + } + + // check max files size + if ($this->options['maxArcFilesSize'] > 0 && $this->options['maxArcFilesSize'] < $this->archiveSize) { + $this->rmdirRecursive($dir); + return $this->setError(elFinder::ERROR_ARC_MAXSIZE); + } + + $extractTo = $this->extractToNewdir; // 'auto', ture or false + + // archive contains one item - extract in archive dir + $name = ''; + $src = $dir . DIRECTORY_SEPARATOR . $filesToProcess[0]; + if (($extractTo === 'auto' || !$extractTo) && count($filesToProcess) === 1 && is_file($src)) { + $name = $filesToProcess[0]; + } else if ($extractTo === 'auto' || $extractTo) { + // for several files - create new directory + // create unique name for directory + $src = $dir; + $splits = elFinder::splitFileExtention(basename($path)); + $name = $splits[0]; + $test = $this->_joinPath(dirname($path), $name); + if ($this->stat($test)) { + $name = $this->uniqueName(dirname($path), $name, '-', false); + } + } + + if ($name !== '' && is_file($src)) { + $result = $this->_joinPath(dirname($path), $name); + + if (!ftp_put($this->connect, $result, $src, FTP_BINARY)) { + $this->rmdirRecursive($dir); + return false; + } + } else { + $dstDir = $this->_dirname($path); + $result = array(); + if (is_dir($src) && $name) { + $target = $this->_joinPath($dstDir, $name); + $_stat = $this->_stat($target); + if ($_stat) { + if (!$this->options['copyJoin']) { + if ($_stat['mime'] === 'directory') { + $this->delTree($target); + } else { + $this->_unlink($target); + } + $_stat = false; + } else { + $dstDir = $target; + } + } + if (!$_stat && (!$dstDir = $this->_mkdir($dstDir, $name))) { + $this->rmdirRecursive($dir); + return false; + } + $result[] = $dstDir; + } + foreach ($filesToProcess as $name) { + $name = rtrim($name, DIRECTORY_SEPARATOR); + $src = $dir . DIRECTORY_SEPARATOR . $name; + if (is_dir($src)) { + $p = dirname($name); + if ($p === '.') { + $p = ''; + } + $name = basename($name); + $target = $this->_joinPath($this->_joinPath($dstDir, $p), $name); + $_stat = $this->_stat($target); + if ($_stat) { + if (!$this->options['copyJoin']) { + if ($_stat['mime'] === 'directory') { + $this->delTree($target); + } else { + $this->_unlink($target); + } + $_stat = false; + } + } + if (!$_stat && (!$target = $this->_mkdir($this->_joinPath($dstDir, $p), $name))) { + $this->rmdirRecursive($dir); + return false; + } + } else { + $target = $this->_joinPath($dstDir, $name); + if (!ftp_put($this->connect, $target, $src, FTP_BINARY)) { + $this->rmdirRecursive($dir); + return false; + } + } + $result[] = $target; + } + if (!$result) { + $this->rmdirRecursive($dir); + return false; + } + } + + is_dir($dir) && $this->rmdirRecursive($dir); + + $this->clearcache(); + return $result ? $result : false; + } + + /** + * Create archive and return its path + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + */ + protected function _archive($dir, $files, $name, $arc) + { + // get current directory + $cwd = getcwd(); + + $tmpDir = $this->tempDir(); + if (!$tmpDir) { + return false; + } + + //download data + if (!$this->ftp_download_files($dir, $files, $tmpDir)) { + //cleanup + $this->rmdirRecursive($tmpDir); + return false; + } + + $remoteArchiveFile = false; + if ($path = $this->makeArchive($tmpDir, $files, $name, $arc)) { + $remoteArchiveFile = $this->_joinPath($dir, $name); + if (!ftp_put($this->connect, $remoteArchiveFile, $path, FTP_BINARY)) { + $remoteArchiveFile = false; + } + } + + //cleanup + if (!$this->rmdirRecursive($tmpDir)) { + return false; + } + + return $remoteArchiveFile; + } + + /** + * Create writable temporary directory and return path to it. + * + * @return string path to the new temporary directory or false in case of error. + */ + private function tempDir() + { + $tempPath = tempnam($this->tmp, 'elFinder'); + if (!$tempPath) { + $this->setError(elFinder::ERROR_CREATING_TEMP_DIR, $this->tmp); + return false; + } + $success = unlink($tempPath); + if (!$success) { + $this->setError(elFinder::ERROR_CREATING_TEMP_DIR, $this->tmp); + return false; + } + $success = mkdir($tempPath, 0700, true); + if (!$success) { + $this->setError(elFinder::ERROR_CREATING_TEMP_DIR, $this->tmp); + return false; + } + return $tempPath; + } + + /** + * Gets an array of absolute remote FTP paths of files and + * folders in $remote_directory omitting symbolic links. + * + * @param $remote_directory string remote FTP path to scan for file and folders recursively + * @param $targets array Array of target item. `null` is to get all of items + * + * @return array of elements each of which is an array of two elements: + *
      + *
    • $item['path'] - absolute remote FTP path
    • + *
    • $item['type'] - either 'f' for file or 'd' for directory
    • + *
    + */ + protected function ftp_scan_dir($remote_directory, $targets = null) + { + $buff = $this->ftpRawList($remote_directory); + $items = array(); + if ($targets && is_array($targets)) { + $targets = array_flip($targets); + } else { + $targets = false; + } + foreach ($buff as $str) { + $info = preg_split("/\s+/", $str, 9); + if (!isset($this->ftpOsUnix)) { + $this->ftpOsUnix = !preg_match('/\d/', substr($info[0], 0, 1)); + } + if (!$this->ftpOsUnix) { + $info = $this->normalizeRawWindows($str); + } + $type = substr($info[0], 0, 1); + $name = trim($info[8]); + if ($name !== '.' && $name !== '..' && (!$targets || isset($targets[$name]))) { + switch ($type) { + case 'l' : //omit symbolic links + case 'd' : + $remote_file_path = $this->_joinPath($remote_directory, $name); + $item = array(); + $item['path'] = $remote_file_path; + $item['type'] = 'd'; // normal file + $items[] = $item; + $items = array_merge($items, $this->ftp_scan_dir($remote_file_path)); + break; + default: + $remote_file_path = $this->_joinPath($remote_directory, $name); + $item = array(); + $item['path'] = $remote_file_path; + $item['type'] = 'f'; // normal file + $items[] = $item; + } + } + } + return $items; + } + + /** + * Downloads specified files from remote directory + * if there is a directory among files it is downloaded recursively (omitting symbolic links). + * + * @param $remote_directory string remote FTP path to a source directory to download from. + * @param array $files list of files to download from remote directory. + * @param $dest_local_directory string destination folder to store downloaded files. + * + * @return bool true on success and false on failure. + */ + private function ftp_download_files($remote_directory, array $files, $dest_local_directory) + { + $contents = $this->ftp_scan_dir($remote_directory, $files); + if (!isset($contents)) { + $this->setError(elFinder::ERROR_FTP_DOWNLOAD_FILE, $remote_directory); + return false; + } + $remoteDirLen = strlen($remote_directory); + foreach ($contents as $item) { + $relative_path = substr($item['path'], $remoteDirLen); + $local_path = $dest_local_directory . DIRECTORY_SEPARATOR . $relative_path; + switch ($item['type']) { + case 'd': + $success = mkdir($local_path); + break; + case 'f': + $success = ftp_get($this->connect, $local_path, $item['path'], FTP_BINARY); + break; + default: + $success = true; + } + if (!$success) { + $this->setError(elFinder::ERROR_FTP_DOWNLOAD_FILE, $remote_directory); + return false; + } + } + return true; + } + + /** + * Delete local directory recursively. + * + * @param $dirPath string to directory to be erased. + * + * @return bool true on success and false on failure. + * @throws Exception + */ + private function deleteDir($dirPath) + { + if (!is_dir($dirPath)) { + $success = unlink($dirPath); + } else { + $success = true; + foreach (array_reverse(elFinderVolumeFTP::listFilesInDirectory($dirPath, false)) as $path) { + $path = $dirPath . DIRECTORY_SEPARATOR . $path; + if (is_link($path)) { + unlink($path); + } else if (is_dir($path)) { + $success = rmdir($path); + } else { + $success = unlink($path); + } + if (!$success) { + break; + } + } + if ($success) { + $success = rmdir($dirPath); + } + } + if (!$success) { + $this->setError(elFinder::ERROR_RM, $dirPath); + return false; + } + return $success; + } + + /** + * Returns array of strings containing all files and folders in the specified local directory. + * + * @param $dir + * @param $omitSymlinks + * @param string $prefix + * + * @return array array of files and folders names relative to the $path + * or an empty array if the directory $path is empty, + *
    + * false if $path is not a directory or does not exist. + * @throws Exception + * @internal param string $path path to directory to scan. + */ + private static function listFilesInDirectory($dir, $omitSymlinks, $prefix = '') + { + if (!is_dir($dir)) { + return false; + } + $excludes = array(".", ".."); + $result = array(); + $files = self::localScandir($dir); + if (!$files) { + return array(); + } + foreach ($files as $file) { + if (!in_array($file, $excludes)) { + $path = $dir . DIRECTORY_SEPARATOR . $file; + if (is_link($path)) { + if ($omitSymlinks) { + continue; + } else { + $result[] = $prefix . $file; + } + } else if (is_dir($path)) { + $result[] = $prefix . $file . DIRECTORY_SEPARATOR; + $subs = elFinderVolumeFTP::listFilesInDirectory($path, $omitSymlinks, $prefix . $file . DIRECTORY_SEPARATOR); + if ($subs) { + $result = array_merge($result, $subs); + } + + } else { + $result[] = $prefix . $file; + } + } + } + return $result; + } + +} // END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeGoogleDrive.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeGoogleDrive.class.php new file mode 100644 index 0000000..d04a4fd --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeGoogleDrive.class.php @@ -0,0 +1,2163 @@ + name of each items. + * + * @var array + */ + protected $names = []; + + /** + * MIME tyoe of directory. + * + * @var string + */ + const DIRMIME = 'application/vnd.google-apps.folder'; + + /** + * Fetch fields for list. + * + * @var string + */ + const FETCHFIELDS_LIST = 'files(id,name,mimeType,modifiedTime,parents,permissions,size,imageMediaMetadata(height,width),thumbnailLink,webContentLink,webViewLink),nextPageToken'; + + /** + * Fetch fields for get. + * + * @var string + */ + const FETCHFIELDS_GET = 'id,name,mimeType,modifiedTime,parents,permissions,size,imageMediaMetadata(height,width),thumbnailLink,webContentLink,webViewLink'; + + /** + * Directory for tmp files + * If not set driver will try to use tmbDir as tmpDir. + * + * @var string + **/ + protected $tmp = ''; + + /** + * Net mount key. + * + * @var string + **/ + public $netMountKey = ''; + + /** + * Current token expires + * + * @var integer + **/ + private $expires; + + /** + * Constructor + * Extend options with required fields. + * + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + **/ + public function __construct() + { + $opts = [ + 'client_id' => '', + 'client_secret' => '', + 'access_token' => [], + 'refresh_token' => '', + 'serviceAccountConfigFile' => '', + 'root' => 'My Drive', + 'gdAlias' => '%s@GDrive', + 'googleApiClient' => '', + 'path' => '/', + 'tmbPath' => '', + 'separator' => '/', + 'useGoogleTmb' => true, + 'acceptedName' => '#.#', + 'rootCssClass' => 'elfinder-navbar-root-googledrive', + 'publishPermission' => [ + 'type' => 'anyone', + 'role' => 'reader', + 'withLink' => true, + ], + 'appsExportMap' => [ + 'application/vnd.google-apps.document' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.google-apps.spreadsheet' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.google-apps.drawing' => 'application/pdf', + 'application/vnd.google-apps.presentation' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.google-apps.script' => 'application/vnd.google-apps.script+json', + 'default' => 'application/pdf', + ], + ]; + $this->options = array_merge($this->options, $opts); + $this->options['mimeDetect'] = 'internal'; + } + + /*********************************************************************/ + /* ORIGINAL FUNCTIONS */ + /*********************************************************************/ + + /** + * Get Parent ID, Item ID, Parent Path as an array from path. + * + * @param string $path + * + * @return array + */ + protected function _gd_splitPath($path) + { + $path = trim($path, '/'); + $pid = ''; + if ($path === '') { + $id = 'root'; + $parent = ''; + } else { + $path = str_replace('\\/', chr(0), $path); + $paths = explode('/', $path); + $id = array_pop($paths); + $id = str_replace(chr(0), '/', $id); + if ($paths) { + $parent = '/' . implode('/', $paths); + $pid = array_pop($paths); + } else { + $rootid = ($this->root === '/') ? 'root' : trim($this->root, '/'); + if ($id === $rootid) { + $parent = ''; + } else { + $parent = $this->root; + $pid = $rootid; + } + } + } + + return array($pid, $id, $parent); + } + + /** + * Drive query and fetchAll. + * + * @param string $sql + * + * @return bool|array + */ + private function _gd_query($opts) + { + $result = []; + $pageToken = null; + $parameters = [ + 'fields' => self::FETCHFIELDS_LIST, + 'pageSize' => 1000, + 'spaces' => 'drive', + ]; + + if (is_array($opts)) { + $parameters = array_merge($parameters, $opts); + } + do { + try { + if ($pageToken) { + $parameters['pageToken'] = $pageToken; + } + $files = $this->service->files->listFiles($parameters); + + $result = array_merge($result, $files->getFiles()); + $pageToken = $files->getNextPageToken(); + } catch (Exception $e) { + $pageToken = null; + } + } while ($pageToken); + + return $result; + } + + /** + * Get dat(googledrive metadata) from GoogleDrive. + * + * @param string $path + * + * @return array googledrive metadata + */ + private function _gd_getFile($path, $fields = '') + { + list(, $itemId) = $this->_gd_splitPath($path); + if (!$fields) { + $fields = self::FETCHFIELDS_GET; + } + try { + $file = $this->service->files->get($itemId, ['fields' => $fields]); + if ($file instanceof Google_Service_Drive_DriveFile) { + return $file; + } else { + return []; + } + } catch (Exception $e) { + return []; + } + } + + /** + * Parse line from googledrive metadata output and return file stat (array). + * + * @param array $raw line from ftp_rawlist() output + * + * @return array + * @author Dmitry Levashov + **/ + protected function _gd_parseRaw($raw) + { + $stat = []; + + $stat['iid'] = isset($raw['id']) ? $raw['id'] : 'root'; + $stat['name'] = isset($raw['name']) ? $raw['name'] : ''; + if (isset($raw['modifiedTime'])) { + $stat['ts'] = strtotime($raw['modifiedTime']); + } + + if ($raw['mimeType'] === self::DIRMIME) { + $stat['mime'] = 'directory'; + $stat['size'] = 0; + } else { + $stat['mime'] = $raw['mimeType'] == 'image/bmp' ? 'image/x-ms-bmp' : $raw['mimeType']; + $stat['size'] = (int)$raw['size']; + if ($size = $raw->getImageMediaMetadata()) { + $stat['width'] = $size['width']; + $stat['height'] = $size['height']; + } + + $published = $this->_gd_isPublished($raw); + + if ($this->options['useGoogleTmb']) { + if (isset($raw['thumbnailLink'])) { + if ($published) { + $stat['tmb'] = 'drive.google.com/thumbnail?authuser=0&sz=s' . $this->options['tmbSize'] . '&id=' . $raw['id']; + } else { + $stat['tmb'] = substr($raw['thumbnailLink'], 8); // remove "https://" + } + } else { + $stat['tmb'] = ''; + } + } + + if ($published) { + $stat['url'] = $this->_gd_getLink($raw); + } elseif (!$this->disabledGetUrl) { + $stat['url'] = '1'; + } + } + + return $stat; + } + + /** + * Get dat(googledrive metadata) from GoogleDrive. + * + * @param string $path + * + * @return array googledrive metadata + */ + private function _gd_getNameByPath($path) + { + list(, $itemId) = $this->_gd_splitPath($path); + if (!$this->names) { + $this->_gd_getDirectoryData(); + } + + return isset($this->names[$itemId]) ? $this->names[$itemId] : ''; + } + + /** + * Make cache of $parents, $names and $directories. + * + * @param bool $usecache + */ + protected function _gd_getDirectoryData($usecache = true) + { + if ($usecache) { + $cache = $this->session->get($this->id . $this->netMountKey, []); + if ($cache) { + $this->parents = $cache['parents']; + $this->names = $cache['names']; + $this->directories = $cache['directories']; + + return; + } + } + + $root = ''; + if ($this->root === '/') { + // get root id + if ($res = $this->_gd_getFile('/', 'id')) { + $root = $res->getId(); + } + } + + $data = []; + $opts = [ + 'fields' => 'files(id, name, parents)', + 'q' => sprintf('trashed=false and mimeType="%s"', self::DIRMIME), + ]; + $res = $this->_gd_query($opts); + foreach ($res as $raw) { + if ($parents = $raw->getParents()) { + $id = $raw->getId(); + $this->parents[$id] = $parents; + $this->names[$id] = $raw->getName(); + foreach ($parents as $p) { + if (isset($data[$p])) { + $data[$p][] = $id; + } else { + $data[$p] = [$id]; + } + } + } + } + if ($root && isset($data[$root])) { + $data['root'] = $data[$root]; + } + $this->directories = $data; + $this->session->set($this->id . $this->netMountKey, [ + 'parents' => $this->parents, + 'names' => $this->names, + 'directories' => $this->directories, + ]); + } + + /** + * Get descendants directories. + * + * @param string $itemId + * + * @return array + */ + protected function _gd_getDirectories($itemId) + { + $ret = []; + if ($this->directories === null) { + $this->_gd_getDirectoryData(); + } + $data = $this->directories; + if (isset($data[$itemId])) { + $ret = $data[$itemId]; + foreach ($data[$itemId] as $cid) { + $ret = array_merge($ret, $this->_gd_getDirectories($cid)); + } + } + + return $ret; + } + + /** + * Get ID based path from item ID. + * + * @param string $id + * + * @return array + */ + protected function _gd_getMountPaths($id) + { + $root = false; + if ($this->directories === null) { + $this->_gd_getDirectoryData(); + } + list($pid) = explode('/', $id, 2); + $path = $id; + if ('/' . $pid === $this->root) { + $root = true; + } elseif (!isset($this->parents[$pid])) { + $root = true; + $path = ltrim(substr($path, strlen($pid)), '/'); + } + $res = []; + if ($root) { + if ($this->root === '/' || strpos('/' . $path, $this->root) === 0) { + $res = [(strpos($path, '/') === false) ? '/' : ('/' . $path)]; + } + } else { + foreach ($this->parents[$pid] as $p) { + $_p = $p . '/' . $path; + $res = array_merge($res, $this->_gd_getMountPaths($_p)); + } + } + + return $res; + } + + /** + * Return is published. + * + * @param object $file + * + * @return bool + */ + protected function _gd_isPublished($file) + { + $res = false; + $pType = $this->options['publishPermission']['type']; + $pRole = $this->options['publishPermission']['role']; + if ($permissions = $file->getPermissions()) { + foreach ($permissions as $permission) { + if ($permission->type === $pType && $permission->role === $pRole) { + $res = true; + break; + } + } + } + + return $res; + } + + /** + * return item URL link. + * + * @param object $file + * + * @return string + */ + protected function _gd_getLink($file) + { + if (strpos($file->mimeType, 'application/vnd.google-apps.') !== 0) { + if ($url = $file->getWebContentLink()) { + return str_replace('export=download', 'export=media', $url); + } + } + if ($url = $file->getWebViewLink()) { + return $url; + } + + return ''; + } + + /** + * Get download url. + * + * @param Google_Service_Drive_DriveFile $file + * + * @return string|false + */ + protected function _gd_getDownloadUrl($file) + { + if (strpos($file->mimeType, 'application/vnd.google-apps.') !== 0) { + return 'https://www.googleapis.com/drive/v3/files/' . $file->getId() . '?alt=media'; + } else { + $mimeMap = $this->options['appsExportMap']; + if (isset($mimeMap[$file->getMimeType()])) { + $mime = $mimeMap[$file->getMimeType()]; + } else { + $mime = $mimeMap['default']; + } + $mime = rawurlencode($mime); + + return 'https://www.googleapis.com/drive/v3/files/' . $file->getId() . '/export?mimeType=' . $mime; + } + + return false; + } + + /** + * Get thumbnail from GoogleDrive.com. + * + * @param string $path + * + * @return string | boolean + */ + protected function _gd_getThumbnail($path) + { + list(, $itemId) = $this->_gd_splitPath($path); + + try { + $contents = $this->service->files->get($itemId, [ + 'alt' => 'media', + ]); + $contents = $contents->getBody()->detach(); + rewind($contents); + + return $contents; + } catch (Exception $e) { + return false; + } + } + + /** + * Publish permissions specified path item. + * + * @param string $path + * + * @return bool + */ + protected function _gd_publish($path) + { + if ($file = $this->_gd_getFile($path)) { + if ($this->_gd_isPublished($file)) { + return true; + } + try { + if ($this->service->permissions->create($file->getId(), new \Google_Service_Drive_Permission($this->options['publishPermission']))) { + return true; + } + } catch (Exception $e) { + return false; + } + } + + return false; + } + + /** + * unPublish permissions specified path. + * + * @param string $path + * + * @return bool + */ + protected function _gd_unPublish($path) + { + if ($file = $this->_gd_getFile($path)) { + if (!$this->_gd_isPublished($file)) { + return true; + } + $permissions = $file->getPermissions(); + $pType = $this->options['publishPermission']['type']; + $pRole = $this->options['publishPermission']['role']; + try { + foreach ($permissions as $permission) { + if ($permission->type === $pType && $permission->role === $pRole) { + $this->service->permissions->delete($file->getId(), $permission->getId()); + + return true; + break; + } + } + } catch (Exception $e) { + return false; + } + } + + return false; + } + + /** + * Read file chunk. + * + * @param resource $handle + * @param int $chunkSize + * + * @return string + */ + protected function _gd_readFileChunk($handle, $chunkSize) + { + $byteCount = 0; + $giantChunk = ''; + while (!feof($handle)) { + // fread will never return more than 8192 bytes if the stream is read buffered and it does not represent a plain file + $chunk = fread($handle, 8192); + $byteCount += strlen($chunk); + $giantChunk .= $chunk; + if ($byteCount >= $chunkSize) { + return $giantChunk; + } + } + + return $giantChunk; + } + + /*********************************************************************/ + /* EXTENDED FUNCTIONS */ + /*********************************************************************/ + + /** + * Prepare + * Call from elFinder::netmout() before volume->mount(). + * + * @return array + * @author Naoki Sawada + * @author Raja Sharma updating for GoogleDrive + **/ + public function netmountPrepare($options) + { + if (empty($options['client_id']) && defined('ELFINDER_GOOGLEDRIVE_CLIENTID')) { + $options['client_id'] = ELFINDER_GOOGLEDRIVE_CLIENTID; + } + if (empty($options['client_secret']) && defined('ELFINDER_GOOGLEDRIVE_CLIENTSECRET')) { + $options['client_secret'] = ELFINDER_GOOGLEDRIVE_CLIENTSECRET; + } + if (empty($options['googleApiClient']) && defined('ELFINDER_GOOGLEDRIVE_GOOGLEAPICLIENT')) { + $options['googleApiClient'] = ELFINDER_GOOGLEDRIVE_GOOGLEAPICLIENT; + include_once $options['googleApiClient']; + } + + if (!isset($options['pass'])) { + $options['pass'] = ''; + } + + try { + $client = new \Google_Client(); + $client->setClientId($options['client_id']); + $client->setClientSecret($options['client_secret']); + + if ($options['pass'] === 'reauth') { + $options['pass'] = ''; + $this->session->set('GoogleDriveAuthParams', [])->set('GoogleDriveTokens', []); + } elseif ($options['pass'] === 'googledrive') { + $options['pass'] = ''; + } + + $options = array_merge($this->session->get('GoogleDriveAuthParams', []), $options); + + if (!isset($options['access_token'])) { + $options['access_token'] = $this->session->get('GoogleDriveTokens', []); + $this->session->remove('GoogleDriveTokens'); + } + $aToken = $options['access_token']; + + $rootObj = $service = null; + if ($aToken) { + try { + $client->setAccessToken($aToken); + if ($client->isAccessTokenExpired()) { + $aToken = array_merge($aToken, $client->fetchAccessTokenWithRefreshToken()); + $client->setAccessToken($aToken); + } + $service = new \Google_Service_Drive($client); + $rootObj = $service->files->get('root'); + + $options['access_token'] = $aToken; + $this->session->set('GoogleDriveAuthParams', $options); + } catch (Exception $e) { + $aToken = []; + $options['access_token'] = []; + if ($options['user'] !== 'init') { + $this->session->set('GoogleDriveAuthParams', $options); + + return ['exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE]; + } + } + } + + $itpCare = isset($options['code']); + $code = $itpCare? $options['code'] : (isset($_GET['code'])? $_GET['code'] : ''); + if ($code || (isset($options['user']) && $options['user'] === 'init')) { + if (empty($options['url'])) { + $options['url'] = elFinder::getConnectorUrl(); + } + + if (isset($options['id'])) { + $callback = $options['url'] + . (strpos($options['url'], '?') !== false? '&' : '?') . 'cmd=netmount&protocol=googledrive&host=' . ($options['id'] === 'elfinder'? '1' : $options['id']); + $client->setRedirectUri($callback); + } + + if (!$aToken && empty($code)) { + $client->setScopes([Google_Service_Drive::DRIVE]); + if (!empty($options['offline'])) { + $client->setApprovalPrompt('force'); + $client->setAccessType('offline'); + } + $url = $client->createAuthUrl(); + + $html = ''; + $html .= ''; + if (empty($options['pass']) && $options['host'] !== '1') { + $options['pass'] = 'return'; + $this->session->set('GoogleDriveAuthParams', $options); + + return ['exit' => true, 'body' => $html]; + } else { + $out = [ + 'node' => $options['id'], + 'json' => '{"protocol": "googledrive", "mode": "makebtn", "body" : "' . str_replace($html, '"', '\\"') . '", "error" : "' . elFinder::ERROR_ACCESS_DENIED . '"}', + 'bind' => 'netmount', + ]; + + return ['exit' => 'callback', 'out' => $out]; + } + } else { + if ($code) { + if (!empty($options['id'])) { + $aToken = $client->fetchAccessTokenWithAuthCode($code); + $options['access_token'] = $aToken; + unset($options['code']); + $this->session->set('GoogleDriveTokens', $aToken)->set('GoogleDriveAuthParams', $options); + $out = [ + 'node' => $options['id'], + 'json' => '{"protocol": "googledrive", "mode": "done", "reset": 1}', + 'bind' => 'netmount', + ]; + } else { + $nodeid = ($_GET['host'] === '1')? 'elfinder' : $_GET['host']; + $out = array( + 'node' => $nodeid, + 'json' => json_encode(array( + 'protocol' => 'googledrive', + 'host' => $nodeid, + 'mode' => 'redirect', + 'options' => array( + 'id' => $nodeid, + 'code'=> $code + ) + )), + 'bind' => 'netmount' + ); + } + if (!$itpCare) { + return array('exit' => 'callback', 'out' => $out); + } else { + return array('exit' => true, 'body' => $out['json']); + } + } + $path = $options['path']; + if ($path === '/') { + $path = 'root'; + } + $folders = []; + foreach ($service->files->listFiles([ + 'pageSize' => 1000, + 'q' => sprintf('trashed = false and "%s" in parents and mimeType = "application/vnd.google-apps.folder"', $path), + ]) as $f) { + $folders[$f->getId()] = $f->getName(); + } + natcasesort($folders); + + if ($options['pass'] === 'folders') { + return ['exit' => true, 'folders' => $folders]; + } + + $folders = ['root' => $rootObj->getName()] + $folders; + $folders = json_encode($folders); + $expires = empty($aToken['refresh_token']) ? $aToken['created'] + $aToken['expires_in'] - 30 : 0; + $mnt2res = empty($aToken['refresh_token']) ? '' : ', "mnt2res": 1'; + $json = '{"protocol": "googledrive", "mode": "done", "folders": ' . $folders . ', "expires": ' . $expires . $mnt2res . '}'; + $options['pass'] = 'return'; + $html = 'Google.com'; + $html .= ''; + $this->session->set('GoogleDriveAuthParams', $options); + + return ['exit' => true, 'body' => $html]; + } + } + } catch (Exception $e) { + $this->session->remove('GoogleDriveAuthParams')->remove('GoogleDriveTokens'); + if (empty($options['pass'])) { + return ['exit' => true, 'body' => '{msg:' . elFinder::ERROR_ACCESS_DENIED . '}' . ' ' . $e->getMessage()]; + } else { + return ['exit' => true, 'error' => [elFinder::ERROR_ACCESS_DENIED, $e->getMessage()]]; + } + } + + if (!$aToken) { + return ['exit' => true, 'error' => elFinder::ERROR_REAUTH_REQUIRE]; + } + + if ($options['path'] === '/') { + $options['path'] = 'root'; + } + + try { + $file = $service->files->get($options['path']); + $options['alias'] = sprintf($this->options['gdAlias'], $file->getName()); + } catch (Google_Service_Exception $e) { + $err = json_decode($e->getMessage(), true); + if (isset($err['error']) && $err['error']['code'] == 404) { + return ['exit' => true, 'error' => [elFinder::ERROR_TRGDIR_NOT_FOUND, $options['path']]]; + } else { + return ['exit' => true, 'error' => $e->getMessage()]; + } + } catch (Exception $e) { + return ['exit' => true, 'error' => $e->getMessage()]; + } + + foreach (['host', 'user', 'pass', 'id', 'offline'] as $key) { + unset($options[$key]); + } + + return $options; + } + + /** + * process of on netunmount + * Drop `googledrive` & rm thumbs. + * + * @param $netVolumes + * @param $key + * + * @return bool + */ + public function netunmount($netVolumes, $key) + { + if (!$this->options['useGoogleTmb']) { + if ($tmbs = glob(rtrim($this->options['tmbPath'], '\\/') . DIRECTORY_SEPARATOR . $this->netMountKey . '*.png')) { + foreach ($tmbs as $file) { + unlink($file); + } + } + } + $this->session->remove($this->id . $this->netMountKey); + + return true; + } + + /** + * Return fileinfo based on filename + * For item ID based path file system + * Please override if needed on each drivers. + * + * @param string $path file cache + * + * @return array + */ + protected function isNameExists($path) + { + list($parentId, $name) = $this->_gd_splitPath($path); + $opts = [ + 'q' => sprintf('trashed=false and "%s" in parents and name="%s"', $parentId, $name), + 'fields' => self::FETCHFIELDS_LIST, + ]; + $srcFile = $this->_gd_query($opts); + + return empty($srcFile) ? false : $this->_gd_parseRaw($srcFile[0]); + } + + /*********************************************************************/ + /* INIT AND CONFIGURE */ + /*********************************************************************/ + + /** + * Prepare FTP connection + * Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn. + * + * @return bool + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + **/ + protected function init() + { + $serviceAccountConfig = ''; + if (empty($this->options['serviceAccountConfigFile'])) { + if (empty($options['client_id'])) { + if (defined('ELFINDER_GOOGLEDRIVE_CLIENTID') && ELFINDER_GOOGLEDRIVE_CLIENTID) { + $this->options['client_id'] = ELFINDER_GOOGLEDRIVE_CLIENTID; + } else { + return $this->setError('Required option "client_id" is undefined.'); + } + } + if (empty($options['client_secret'])) { + if (defined('ELFINDER_GOOGLEDRIVE_CLIENTSECRET') && ELFINDER_GOOGLEDRIVE_CLIENTSECRET) { + $this->options['client_secret'] = ELFINDER_GOOGLEDRIVE_CLIENTSECRET; + } else { + return $this->setError('Required option "client_secret" is undefined.'); + } + } + if (!$this->options['access_token'] && !$this->options['refresh_token']) { + return $this->setError('Required option "access_token" or "refresh_token" is undefined.'); + } + } else { + if (!is_readable($this->options['serviceAccountConfigFile'])) { + return $this->setError('Option "serviceAccountConfigFile" file is not readable.'); + } + $serviceAccountConfig = $this->options['serviceAccountConfigFile']; + } + + try { + if (!$serviceAccountConfig) { + $aTokenFile = ''; + if ($this->options['refresh_token']) { + // permanent mount + $aToken = $this->options['refresh_token']; + $this->options['access_token'] = ''; + $tmp = elFinder::getStaticVar('commonTempPath'); + if (!$tmp) { + $tmp = $this->getTempPath(); + } + if ($tmp) { + $aTokenFile = $tmp . DIRECTORY_SEPARATOR . md5($this->options['client_id'] . $this->options['refresh_token']) . '.gtoken'; + if (is_file($aTokenFile)) { + $this->options['access_token'] = json_decode(file_get_contents($aTokenFile), true); + } + } + } else { + // make net mount key for network mount + if (is_array($this->options['access_token'])) { + $aToken = !empty($this->options['access_token']['refresh_token']) + ? $this->options['access_token']['refresh_token'] + : $this->options['access_token']['access_token']; + } else { + return $this->setError('Required option "access_token" is not Array or empty.'); + } + } + } + + $errors = []; + if ($this->needOnline && !$this->service) { + if (($this->options['googleApiClient'] || defined('ELFINDER_GOOGLEDRIVE_GOOGLEAPICLIENT')) && !class_exists('Google_Client')) { + include_once $this->options['googleApiClient'] ? $this->options['googleApiClient'] : ELFINDER_GOOGLEDRIVE_GOOGLEAPICLIENT; + } + if (!class_exists('Google_Client')) { + return $this->setError('Class Google_Client not found.'); + } + + $this->client = new \Google_Client(); + + $client = $this->client; + + if (!$serviceAccountConfig) { + if ($this->options['access_token']) { + $client->setAccessToken($this->options['access_token']); + $access_token = $this->options['access_token']; + } + if ($client->isAccessTokenExpired()) { + $client->setClientId($this->options['client_id']); + $client->setClientSecret($this->options['client_secret']); + $access_token = $client->fetchAccessTokenWithRefreshToken($this->options['refresh_token'] ?: null); + $client->setAccessToken($access_token); + if ($aTokenFile) { + file_put_contents($aTokenFile, json_encode($access_token)); + } else { + $access_token['refresh_token'] = $this->options['access_token']['refresh_token']; + } + if (!empty($this->options['netkey'])) { + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'access_token', $access_token); + } + $this->options['access_token'] = $access_token; + } + $this->expires = empty($access_token['refresh_token']) ? $access_token['created'] + $access_token['expires_in'] - 30 : 0; + } else { + $client->setAuthConfigFile($serviceAccountConfig); + $client->setScopes([Google_Service_Drive::DRIVE]); + $aToken = $client->getClientId(); + } + $this->service = new \Google_Service_Drive($client); + } + + if ($this->needOnline) { + $this->netMountKey = md5($aToken . '-' . $this->options['path']); + } + } catch (InvalidArgumentException $e) { + $errors[] = $e->getMessage(); + } catch (Google_Service_Exception $e) { + $errors[] = $e->getMessage(); + } + + if ($this->needOnline && !$this->service) { + $this->session->remove($this->id . $this->netMountKey); + if ($aTokenFile) { + if (is_file($aTokenFile)) { + unlink($aTokenFile); + } + } + $errors[] = 'Google Drive Service could not be loaded.'; + + return $this->setError($errors); + } + + // normalize root path + if ($this->options['path'] == 'root') { + $this->options['path'] = '/'; + } + $this->root = $this->options['path'] = $this->_normpath($this->options['path']); + + if (empty($this->options['alias'])) { + if ($this->needOnline) { + $this->options['root'] = ($this->options['root'] === '')? $this->_gd_getNameByPath('root') : $this->options['root']; + $this->options['alias'] = ($this->options['path'] === '/') ? $this->options['root'] : sprintf($this->options['gdAlias'], $this->_gd_getNameByPath($this->options['path'])); + if (!empty($this->options['netkey'])) { + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']); + } + } else { + $this->options['root'] = ($this->options['root'] === '')? 'GoogleDrive' : $this->options['root']; + $this->options['alias'] = $this->options['root']; + } + } + + $this->rootName = isset($this->options['alias'])? $this->options['alias'] : 'GoogleDrive'; + + if (!empty($this->options['tmpPath'])) { + if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($this->options['tmpPath'])) { + $this->tmp = $this->options['tmpPath']; + } + } + + if (!$this->tmp && ($tmp = elFinder::getStaticVar('commonTempPath'))) { + $this->tmp = $tmp; + } + + // This driver dose not support `syncChkAsTs` + $this->options['syncChkAsTs'] = false; + + // 'lsPlSleep' minmum 10 sec + $this->options['lsPlSleep'] = max(10, $this->options['lsPlSleep']); + + if ($this->options['useGoogleTmb']) { + $this->options['tmbURL'] = 'https://'; + $this->options['tmbPath'] = ''; + } + + // enable command archive + $this->options['useRemoteArchive'] = true; + + return true; + } + + /** + * Configure after successfull mount. + * + * @author Dmitry (dio) Levashov + **/ + protected function configure() + { + parent::configure(); + + // fallback of $this->tmp + if (!$this->tmp && $this->tmbPathWritable) { + $this->tmp = $this->tmbPath; + } + + if ($this->needOnline && $this->isMyReload()) { + $this->_gd_getDirectoryData(false); + } + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /** + * Close opened connection. + * + * @author Dmitry (dio) Levashov + **/ + public function umount() + { + } + + /** + * Cache dir contents. + * + * @param string $path dir path + * + * @return array + * @author Dmitry Levashov + */ + protected function cacheDir($path) + { + $this->dirsCache[$path] = []; + $hasDir = false; + + list(, $pid) = $this->_gd_splitPath($path); + + $opts = [ + 'fields' => self::FETCHFIELDS_LIST, + 'q' => sprintf('trashed=false and "%s" in parents', $pid), + ]; + + $res = $this->_gd_query($opts); + + $mountPath = $this->_normpath($path . '/'); + + if ($res) { + foreach ($res as $raw) { + if ($stat = $this->_gd_parseRaw($raw)) { + $stat = $this->updateCache($mountPath . $raw->id, $stat); + if (empty($stat['hidden']) && $path !== $mountPath . $raw->id) { + if (!$hasDir && $stat['mime'] === 'directory') { + $hasDir = true; + } + $this->dirsCache[$path][] = $mountPath . $raw->id; + } + } + } + } + + if (isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'][$path] = $hasDir; + } + + return $this->dirsCache[$path]; + } + + /** + * Recursive files search. + * + * @param string $path dir path + * @param string $q search string + * @param array $mimes + * + * @return array + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function doSearch($path, $q, $mimes) + { + if (!empty($this->doSearchCurrentQuery['matchMethod'])) { + // has custom match method use elFinderVolumeDriver::doSearch() + return parent::doSearch($path, $q, $mimes); + } + + list(, $itemId) = $this->_gd_splitPath($path); + + $path = $this->_normpath($path . '/'); + $result = []; + $query = ''; + + if ($itemId !== 'root') { + $dirs = array_merge([$itemId], $this->_gd_getDirectories($itemId)); + $query = '(\'' . implode('\' in parents or \'', $dirs) . '\' in parents)'; + } + + $tmp = []; + if (!$mimes) { + foreach (explode(' ', $q) as $_v) { + $tmp[] = 'fullText contains \'' . str_replace('\'', '\\\'', $_v) . '\''; + } + $query .= ($query ? ' and ' : '') . implode(' and ', $tmp); + } else { + foreach ($mimes as $_v) { + $tmp[] = 'mimeType contains \'' . str_replace('\'', '\\\'', $_v) . '\''; + } + $query .= ($query ? ' and ' : '') . '(' . implode(' or ', $tmp) . ')'; + } + + $opts = [ + 'q' => sprintf('trashed=false and (%s)', $query), + ]; + + $res = $this->_gd_query($opts); + + $timeout = $this->options['searchTimeout'] ? $this->searchStart + $this->options['searchTimeout'] : 0; + foreach ($res as $raw) { + if ($timeout && $timeout < time()) { + $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->_path($path)); + break; + } + if ($stat = $this->_gd_parseRaw($raw)) { + if ($parents = $raw->getParents()) { + foreach ($parents as $parent) { + $paths = $this->_gd_getMountPaths($parent); + foreach ($paths as $path) { + $path = ($path === '') ? '/' : (rtrim($path, '/') . '/'); + if (!isset($this->cache[$path . $raw->id])) { + $stat = $this->updateCache($path . $raw->id, $stat); + } else { + $stat = $this->cache[$path . $raw->id]; + } + if (empty($stat['hidden'])) { + $stat['path'] = $this->_path($path) . $stat['name']; + $result[] = $stat; + } + } + } + } + } + } + + return $result; + } + + /** + * Copy file/recursive copy dir only in current volume. + * Return new file path or false. + * + * @param string $src source path + * @param string $dst destination dir path + * @param string $name new file name (optionaly) + * + * @return string|false + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + **/ + protected function copy($src, $dst, $name) + { + $this->clearcache(); + $res = $this->_gd_getFile($src); + if ($res['mimeType'] == self::DIRMIME) { + $newDir = $this->_mkdir($dst, $name); + if ($newDir) { + list(, $itemId) = $this->_gd_splitPath($newDir); + list(, $srcId) = $this->_gd_splitPath($src); + $path = $this->_joinPath($dst, $itemId); + $opts = [ + 'q' => sprintf('trashed=false and "%s" in parents', $srcId), + ]; + + $res = $this->_gd_query($opts); + foreach ($res as $raw) { + $raw['mimeType'] == self::DIRMIME ? $this->copy($src . '/' . $raw['id'], $path, $raw['name']) : $this->_copy($src . '/' . $raw['id'], $path, $raw['name']); + } + + $ret = $this->_joinPath($dst, $itemId); + $this->added[] = $this->stat($ret); + } else { + $ret = $this->setError(elFinder::ERROR_COPY, $this->_path($src)); + } + } else { + if ($itemId = $this->_copy($src, $dst, $name)) { + $ret = $this->_joinPath($dst, $itemId); + $this->added[] = $this->stat($ret); + } else { + $ret = $this->setError(elFinder::ERROR_COPY, $this->_path($src)); + } + } + return $ret; + } + + /** + * Remove file/ recursive remove dir. + * + * @param string $path file path + * @param bool $force try to remove even if file locked + * @param bool $recursive + * + * @return bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function remove($path, $force = false, $recursive = false) + { + $stat = $this->stat($path); + $stat['realpath'] = $path; + $this->rmTmb($stat); + $this->clearcache(); + + if (empty($stat)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path), elFinder::ERROR_FILE_NOT_FOUND); + } + + if (!$force && !empty($stat['locked'])) { + return $this->setError(elFinder::ERROR_LOCKED, $this->_path($path)); + } + + if ($stat['mime'] == 'directory') { + if (!$recursive && !$this->_rmdir($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } else { + if (!$recursive && !$this->_unlink($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } + + $this->removed[] = $stat; + + return true; + } + + /** + * Create thumnbnail and return it's URL on success. + * + * @param string $path file path + * @param $stat + * + * @return string|false + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function createTmb($path, $stat) + { + if (!$stat || !$this->canCreateTmb($path, $stat)) { + return false; + } + + $name = $this->tmbname($stat); + $tmb = $this->tmbPath . DIRECTORY_SEPARATOR . $name; + + // copy image into tmbPath so some drivers does not store files on local fs + if (!$data = $this->_gd_getThumbnail($path)) { + return false; + } + if (!file_put_contents($tmb, $data)) { + return false; + } + + $result = false; + + $tmbSize = $this->tmbSize; + + if (($s = getimagesize($tmb)) == false) { + return false; + } + + /* If image smaller or equal thumbnail size - just fitting to thumbnail square */ + if ($s[0] <= $tmbSize && $s[1] <= $tmbSize) { + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } else { + if ($this->options['tmbCrop']) { + + /* Resize and crop if image bigger than thumbnail */ + if (!(($s[0] > $tmbSize && $s[1] <= $tmbSize) || ($s[0] <= $tmbSize && $s[1] > $tmbSize)) || ($s[0] > $tmbSize && $s[1] > $tmbSize)) { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, false, 'png'); + } + + if (($s = getimagesize($tmb)) != false) { + $x = $s[0] > $tmbSize ? intval(($s[0] - $tmbSize) / 2) : 0; + $y = $s[1] > $tmbSize ? intval(($s[1] - $tmbSize) / 2) : 0; + $result = $this->imgCrop($tmb, $tmbSize, $tmbSize, $x, $y, 'png'); + } + } else { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, true, 'png'); + } + + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } + + if (!$result) { + unlink($tmb); + + return false; + } + + return $name; + } + + /** + * Return thumbnail file name for required file. + * + * @param array $stat file stat + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function tmbname($stat) + { + return $this->netMountKey . $stat['iid'] . $stat['ts'] . '.png'; + } + + /** + * Return content URL (for netmout volume driver) + * If file.url == 1 requests from JavaScript client with XHR. + * + * @param string $hash file hash + * @param array $options options array + * + * @return bool|string + * @author Naoki Sawada + */ + public function getContentUrl($hash, $options = []) + { + if (!empty($options['onetime']) && $this->options['onetimeUrl']) { + return parent::getContentUrl($hash, $options); + } + if (!empty($options['temporary'])) { + // try make temporary file + $url = parent::getContentUrl($hash, $options); + if ($url) { + return $url; + } + } + if (($file = $this->file($hash)) == false || !$file['url'] || $file['url'] == 1) { + $path = $this->decode($hash); + + if ($this->_gd_publish($path)) { + if ($raw = $this->_gd_getFile($path)) { + return $this->_gd_getLink($raw); + } + } + } + + return false; + } + + /** + * Return debug info for client. + * + * @return array + **/ + public function debug() + { + $res = parent::debug(); + if (!empty($this->options['netkey']) && empty($this->options['refresh_token']) && $this->options['access_token'] && isset($this->options['access_token']['refresh_token'])) { + $res['refresh_token'] = $this->options['access_token']['refresh_token']; + } + + return $res; + } + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dirname($path) + { + list(, , $parent) = $this->_gd_splitPath($path); + + return $this->_normpath($parent); + } + + /** + * Return file name. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _basename($path) + { + list(, $basename) = $this->_gd_splitPath($path); + + return $basename; + } + + /** + * Join dir name and file name and retur full path. + * + * @param string $dir + * @param string $name + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _joinPath($dir, $name) + { + return $this->_normpath($dir . '/' . str_replace('/', '\\/', $name)); + } + + /** + * Return normalized path, this works the same as os.path.normpath() in Python. + * + * @param string $path path + * + * @return string + * @author Troex Nevelin + **/ + protected function _normpath($path) + { + if (DIRECTORY_SEPARATOR !== '/') { + $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + } + $path = '/' . ltrim($path, '/'); + + return $path; + } + + /** + * Return file path related to root dir. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _relpath($path) + { + return $path; + } + + /** + * Convert path related to root dir into real path. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _abspath($path) + { + return $path; + } + + /** + * Return fake path started from root dir. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _path($path) + { + if (!$this->names) { + $this->_gd_getDirectoryData(); + } + $path = $this->_normpath(substr($path, strlen($this->root))); + $names = []; + $paths = explode('/', $path); + foreach ($paths as $_p) { + $names[] = isset($this->names[$_p]) ? $this->names[$_p] : $_p; + } + + return $this->rootName . implode('/', $names); + } + + /** + * Return true if $path is children of $parent. + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _inpath($path, $parent) + { + return $path == $parent || strpos($path, $parent . '/') === 0; + } + + /***************** file stat ********************/ + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally. + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + protected function _stat($path) + { + if ($raw = $this->_gd_getFile($path)) { + $stat = $this->_gd_parseRaw($raw); + if ($path === $this->root) { + $stat['expires'] = $this->expires; + } + return $stat; + } + + return false; + } + + /** + * Return true if path is dir and has at least one childs directory. + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _subdirs($path) + { + if ($this->directories === null) { + $this->_gd_getDirectoryData(); + } + list(, $itemId) = $this->_gd_splitPath($path); + + return isset($this->directories[$itemId]); + } + + /** + * Return object width and height + * Ususaly used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * + * @return string + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function _dimensions($path, $mime) + { + if (strpos($mime, 'image') !== 0) { + return ''; + } + $ret = ''; + + if ($file = $this->_gd_getFile($path)) { + if (isset($file['imageMediaMetadata'])) { + $ret = array('dim' => $file['imageMediaMetadata']['width'] . 'x' . $file['imageMediaMetadata']['height']); + if (func_num_args() > 2) { + $args = func_get_arg(2); + } else { + $args = array(); + } + if (!empty($args['substitute'])) { + $tmbSize = intval($args['substitute']); + $srcSize = explode('x', $ret['dim']); + if ($srcSize[0] && $srcSize[1]) { + if (min(($tmbSize / $srcSize[0]), ($tmbSize / $srcSize[1])) < 1) { + if ($this->_gd_isPublished($file)) { + $tmbSize = strval($tmbSize); + $ret['url'] = 'https://drive.google.com/thumbnail?authuser=0&sz=s' . $tmbSize . '&id=' . $file['id']; + } elseif ($subImgLink = $this->getSubstituteImgLink(elFinder::$currentArgs['target'], $srcSize)) { + $ret['url'] = $subImgLink; + } + } + } + } + } + } + + return $ret; + } + + /******************** file/dir content *********************/ + + /** + * Return files list in directory. + * + * @param string $path dir path + * + * @return array + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + **/ + protected function _scandir($path) + { + return isset($this->dirsCache[$path]) + ? $this->dirsCache[$path] + : $this->cacheDir($path); + } + + /** + * Open file and return file pointer. + * + * @param string $path file path + * @param bool $write open file for writing + * + * @return resource|false + * @author Dmitry (dio) Levashov + **/ + protected function _fopen($path, $mode = 'rb') + { + if ($mode === 'rb' || $mode === 'r') { + if ($file = $this->_gd_getFile($path)) { + if ($dlurl = $this->_gd_getDownloadUrl($file)) { + $token = $this->client->getAccessToken(); + if (!$token && $this->client->isUsingApplicationDefaultCredentials()) { + $this->client->fetchAccessTokenWithAssertion(); + $token = $this->client->getAccessToken(); + } + $access_token = ''; + if (is_array($token)) { + $access_token = $token['access_token']; + } else { + if ($token = json_decode($this->client->getAccessToken())) { + $access_token = $token->access_token; + } + } + if ($access_token) { + $data = array( + 'target' => $dlurl, + 'headers' => array('Authorization: Bearer ' . $access_token), + ); + + // to support range request + if (func_num_args() > 2) { + $opts = func_get_arg(2); + } else { + $opts = array(); + } + if (!empty($opts['httpheaders'])) { + $data['headers'] = array_merge($opts['httpheaders'], $data['headers']); + } + + return elFinder::getStreamByUrl($data); + } + } + } + } + + return false; + } + + /** + * Close opened file. + * + * @param resource $fp file pointer + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _fclose($fp, $path = '') + { + is_resource($fp) && fclose($fp); + if ($path) { + unlink($this->getTempFile($path)); + } + } + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed. + * + * @param string $path parent dir path + * @param string $name new directory name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkdir($path, $name) + { + $path = $this->_joinPath($path, $name); + list($parentId, , $parent) = $this->_gd_splitPath($path); + + try { + $file = new \Google_Service_Drive_DriveFile(); + + $file->setName($name); + $file->setMimeType(self::DIRMIME); + $file->setParents([$parentId]); + + //create the Folder in the Parent + $obj = $this->service->files->create($file); + + if ($obj instanceof Google_Service_Drive_DriveFile) { + $path = $this->_joinPath($parent, $obj['id']); + $this->_gd_getDirectoryData(false); + + return $path; + } else { + return false; + } + } catch (Exception $e) { + return $this->setError('GoogleDrive error: ' . $e->getMessage()); + } + } + + /** + * Create file and return it's path or false on failed. + * + * @param string $path parent dir path + * @param string $name new file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkfile($path, $name) + { + return $this->_save($this->tmpfile(), $path, $name, []); + } + + /** + * Create symlink. FTP driver does not support symlinks. + * + * @param string $target link target + * @param string $path symlink path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _symlink($target, $path, $name) + { + return false; + } + + /** + * Copy file into another file. + * + * @param string $source source file path + * @param string $targetDir target directory path + * @param string $name new file name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _copy($source, $targetDir, $name) + { + $source = $this->_normpath($source); + $targetDir = $this->_normpath($targetDir); + + try { + $file = new \Google_Service_Drive_DriveFile(); + $file->setName($name); + + //Set the Parent id + list(, $parentId) = $this->_gd_splitPath($targetDir); + $file->setParents([$parentId]); + + list(, $srcId) = $this->_gd_splitPath($source); + $file = $this->service->files->copy($srcId, $file, ['fields' => self::FETCHFIELDS_GET]); + $itemId = $file->id; + + return $itemId; + } catch (Exception $e) { + return $this->setError('GoogleDrive error: ' . $e->getMessage()); + } + + return true; + } + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param string $target target dir path + * @param string $name file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _move($source, $targetDir, $name) + { + list($removeParents, $itemId) = $this->_gd_splitPath($source); + $target = $this->_normpath($targetDir . '/' . $itemId); + try { + //moving and renaming a file or directory + $files = new \Google_Service_Drive_DriveFile(); + $files->setName($name); + + //Set new Parent and remove old parent + list(, $addParents) = $this->_gd_splitPath($targetDir); + $opts = ['addParents' => $addParents, 'removeParents' => $removeParents]; + + $file = $this->service->files->update($itemId, $files, $opts); + + if ($file->getMimeType() === self::DIRMIME) { + $this->_gd_getDirectoryData(false); + } + } catch (Exception $e) { + return $this->setError('GoogleDrive error: ' . $e->getMessage()); + } + + return $target; + } + + /** + * Remove file. + * + * @param string $path file path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _unlink($path) + { + try { + $files = new \Google_Service_Drive_DriveFile(); + $files->setTrashed(true); + + list($pid, $itemId) = $this->_gd_splitPath($path); + $opts = ['removeParents' => $pid]; + $this->service->files->update($itemId, $files, $opts); + } catch (Exception $e) { + return $this->setError('GoogleDrive error: ' . $e->getMessage()); + } + + return true; + } + + /** + * Remove dir. + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _rmdir($path) + { + $res = $this->_unlink($path); + $res && $this->_gd_getDirectoryData(false); + + return $res; + } + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param $path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Dmitry (dio) Levashov + */ + protected function _save($fp, $path, $name, $stat) + { + if ($name !== '') { + $path .= '/' . str_replace('/', '\\/', $name); + } + list($parentId, $itemId, $parent) = $this->_gd_splitPath($path); + if ($name === '') { + $stat['iid'] = $itemId; + } + + if (!$stat || empty($stat['iid'])) { + $opts = [ + 'q' => sprintf('trashed=false and "%s" in parents and name="%s"', $parentId, $name), + 'fields' => self::FETCHFIELDS_LIST, + ]; + $srcFile = $this->_gd_query($opts); + $srcFile = empty($srcFile) ? null : $srcFile[0]; + } else { + $srcFile = $this->_gd_getFile($path); + } + + try { + $mode = 'update'; + $mime = isset($stat['mime']) ? $stat['mime'] : ''; + + $file = new Google_Service_Drive_DriveFile(); + if ($srcFile) { + $mime = $srcFile->getMimeType(); + } else { + $mode = 'insert'; + $file->setName($name); + $file->setParents([ + $parentId, + ]); + } + + if (!$mime) { + $mime = self::mimetypeInternalDetect($name); + } + if ($mime === 'unknown') { + $mime = 'application/octet-stream'; + } + $file->setMimeType($mime); + + $size = 0; + if (isset($stat['size'])) { + $size = $stat['size']; + } else { + $fstat = fstat($fp); + if (!empty($fstat['size'])) { + $size = $fstat['size']; + } + } + + // set chunk size (max: 100MB) + $chunkSizeBytes = 100 * 1024 * 1024; + if ($size > 0) { + $memory = elFinder::getIniBytes('memory_limit'); + if ($memory > 0) { + $chunkSizeBytes = max(262144, min([$chunkSizeBytes, (intval($memory / 4 / 256) * 256)])); + } + } + + if ($size > $chunkSizeBytes) { + $client = $this->client; + // Call the API with the media upload, defer so it doesn't immediately return. + $client->setDefer(true); + if ($mode === 'insert') { + $request = $this->service->files->create($file, [ + 'fields' => self::FETCHFIELDS_GET, + ]); + } else { + $request = $this->service->files->update($srcFile->getId(), $file, [ + 'fields' => self::FETCHFIELDS_GET, + ]); + } + + // Create a media file upload to represent our upload process. + $media = new Google_Http_MediaFileUpload($client, $request, $mime, null, true, $chunkSizeBytes); + $media->setFileSize($size); + // Upload the various chunks. $status will be false until the process is + // complete. + $status = false; + while (!$status && !feof($fp)) { + elFinder::checkAborted(); + // read until you get $chunkSizeBytes from TESTFILE + // fread will never return more than 8192 bytes if the stream is read buffered and it does not represent a plain file + // An example of a read buffered file is when reading from a URL + $chunk = $this->_gd_readFileChunk($fp, $chunkSizeBytes); + $status = $media->nextChunk($chunk); + } + // The final value of $status will be the data from the API for the object + // that has been uploaded. + if ($status !== false) { + $obj = $status; + } + + $client->setDefer(false); + } else { + $params = [ + 'data' => stream_get_contents($fp), + 'uploadType' => 'media', + 'fields' => self::FETCHFIELDS_GET, + ]; + if ($mode === 'insert') { + $obj = $this->service->files->create($file, $params); + } else { + $obj = $this->service->files->update($srcFile->getId(), $file, $params); + } + } + if ($obj instanceof Google_Service_Drive_DriveFile) { + return $this->_joinPath($parent, $obj->getId()); + } else { + return false; + } + } catch (Exception $e) { + return $this->setError('GoogleDrive error: ' . $e->getMessage()); + } + } + + /** + * Get file contents. + * + * @param string $path file path + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function _getContents($path) + { + $contents = ''; + + try { + list(, $itemId) = $this->_gd_splitPath($path); + + $contents = $this->service->files->get($itemId, [ + 'alt' => 'media', + ]); + $contents = (string)$contents->getBody(); + } catch (Exception $e) { + return $this->setError('GoogleDrive error: ' . $e->getMessage()); + } + + return $contents; + } + + /** + * Write a string to a file. + * + * @param string $path file path + * @param string $content new file content + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _filePutContents($path, $content) + { + $res = false; + + if ($local = $this->getTempFile($path)) { + if (file_put_contents($local, $content, LOCK_EX) !== false + && ($fp = fopen($local, 'rb'))) { + clearstatcache(); + $res = $this->_save($fp, $path, '', []); + fclose($fp); + } + file_exists($local) && unlink($local); + } + + return $res; + } + + /** + * Detect available archivers. + **/ + protected function _checkArchivers() + { + // die('Not yet implemented. (_checkArchivers)'); + return []; + } + + /** + * chmod implementation. + * + * @return bool + **/ + protected function _chmod($path, $mode) + { + return false; + } + + /** + * Unpack archive. + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return void + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + */ + protected function _unpack($path, $arc) + { + die('Not yet implemented. (_unpack)'); + //return false; + } + + /** + * Extract files from archive. + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return void + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + */ + protected function _extract($path, $arc) + { + die('Not yet implemented. (_extract)'); + } + + /** + * Create archive and return its path. + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _archive($dir, $files, $name, $arc) + { + die('Not yet implemented. (_archive)'); + } +} // END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeGroup.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeGroup.class.php new file mode 100644 index 0000000..38e7e54 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeGroup.class.php @@ -0,0 +1,315 @@ +options['type'] = 'group'; + $this->options['path'] = '/'; + $this->options['dirUrlOwn'] = true; + $this->options['syncMinMs'] = 0; + $this->options['tmbPath'] = ''; + $this->options['disabled'] = array( + 'archive', + 'copy', + 'cut', + 'duplicate', + 'edit', + 'empty', + 'extract', + 'getfile', + 'mkdir', + 'mkfile', + 'paste', + 'resize', + 'rm', + 'upload' + ); + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /*********************** paths/urls *************************/ + + /** + * @inheritdoc + **/ + protected function _dirname($path) + { + return '/'; + } + + /** + * {@inheritDoc} + **/ + protected function _basename($path) + { + return ''; + } + + /** + * {@inheritDoc} + **/ + protected function _joinPath($dir, $name) + { + return '/' . $name; + } + + /** + * {@inheritDoc} + **/ + protected function _normpath($path) + { + return '/'; + } + + /** + * {@inheritDoc} + **/ + protected function _relpath($path) + { + return '/'; + } + + /** + * {@inheritDoc} + **/ + protected function _abspath($path) + { + return '/'; + } + + /** + * {@inheritDoc} + **/ + protected function _path($path) + { + return '/'; + } + + /** + * {@inheritDoc} + **/ + protected function _inpath($path, $parent) + { + return false; + } + + + + /***************** file stat ********************/ + + /** + * {@inheritDoc} + **/ + protected function _stat($path) + { + if ($path === '/') { + return array( + 'size' => 0, + 'ts' => 0, + 'mime' => 'directory', + 'read' => true, + 'write' => false, + 'locked' => true, + 'hidden' => false, + 'dirs' => 0 + ); + } + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _subdirs($path) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _dimensions($path, $mime) + { + return false; + } + /******************** file/dir content *********************/ + + /** + * {@inheritDoc} + **/ + protected function readlink($path) + { + return null; + } + + /** + * {@inheritDoc} + **/ + protected function _scandir($path) + { + return array(); + } + + /** + * {@inheritDoc} + **/ + protected function _fopen($path, $mode = 'rb') + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _fclose($fp, $path = '') + { + return true; + } + + /******************** file/dir manipulations *************************/ + + /** + * {@inheritDoc} + **/ + protected function _mkdir($path, $name) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _mkfile($path, $name) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _symlink($source, $targetDir, $name) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _copy($source, $targetDir, $name) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _move($source, $targetDir, $name) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _unlink($path) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _rmdir($path) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _save($fp, $dir, $name, $stat) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _getContents($path) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _filePutContents($path, $content) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _checkArchivers() + { + return; + } + + /** + * {@inheritDoc} + **/ + protected function _chmod($path, $mode) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _findSymlinks($path) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _extract($path, $arc) + { + return false; + } + + /** + * {@inheritDoc} + **/ + protected function _archive($dir, $files, $name, $arc) + { + return false; + } +} + diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeLocalFileSystem.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeLocalFileSystem.class.php new file mode 100644 index 0000000..cc252db --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeLocalFileSystem.class.php @@ -0,0 +1,1442 @@ +callback = $callback; + parent::__construct($iterator); + } + + public function accept() + { + return call_user_func($this->callback, parent::current(), parent::key(), parent::getInnerIterator()); + } + + public function getChildren() + { + return new self($this->getInnerIterator()->getChildren(), $this->callback); + } + } +} + +/** + * elFinder driver for local filesystem. + * + * @author Dmitry (dio) Levashov + * @author Troex Nevelin + **/ +class elFinderVolumeLocalFileSystem extends elFinderVolumeDriver +{ + + /** + * Driver id + * Must be started from letter and contains [a-z0-9] + * Used as part of volume id + * + * @var string + **/ + protected $driverId = 'l'; + + /** + * Required to count total archive files size + * + * @var int + **/ + protected $archiveSize = 0; + + /** + * Is checking stat owner + * + * @var boolean + */ + protected $statOwner = false; + + /** + * Path to quarantine directory + * + * @var string + */ + private $quarantine; + + /** + * Constructor + * Extend options with required fields + * + * @author Dmitry (dio) Levashov + */ + public function __construct() + { + $this->options['alias'] = ''; // alias to replace root dir name + $this->options['dirMode'] = 0755; // new dirs mode + $this->options['fileMode'] = 0644; // new files mode + $this->options['quarantine'] = '.quarantine'; // quarantine folder name - required to check archive (must be hidden) + $this->options['rootCssClass'] = 'elfinder-navbar-root-local'; + $this->options['followSymLinks'] = true; + $this->options['detectDirIcon'] = ''; // file name that is detected as a folder icon e.g. '.diricon.png' + $this->options['keepTimestamp'] = array('copy', 'move'); // keep timestamp at inner filesystem allowed 'copy', 'move' and 'upload' + $this->options['substituteImg'] = true; // support substitute image with dim command + $this->options['statCorrector'] = null; // callable to correct stat data `function(&$stat, $path, $statOwner, $volumeDriveInstance){}` + } + + /*********************************************************************/ + /* INIT AND CONFIGURE */ + /*********************************************************************/ + + /** + * Prepare driver before mount volume. + * Return true if volume is ready. + * + * @return bool + **/ + protected function init() + { + // Normalize directory separator for windows + if (DIRECTORY_SEPARATOR !== '/') { + foreach (array('path', 'tmbPath', 'tmpPath', 'quarantine') as $key) { + if (!empty($this->options[$key])) { + $this->options[$key] = str_replace('/', DIRECTORY_SEPARATOR, $this->options[$key]); + } + } + // PHP >= 7.1 Supports UTF-8 path on Windows + if (version_compare(PHP_VERSION, '7.1', '>=')) { + $this->options['encoding'] = ''; + $this->options['locale'] = ''; + } + } + if (!$cwd = getcwd()) { + return $this->setError('elFinder LocalVolumeDriver requires a result of getcwd().'); + } + // detect systemRoot + if (!isset($this->options['systemRoot'])) { + if ($cwd[0] === DIRECTORY_SEPARATOR || $this->root[0] === DIRECTORY_SEPARATOR) { + $this->systemRoot = DIRECTORY_SEPARATOR; + } else if (preg_match('/^([a-zA-Z]:' . preg_quote(DIRECTORY_SEPARATOR, '/') . ')/', $this->root, $m)) { + $this->systemRoot = $m[1]; + } else if (preg_match('/^([a-zA-Z]:' . preg_quote(DIRECTORY_SEPARATOR, '/') . ')/', $cwd, $m)) { + $this->systemRoot = $m[1]; + } + } + $this->root = $this->getFullPath($this->root, $cwd); + if (!empty($this->options['startPath'])) { + $this->options['startPath'] = $this->getFullPath($this->options['startPath'], $this->root); + } + + if (is_null($this->options['syncChkAsTs'])) { + $this->options['syncChkAsTs'] = true; + } + if (is_null($this->options['syncCheckFunc'])) { + $this->options['syncCheckFunc'] = array($this, 'localFileSystemInotify'); + } + // check 'statCorrector' + if (empty($this->options['statCorrector']) || !is_callable($this->options['statCorrector'])) { + $this->options['statCorrector'] = null; + } + + return true; + } + + /** + * Configure after successfull mount. + * + * @return void + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function configure() + { + $hiddens = array(); + $root = $this->stat($this->root); + + // check thumbnails path + if (!empty($this->options['tmbPath'])) { + if (strpos($this->options['tmbPath'], DIRECTORY_SEPARATOR) === false) { + $hiddens['tmb'] = $this->options['tmbPath']; + $this->options['tmbPath'] = $this->_abspath($this->options['tmbPath']); + } else { + $this->options['tmbPath'] = $this->_normpath($this->options['tmbPath']); + } + } + // check temp path + if (!empty($this->options['tmpPath'])) { + if (strpos($this->options['tmpPath'], DIRECTORY_SEPARATOR) === false) { + $hiddens['temp'] = $this->options['tmpPath']; + $this->options['tmpPath'] = $this->_abspath($this->options['tmpPath']); + } else { + $this->options['tmpPath'] = $this->_normpath($this->options['tmpPath']); + } + } + // check quarantine path + if (!empty($this->options['quarantine'])) { + if (strpos($this->options['quarantine'], DIRECTORY_SEPARATOR) === false) { + $hiddens['quarantine'] = $this->options['quarantine']; + $this->options['quarantine'] = $this->_abspath($this->options['quarantine']); + } else { + $this->options['quarantine'] = $this->_normpath($this->options['quarantine']); + } + } + + parent::configure(); + + // check tmbPath + if (!$this->tmbPath && isset($hiddens['tmb'])) { + unset($hiddens['tmb']); + } + + // if no thumbnails url - try detect it + if ($root['read'] && !$this->tmbURL && $this->URL) { + if (strpos($this->tmbPath, $this->root) === 0) { + $this->tmbURL = $this->URL . str_replace(DIRECTORY_SEPARATOR, '/', substr($this->tmbPath, strlen($this->root) + 1)); + if (preg_match("|[^/?&=]$|", $this->tmbURL)) { + $this->tmbURL .= '/'; + } + } + } + + // set $this->tmp by options['tmpPath'] + $this->tmp = ''; + if (!empty($this->options['tmpPath'])) { + if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'], $this->options['dirMode'], true)) && is_writable($this->options['tmpPath'])) { + $this->tmp = $this->options['tmpPath']; + } else { + if (isset($hiddens['temp'])) { + unset($hiddens['temp']); + } + } + } + if (!$this->tmp && ($tmp = elFinder::getStaticVar('commonTempPath'))) { + $this->tmp = $tmp; + } + + // check quarantine dir + $this->quarantine = ''; + if (!empty($this->options['quarantine'])) { + if ((is_dir($this->options['quarantine']) || mkdir($this->options['quarantine'], $this->options['dirMode'], true)) && is_writable($this->options['quarantine'])) { + $this->quarantine = $this->options['quarantine']; + } else { + if (isset($hiddens['quarantine'])) { + unset($hiddens['quarantine']); + } + } + } + + if (!$this->quarantine) { + if (!$this->tmp) { + $this->archivers['extract'] = array(); + $this->disabled[] = 'extract'; + } else { + $this->quarantine = $this->tmp; + } + } + + if ($hiddens) { + foreach ($hiddens as $hidden) { + $this->attributes[] = array( + 'pattern' => '~^' . preg_quote(DIRECTORY_SEPARATOR . $hidden, '~') . '$~', + 'read' => false, + 'write' => false, + 'locked' => true, + 'hidden' => true + ); + } + } + + if (!empty($this->options['keepTimestamp'])) { + $this->options['keepTimestamp'] = array_flip($this->options['keepTimestamp']); + } + + $this->statOwner = (!empty($this->options['statOwner'])); + } + + /** + * Long pooling sync checker + * This function require server command `inotifywait` + * If `inotifywait` need full path, Please add `define('ELFINER_INOTIFYWAIT_PATH', '/PATH_TO/inotifywait');` into connector.php + * + * @param string $path + * @param int $standby + * @param number $compare + * + * @return number|bool + * @throws elFinderAbortException + */ + public function localFileSystemInotify($path, $standby, $compare) + { + if (isset($this->sessionCache['localFileSystemInotify_disable'])) { + return false; + } + $path = realpath($path); + $mtime = filemtime($path); + if (!$mtime) { + return false; + } + if ($mtime != $compare) { + return $mtime; + } + $inotifywait = defined('ELFINER_INOTIFYWAIT_PATH') ? ELFINER_INOTIFYWAIT_PATH : 'inotifywait'; + $standby = max(1, intval($standby)); + $cmd = $inotifywait . ' ' . escapeshellarg($path) . ' -t ' . $standby . ' -e moved_to,moved_from,move,close_write,delete,delete_self'; + $this->procExec($cmd, $o, $r); + if ($r === 0) { + // changed + clearstatcache(); + if (file_exists($path)) { + $mtime = filemtime($path); // error on busy? + return $mtime ? $mtime : time(); + } else { + // target was removed + return 0; + } + } else if ($r === 2) { + // not changed (timeout) + return $compare; + } + // error + // cache to $_SESSION + $this->sessionCache['localFileSystemInotify_disable'] = true; + $this->session->set($this->id, $this->sessionCache); + return false; + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dirname($path) + { + return dirname($path); + } + + /** + * Return file name + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _basename($path) + { + return basename($path); + } + + /** + * Join dir name and file name and retur full path + * + * @param string $dir + * @param string $name + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _joinPath($dir, $name) + { + return rtrim($dir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $name; + } + + /** + * Return normalized path, this works the same as os.path.normpath() in Python + * + * @param string $path path + * + * @return string + * @author Troex Nevelin + **/ + protected function _normpath($path) + { + if (empty($path)) { + return '.'; + } + + $changeSep = (DIRECTORY_SEPARATOR !== '/'); + if ($changeSep) { + $drive = ''; + if (preg_match('/^([a-zA-Z]:)(.*)/', $path, $m)) { + $drive = $m[1]; + $path = $m[2] ? $m[2] : '/'; + } + $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + } + + if (strpos($path, '/') === 0) { + $initial_slashes = true; + } else { + $initial_slashes = false; + } + + if (($initial_slashes) + && (strpos($path, '//') === 0) + && (strpos($path, '///') === false)) { + $initial_slashes = 2; + } + + $initial_slashes = (int)$initial_slashes; + + $comps = explode('/', $path); + $new_comps = array(); + foreach ($comps as $comp) { + if (in_array($comp, array('', '.'))) { + continue; + } + + if (($comp != '..') + || (!$initial_slashes && !$new_comps) + || ($new_comps && (end($new_comps) == '..'))) { + array_push($new_comps, $comp); + } elseif ($new_comps) { + array_pop($new_comps); + } + } + $comps = $new_comps; + $path = implode('/', $comps); + if ($initial_slashes) { + $path = str_repeat('/', $initial_slashes) . $path; + } + + if ($changeSep) { + $path = $drive . str_replace('/', DIRECTORY_SEPARATOR, $path); + } + + return $path ? $path : '.'; + } + + /** + * Return file path related to root dir + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _relpath($path) + { + if ($path === $this->root) { + return ''; + } else { + if (strpos($path, $this->root) === 0) { + return ltrim(substr($path, strlen($this->root)), DIRECTORY_SEPARATOR); + } else { + // for link + return $path; + } + } + } + + /** + * Convert path related to root dir into real path + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _abspath($path) + { + if ($path === DIRECTORY_SEPARATOR) { + return $this->root; + } else { + if (strpos($path, $this->systemRoot) === 0) { + return $path; + } else if (DIRECTORY_SEPARATOR !== '/' && preg_match('/^[a-zA-Z]:' . preg_quote(DIRECTORY_SEPARATOR, '/') . '/', $path)) { + return $path; + } else { + return $this->_joinPath($this->root, $path); + } + } + } + + /** + * Return fake path started from root dir + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _path($path) + { + return $this->rootName . ($path == $this->root ? '' : $this->separator . $this->_relpath($path)); + } + + /** + * Return true if $path is children of $parent + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _inpath($path, $parent) + { + $cwd = getcwd(); + $real_path = $this->getFullPath($path, $cwd); + $real_parent = $this->getFullPath($parent, $cwd); + if ($real_path && $real_parent) { + return $real_path === $real_parent || strpos($real_path, rtrim($real_parent, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR) === 0; + } + return false; + } + + + + /***************** file stat ********************/ + + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + protected function _stat($path) + { + $stat = array(); + + if (!file_exists($path) && !is_link($path)) { + return $stat; + } + + //Verifies the given path is the root or is inside the root. Prevents directory traveral. + if (!$this->_inpath($path, $this->root)) { + return $stat; + } + + $stat['isowner'] = false; + $linkreadable = false; + if ($path != $this->root && is_link($path)) { + if (!$this->options['followSymLinks']) { + return array(); + } + if (!($target = $this->readlink($path)) + || $target == $path) { + if (is_null($target)) { + $stat = array(); + return $stat; + } else { + $stat['mime'] = 'symlink-broken'; + $target = readlink($path); + $lstat = lstat($path); + $ostat = $this->getOwnerStat($lstat['uid'], $lstat['gid']); + $linkreadable = !empty($ostat['isowner']); + } + } + $stat['alias'] = $this->_path($target); + $stat['target'] = $target; + } + + $readable = is_readable($path); + + if ($readable) { + $size = sprintf('%u', filesize($path)); + $stat['ts'] = filemtime($path); + if ($this->statOwner) { + $fstat = stat($path); + $uid = $fstat['uid']; + $gid = $fstat['gid']; + $stat['perm'] = substr((string)decoct($fstat['mode']), -4); + $stat = array_merge($stat, $this->getOwnerStat($uid, $gid)); + } + } + + if (($dir = is_dir($path)) && $this->options['detectDirIcon']) { + $favicon = $path . DIRECTORY_SEPARATOR . $this->options['detectDirIcon']; + if ($this->URL && file_exists($favicon)) { + $stat['icon'] = $this->URL . str_replace(DIRECTORY_SEPARATOR, '/', substr($favicon, strlen($this->root) + 1)); + } + } + + if (!isset($stat['mime'])) { + $stat['mime'] = $dir ? 'directory' : $this->mimetype($path); + } + //logical rights first + $stat['read'] = ($linkreadable || $readable) ? null : false; + $stat['write'] = is_writable($path) ? null : false; + + if (is_null($stat['read'])) { + if ($dir) { + $stat['size'] = 0; + } else if (isset($size)) { + $stat['size'] = $size; + } + } + + if ($this->options['statCorrector']) { + call_user_func_array($this->options['statCorrector'], array(&$stat, $path, $this->statOwner, $this)); + } + + return $stat; + } + + /** + * Get stat `owner`, `group` and `isowner` by `uid` and `gid` + * Sub-fuction of _stat() and _scandir() + * + * @param integer $uid + * @param integer $gid + * + * @return array stat + */ + protected function getOwnerStat($uid, $gid) + { + static $names = null; + static $phpuid = null; + + if (is_null($names)) { + $names = array('uid' => array(), 'gid' => array()); + } + if (is_null($phpuid)) { + if (is_callable('posix_getuid')) { + $phpuid = posix_getuid(); + } else { + $phpuid = 0; + } + } + + $stat = array(); + + if ($uid) { + $stat['isowner'] = ($phpuid == $uid); + if (isset($names['uid'][$uid])) { + $stat['owner'] = $names['uid'][$uid]; + } else if (is_callable('posix_getpwuid')) { + $pwuid = posix_getpwuid($uid); + $stat['owner'] = $names['uid'][$uid] = $pwuid['name']; + } else { + $stat['owner'] = $names['uid'][$uid] = $uid; + } + } + if ($gid) { + if (isset($names['gid'][$gid])) { + $stat['group'] = $names['gid'][$gid]; + } else if (is_callable('posix_getgrgid')) { + $grgid = posix_getgrgid($gid); + $stat['group'] = $names['gid'][$gid] = $grgid['name']; + } else { + $stat['group'] = $names['gid'][$gid] = $gid; + } + } + + return $stat; + } + + /** + * Return true if path is dir and has at least one childs directory + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _subdirs($path) + { + + $dirs = false; + if (is_dir($path) && is_readable($path)) { + if (class_exists('FilesystemIterator', false)) { + $dirItr = new ParentIterator( + new RecursiveDirectoryIterator($path, + FilesystemIterator::SKIP_DOTS | + FilesystemIterator::CURRENT_AS_SELF | + (defined('RecursiveDirectoryIterator::FOLLOW_SYMLINKS') ? + RecursiveDirectoryIterator::FOLLOW_SYMLINKS : 0) + ) + ); + $dirItr->rewind(); + if ($dirItr->hasChildren()) { + $dirs = true; + $name = $dirItr->getSubPathName(); + while ($dirItr->valid()) { + if (!$this->attr($path . DIRECTORY_SEPARATOR . $name, 'read', null, true)) { + $dirs = false; + $dirItr->next(); + $name = $dirItr->getSubPathName(); + continue; + } + $dirs = true; + break; + } + } + } else { + $path = strtr($path, array('[' => '\\[', ']' => '\\]', '*' => '\\*', '?' => '\\?')); + return (bool)glob(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '*', GLOB_ONLYDIR); + } + } + return $dirs; + } + + /** + * Return object width and height + * Usualy used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dimensions($path, $mime) + { + clearstatcache(); + return strpos($mime, 'image') === 0 && is_readable($path) && filesize($path) && ($s = getimagesize($path)) !== false + ? $s[0] . 'x' . $s[1] + : false; + } + /******************** file/dir content *********************/ + + /** + * Return symlink target file + * + * @param string $path link path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function readlink($path) + { + if (!($target = readlink($path))) { + return null; + } + + if (strpos($target, $this->systemRoot) !== 0) { + $target = $this->_joinPath(dirname($path), $target); + } + + if (!file_exists($target)) { + return false; + } + + return $target; + } + + /** + * Return files list in directory. + * + * @param string $path dir path + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function _scandir($path) + { + elFinder::checkAborted(); + $files = array(); + $cache = array(); + $dirWritable = is_writable($path); + $dirItr = array(); + $followSymLinks = $this->options['followSymLinks']; + try { + $dirItr = new DirectoryIterator($path); + } catch (UnexpectedValueException $e) { + } + + foreach ($dirItr as $file) { + try { + if ($file->isDot()) { + continue; + } + + $files[] = $fpath = $file->getPathname(); + + $br = false; + $stat = array(); + + $stat['isowner'] = false; + $linkreadable = false; + if ($file->isLink()) { + if (!$followSymLinks) { + continue; + } + if (!($target = $this->readlink($fpath)) + || $target == $fpath) { + if (is_null($target)) { + $stat = array(); + $br = true; + } else { + $_path = $fpath; + $stat['mime'] = 'symlink-broken'; + $target = readlink($_path); + $lstat = lstat($_path); + $ostat = $this->getOwnerStat($lstat['uid'], $lstat['gid']); + $linkreadable = !empty($ostat['isowner']); + $dir = false; + $stat['alias'] = $this->_path($target); + $stat['target'] = $target; + } + } else { + $dir = is_dir($target); + $stat['alias'] = $this->_path($target); + $stat['target'] = $target; + $stat['mime'] = $dir ? 'directory' : $this->mimetype($stat['alias']); + } + } else { + if (($dir = $file->isDir()) && $this->options['detectDirIcon']) { + $path = $file->getPathname(); + $favicon = $path . DIRECTORY_SEPARATOR . $this->options['detectDirIcon']; + if ($this->URL && file_exists($favicon)) { + $stat['icon'] = $this->URL . str_replace(DIRECTORY_SEPARATOR, '/', substr($favicon, strlen($this->root) + 1)); + } + } + $stat['mime'] = $dir ? 'directory' : $this->mimetype($fpath); + } + $size = sprintf('%u', $file->getSize()); + $stat['ts'] = $file->getMTime(); + if (!$br) { + if ($this->statOwner && !$linkreadable) { + $uid = $file->getOwner(); + $gid = $file->getGroup(); + $stat['perm'] = substr((string)decoct($file->getPerms()), -4); + $stat = array_merge($stat, $this->getOwnerStat($uid, $gid)); + } + + //logical rights first + $stat['read'] = ($linkreadable || $file->isReadable()) ? null : false; + $stat['write'] = $file->isWritable() ? null : false; + $stat['locked'] = $dirWritable ? null : true; + + if (is_null($stat['read'])) { + $stat['size'] = $dir ? 0 : $size; + } + + if ($this->options['statCorrector']) { + call_user_func_array($this->options['statCorrector'], array(&$stat, $fpath, $this->statOwner, $this)); + } + } + + $cache[] = array($fpath, $stat); + } catch (RuntimeException $e) { + continue; + } + } + + if ($cache) { + $cache = $this->convEncOut($cache, false); + foreach ($cache as $d) { + $this->updateCache($d[0], $d[1]); + } + } + + return $files; + } + + /** + * Open file and return file pointer + * + * @param string $path file path + * @param string $mode + * + * @return false|resource + * @internal param bool $write open file for writing + * @author Dmitry (dio) Levashov + */ + protected function _fopen($path, $mode = 'rb') + { + return fopen($path, $mode); + } + + /** + * Close opened file + * + * @param resource $fp file pointer + * @param string $path + * + * @return bool + * @author Dmitry (dio) Levashov + */ + protected function _fclose($fp, $path = '') + { + return (is_resource($fp) && fclose($fp)); + } + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed + * + * @param string $path parent dir path + * @param string $name new directory name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkdir($path, $name) + { + $path = $this->_joinPath($path, $name); + + if (mkdir($path)) { + chmod($path, $this->options['dirMode']); + return $path; + } + + return false; + } + + /** + * Create file and return it's path or false on failed + * + * @param string $path parent dir path + * @param string $name new file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkfile($path, $name) + { + $path = $this->_joinPath($path, $name); + + if (($fp = fopen($path, 'w'))) { + fclose($fp); + chmod($path, $this->options['fileMode']); + return $path; + } + return false; + } + + /** + * Create symlink + * + * @param string $source file to link to + * @param string $targetDir folder to create link in + * @param string $name symlink name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _symlink($source, $targetDir, $name) + { + return symlink($source, $this->_joinPath($targetDir, $name)); + } + + /** + * Copy file into another file + * + * @param string $source source file path + * @param string $targetDir target directory path + * @param string $name new file name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _copy($source, $targetDir, $name) + { + $mtime = filemtime($source); + $target = $this->_joinPath($targetDir, $name); + if ($ret = copy($source, $target)) { + isset($this->options['keepTimestamp']['copy']) && $mtime && touch($target, $mtime); + } + return $ret; + } + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param $targetDir + * @param string $name file name + * + * @return bool|string + * @internal param string $target target dir path + * @author Dmitry (dio) Levashov + */ + protected function _move($source, $targetDir, $name) + { + $mtime = filemtime($source); + $target = $this->_joinPath($targetDir, $name); + if ($ret = rename($source, $target) ? $target : false) { + isset($this->options['keepTimestamp']['move']) && $mtime && touch($target, $mtime); + } + return $ret; + } + + /** + * Remove file + * + * @param string $path file path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _unlink($path) + { + return is_file($path) && unlink($path); + } + + /** + * Remove dir + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _rmdir($path) + { + return rmdir($path); + } + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param string $dir target dir path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Dmitry (dio) Levashov + **/ + protected function _save($fp, $dir, $name, $stat) + { + $path = $this->_joinPath($dir, $name); + + $meta = stream_get_meta_data($fp); + $uri = isset($meta['uri']) ? $meta['uri'] : ''; + if ($uri && !preg_match('#^[a-zA-Z0-9]+://#', $uri) && !is_link($uri)) { + fclose($fp); + $mtime = filemtime($uri); + $isCmdPaste = ($this->ARGS['cmd'] === 'paste'); + $isCmdCopy = ($isCmdPaste && empty($this->ARGS['cut'])); + if (($isCmdCopy || !rename($uri, $path)) && !copy($uri, $path)) { + return false; + } + // keep timestamp on upload + if ($mtime && $this->ARGS['cmd'] === 'upload') { + touch($path, isset($this->options['keepTimestamp']['upload']) ? $mtime : time()); + } + } else { + if (file_put_contents($path, $fp, LOCK_EX) === false) { + return false; + } + } + + chmod($path, $this->options['fileMode']); + return $path; + } + + /** + * Get file contents + * + * @param string $path file path + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function _getContents($path) + { + return file_get_contents($path); + } + + /** + * Write a string to a file + * + * @param string $path file path + * @param string $content new file content + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _filePutContents($path, $content) + { + return (file_put_contents($path, $content, LOCK_EX) !== false); + } + + /** + * Detect available archivers + * + * @return void + * @throws elFinderAbortException + */ + protected function _checkArchivers() + { + $this->archivers = $this->getArchivers(); + return; + } + + /** + * chmod availability + * + * @param string $path + * @param string $mode + * + * @return bool + */ + protected function _chmod($path, $mode) + { + $modeOct = is_string($mode) ? octdec($mode) : octdec(sprintf("%04o", $mode)); + return chmod($path, $modeOct); + } + + /** + * Recursive symlinks search + * + * @param string $path file/dir path + * + * @return bool + * @throws Exception + * @author Dmitry (dio) Levashov + */ + protected function _findSymlinks($path) + { + return self::localFindSymlinks($path); + } + + /** + * Extract files from archive + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return array|string|boolean + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + */ + protected function _extract($path, $arc) + { + + if ($this->quarantine) { + + $dir = $this->quarantine . DIRECTORY_SEPARATOR . md5(basename($path) . mt_rand()); + $archive = (isset($arc['toSpec']) || $arc['cmd'] === 'phpfunction') ? '' : $dir . DIRECTORY_SEPARATOR . basename($path); + + if (!mkdir($dir)) { + return false; + } + + // insurance unexpected shutdown + register_shutdown_function(array($this, 'rmdirRecursive'), realpath($dir)); + + chmod($dir, 0777); + + // copy in quarantine + if (!is_readable($path) || ($archive && !copy($path, $archive))) { + return false; + } + + // extract in quarantine + try { + $this->unpackArchive($path, $arc, $archive ? true : $dir); + } catch(Exception $e) { + return $this->setError($e->getMessage()); + } + + // get files list + try { + $ls = self::localScandir($dir); + } catch (Exception $e) { + return false; + } + + // no files - extract error ? + if (empty($ls)) { + return false; + } + + $this->archiveSize = 0; + + // find symlinks and check extracted items + $checkRes = $this->checkExtractItems($dir); + if ($checkRes['symlinks']) { + self::localRmdirRecursive($dir); + return $this->setError(array_merge($this->error, array(elFinder::ERROR_ARC_SYMLINKS))); + } + $this->archiveSize = $checkRes['totalSize']; + if ($checkRes['rmNames']) { + foreach ($checkRes['rmNames'] as $name) { + $this->addError(elFinder::ERROR_SAVE, $name); + } + } + + // check max files size + if ($this->options['maxArcFilesSize'] > 0 && $this->options['maxArcFilesSize'] < $this->archiveSize) { + $this->delTree($dir); + return $this->setError(elFinder::ERROR_ARC_MAXSIZE); + } + + $extractTo = $this->extractToNewdir; // 'auto', ture or false + + // archive contains one item - extract in archive dir + $name = ''; + $src = $dir . DIRECTORY_SEPARATOR . $ls[0]; + if (($extractTo === 'auto' || !$extractTo) && count($ls) === 1 && is_file($src)) { + $name = $ls[0]; + } else if ($extractTo === 'auto' || $extractTo) { + // for several files - create new directory + // create unique name for directory + $src = $dir; + $splits = elFinder::splitFileExtention(basename($path)); + $name = $splits[0]; + $test = dirname($path) . DIRECTORY_SEPARATOR . $name; + if (file_exists($test) || is_link($test)) { + $name = $this->uniqueName(dirname($path), $name, '-', false); + } + } + + if ($name !== '') { + $result = dirname($path) . DIRECTORY_SEPARATOR . $name; + + if (!rename($src, $result)) { + $this->delTree($dir); + return false; + } + } else { + $dstDir = dirname($path); + $result = array(); + foreach ($ls as $name) { + $target = $dstDir . DIRECTORY_SEPARATOR . $name; + if (self::localMoveRecursive($dir . DIRECTORY_SEPARATOR . $name, $target, true, $this->options['copyJoin'])) { + $result[] = $target; + } + } + if (!$result) { + $this->delTree($dir); + return false; + } + } + + is_dir($dir) && $this->delTree($dir); + + return (is_array($result) || file_exists($result)) ? $result : false; + } + //TODO: Add return statement here + return false; + } + + /** + * Create archive and return its path + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + */ + protected function _archive($dir, $files, $name, $arc) + { + return $this->makeArchive($dir, $files, $name, $arc); + } + + /******************** Over write functions *************************/ + + /** + * File path of local server side work file path + * + * @param string $path + * + * @return string + * @author Naoki Sawada + */ + protected function getWorkFile($path) + { + return $path; + } + + /** + * Delete dirctory trees + * + * @param string $localpath path need convert encoding to server encoding + * + * @return boolean + * @throws elFinderAbortException + * @author Naoki Sawada + */ + protected function delTree($localpath) + { + return $this->rmdirRecursive($localpath); + } + + /** + * Return fileinfo based on filename + * For item ID based path file system + * Please override if needed on each drivers + * + * @param string $path file cache + * + * @return array|boolean false + */ + protected function isNameExists($path) + { + $exists = file_exists($this->convEncIn($path)); + // restore locale + $this->convEncOut(); + return $exists ? $this->stat($path) : false; + } + + /******************** Over write (Optimized) functions *************************/ + + /** + * Recursive files search + * + * @param string $path dir path + * @param string $q search string + * @param array $mimes + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function doSearch($path, $q, $mimes) + { + if (!empty($this->doSearchCurrentQuery['matchMethod']) || $this->encoding || !class_exists('FilesystemIterator', false)) { + // has custom match method or non UTF-8, use elFinderVolumeDriver::doSearch() + return parent::doSearch($path, $q, $mimes); + } + + $result = array(); + + $timeout = $this->options['searchTimeout'] ? $this->searchStart + $this->options['searchTimeout'] : 0; + if ($timeout && $timeout < time()) { + $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->path($this->encode($path))); + return $result; + } + elFinder::extendTimeLimit($this->options['searchTimeout'] + 30); + + $match = array(); + try { + $iterator = new RecursiveIteratorIterator( + new RecursiveCallbackFilterIterator( + new RecursiveDirectoryIterator($path, + FilesystemIterator::KEY_AS_PATHNAME | + FilesystemIterator::SKIP_DOTS | + ((defined('RecursiveDirectoryIterator::FOLLOW_SYMLINKS') && $this->options['followSymLinks']) ? + RecursiveDirectoryIterator::FOLLOW_SYMLINKS : 0) + ), + array($this, 'localFileSystemSearchIteratorFilter') + ), + RecursiveIteratorIterator::SELF_FIRST, + RecursiveIteratorIterator::CATCH_GET_CHILD + ); + foreach ($iterator as $key => $node) { + if ($timeout && ($this->error || $timeout < time())) { + !$this->error && $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->path($this->encode($node->getPath))); + break; + } + if ($node->isDir()) { + if ($this->stripos($node->getFilename(), $q) !== false) { + $match[] = $key; + } + } else { + $match[] = $key; + } + } + } catch (Exception $e) { + } + + if ($match) { + foreach ($match as $p) { + if ($timeout && ($this->error || $timeout < time())) { + !$this->error && $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->path($this->encode(dirname($p)))); + break; + } + + $stat = $this->stat($p); + + if (!$stat) { // invalid links + continue; + } + + if (!empty($stat['hidden']) || !$this->mimeAccepted($stat['mime'], $mimes)) { + continue; + } + + if ((!$mimes || $stat['mime'] !== 'directory')) { + $stat['path'] = $this->path($stat['hash']); + if ($this->URL && !isset($stat['url'])) { + $_path = str_replace(DIRECTORY_SEPARATOR, '/', substr($p, strlen($this->root) + 1)); + $stat['url'] = $this->URL . str_replace('%2F', '/', rawurlencode($_path)); + } + + $result[] = $stat; + } + } + } + + return $result; + } + + /******************** Original local functions ************************ + * + * @param $file + * @param $key + * @param $iterator + * + * @return bool + */ + + public function localFileSystemSearchIteratorFilter($file, $key, $iterator) + { + /* @var FilesystemIterator $file */ + /* @var RecursiveDirectoryIterator $iterator */ + $name = $file->getFilename(); + if ($this->doSearchCurrentQuery['excludes']) { + foreach ($this->doSearchCurrentQuery['excludes'] as $exclude) { + if ($this->stripos($name, $exclude) !== false) { + return false; + } + } + } + if ($iterator->hasChildren()) { + if ($this->options['searchExDirReg'] && preg_match($this->options['searchExDirReg'], $key)) { + return false; + } + return (bool)$this->attr($key, 'read', null, true); + } + return ($this->stripos($name, $this->doSearchCurrentQuery['q']) === false) ? false : true; + } + + /** + * Creates a symbolic link + * + * @param string $target The target + * @param string $link The link + * + * @return boolean ( result of symlink() ) + */ + protected function localFileSystemSymlink($target, $link) + { + $res = false; + $errlev = error_reporting(); + error_reporting($errlev ^ E_WARNING); + if ($res = symlink(realpath($target), $link)) { + $res = is_readable($link); + } + error_reporting($errlev); + return $res; + } +} // END class + diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeMySQL.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeMySQL.class.php new file mode 100644 index 0000000..6ced886 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeMySQL.class.php @@ -0,0 +1,1022 @@ + 'localhost', + 'user' => '', + 'pass' => '', + 'db' => '', + 'port' => null, + 'socket' => null, + 'files_table' => 'elfinder_file', + 'tmbPath' => '', + 'tmpPath' => '', + 'rootCssClass' => 'elfinder-navbar-root-sql', + 'noSessionCache' => array('hasdirs'), + 'isLocalhost' => false + ); + $this->options = array_merge($this->options, $opts); + $this->options['mimeDetect'] = 'internal'; + } + + /*********************************************************************/ + /* INIT AND CONFIGURE */ + /*********************************************************************/ + + /** + * Prepare driver before mount volume. + * Connect to db, check required tables and fetch root path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function init() + { + + if (!($this->options['host'] || $this->options['socket']) + || !$this->options['user'] + || !$this->options['pass'] + || !$this->options['db'] + || !$this->options['path'] + || !$this->options['files_table']) { + return $this->setError('Required options "host", "socket", "user", "pass", "db", "path" or "files_table" are undefined.'); + } + + $err = null; + if ($this->db = @new mysqli($this->options['host'], $this->options['user'], $this->options['pass'], $this->options['db'], $this->options['port'], $this->options['socket'])) { + if ($this->db && $this->db->connect_error) { + $err = $this->db->connect_error; + } + } else { + $err = mysqli_connect_error(); + } + if ($err) { + return $this->setError(array('Unable to connect to MySQL server.', $err)); + } + + if (!$this->needOnline && empty($this->ARGS['init'])) { + $this->db->close(); + $this->db = null; + return true; + } + + $this->db->set_charset('utf8'); + + if ($res = $this->db->query('SHOW TABLES')) { + while ($row = $res->fetch_array()) { + if ($row[0] == $this->options['files_table']) { + $this->tbf = $this->options['files_table']; + break; + } + } + } + + if (!$this->tbf) { + return $this->setError('The specified database table cannot be found.'); + } + + $this->updateCache($this->options['path'], $this->_stat($this->options['path'])); + + // enable command archive + $this->options['useRemoteArchive'] = true; + + // check isLocalhost + $this->isLocalhost = $this->options['isLocalhost'] || $this->options['host'] === 'localhost' || $this->options['host'] === '127.0.0.1' || $this->options['host'] === '::1'; + + return true; + } + + + /** + * Set tmp path + * + * @return void + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function configure() + { + parent::configure(); + + if (($tmp = $this->options['tmpPath'])) { + if (!file_exists($tmp)) { + if (mkdir($tmp)) { + chmod($tmp, $this->options['tmbPathMode']); + } + } + + $this->tmpPath = is_dir($tmp) && is_writable($tmp) ? $tmp : false; + } + if (!$this->tmpPath && ($tmp = elFinder::getStaticVar('commonTempPath'))) { + $this->tmpPath = $tmp; + } + + // fallback of $this->tmp + if (!$this->tmpPath && $this->tmbPathWritable) { + $this->tmpPath = $this->tmbPath; + } + + $this->mimeDetect = 'internal'; + } + + /** + * Close connection + * + * @return void + * @author Dmitry (dio) Levashov + **/ + public function umount() + { + $this->db && $this->db->close(); + } + + /** + * Return debug info for client + * + * @return array + * @author Dmitry (dio) Levashov + **/ + public function debug() + { + $debug = parent::debug(); + $debug['sqlCount'] = $this->sqlCnt; + if ($this->dbError) { + $debug['dbError'] = $this->dbError; + } + return $debug; + } + + /** + * Perform sql query and return result. + * Increase sqlCnt and save error if occured + * + * @param string $sql query + * + * @return bool|mysqli_result + * @author Dmitry (dio) Levashov + */ + protected function query($sql) + { + $this->sqlCnt++; + $res = $this->db->query($sql); + if (!$res) { + $this->dbError = $this->db->error; + } + return $res; + } + + /** + * Create empty object with required mimetype + * + * @param string $path parent dir path + * @param string $name object name + * @param string $mime mime type + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function make($path, $name, $mime) + { + $sql = 'INSERT INTO %s (`parent_id`, `name`, `size`, `mtime`, `mime`, `content`, `read`, `write`, `locked`, `hidden`, `width`, `height`) VALUES (\'%s\', \'%s\', 0, %d, \'%s\', \'\', \'%d\', \'%d\', \'%d\', \'%d\', 0, 0)'; + $sql = sprintf($sql, $this->tbf, $path, $this->db->real_escape_string($name), time(), $mime, $this->defaults['read'], $this->defaults['write'], $this->defaults['locked'], $this->defaults['hidden']); + // echo $sql; + return $this->query($sql) && $this->db->affected_rows > 0; + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /** + * Cache dir contents + * + * @param string $path dir path + * + * @return string + * @author Dmitry Levashov + **/ + protected function cacheDir($path) + { + $this->dirsCache[$path] = array(); + + $sql = 'SELECT f.id, f.parent_id, f.name, f.size, f.mtime AS ts, f.mime, f.read, f.write, f.locked, f.hidden, f.width, f.height, IF(ch.id, 1, 0) AS dirs + FROM ' . $this->tbf . ' AS f + LEFT JOIN ' . $this->tbf . ' AS ch ON ch.parent_id=f.id AND ch.mime=\'directory\' + WHERE f.parent_id=\'' . $path . '\' + GROUP BY f.id, ch.id'; + + $res = $this->query($sql); + if ($res) { + while ($row = $res->fetch_assoc()) { + $id = $row['id']; + if ($row['parent_id'] && $id != $this->root) { + $row['phash'] = $this->encode($row['parent_id']); + } + + if ($row['mime'] == 'directory') { + unset($row['width']); + unset($row['height']); + $row['size'] = 0; + } else { + unset($row['dirs']); + } + + unset($row['id']); + unset($row['parent_id']); + + + if (($stat = $this->updateCache($id, $row)) && empty($stat['hidden'])) { + $this->dirsCache[$path][] = $id; + } + } + } + + return $this->dirsCache[$path]; + } + + /** + * Return array of parents paths (ids) + * + * @param int $path file path (id) + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function getParents($path) + { + $parents = array(); + + while ($path) { + if ($file = $this->stat($path)) { + array_unshift($parents, $path); + $path = isset($file['phash']) ? $this->decode($file['phash']) : false; + } + } + + if (count($parents)) { + array_pop($parents); + } + return $parents; + } + + /** + * Return correct file path for LOAD_FILE method + * + * @param string $path file path (id) + * + * @return string + * @author Troex Nevelin + **/ + protected function loadFilePath($path) + { + $realPath = realpath($path); + if (DIRECTORY_SEPARATOR == '\\') { // windows + $realPath = str_replace('\\', '\\\\', $realPath); + } + return $this->db->real_escape_string($realPath); + } + + /** + * Recursive files search + * + * @param string $path dir path + * @param string $q search string + * @param array $mimes + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function doSearch($path, $q, $mimes) + { + if (!empty($this->doSearchCurrentQuery['matchMethod'])) { + // has custom match method use elFinderVolumeDriver::doSearch() + return parent::doSearch($path, $q, $mimes); + } + + $dirs = array(); + $timeout = $this->options['searchTimeout'] ? $this->searchStart + $this->options['searchTimeout'] : 0; + + if ($path != $this->root || $this->rootHasParent) { + $dirs = $inpath = array(intval($path)); + while ($inpath) { + $in = '(' . join(',', $inpath) . ')'; + $inpath = array(); + $sql = 'SELECT f.id FROM %s AS f WHERE f.parent_id IN ' . $in . ' AND `mime` = \'directory\''; + $sql = sprintf($sql, $this->tbf); + if ($res = $this->query($sql)) { + $_dir = array(); + while ($dat = $res->fetch_assoc()) { + $inpath[] = $dat['id']; + } + $dirs = array_merge($dirs, $inpath); + } + } + } + + $result = array(); + + if ($mimes) { + $whrs = array(); + foreach ($mimes as $mime) { + if (strpos($mime, '/') === false) { + $whrs[] = sprintf('f.mime LIKE \'%s/%%\'', $this->db->real_escape_string($mime)); + } else { + $whrs[] = sprintf('f.mime = \'%s\'', $this->db->real_escape_string($mime)); + } + } + $whr = join(' OR ', $whrs); + } else { + $whr = sprintf('f.name LIKE \'%%%s%%\'', $this->db->real_escape_string($q)); + } + if ($dirs) { + $whr = '(' . $whr . ') AND (`parent_id` IN (' . join(',', $dirs) . '))'; + } + + $sql = 'SELECT f.id, f.parent_id, f.name, f.size, f.mtime AS ts, f.mime, f.read, f.write, f.locked, f.hidden, f.width, f.height, 0 AS dirs + FROM %s AS f + WHERE %s'; + + $sql = sprintf($sql, $this->tbf, $whr); + + if (($res = $this->query($sql))) { + while ($row = $res->fetch_assoc()) { + if ($timeout && $timeout < time()) { + $this->setError(elFinder::ERROR_SEARCH_TIMEOUT, $this->path($this->encode($path))); + break; + } + + if (!$this->mimeAccepted($row['mime'], $mimes)) { + continue; + } + $id = $row['id']; + if ($id == $this->root) { + continue; + } + if ($row['parent_id'] && $id != $this->root) { + $row['phash'] = $this->encode($row['parent_id']); + } + $row['path'] = $this->_path($id); + + if ($row['mime'] == 'directory') { + unset($row['width']); + unset($row['height']); + } else { + unset($row['dirs']); + } + + unset($row['id']); + unset($row['parent_id']); + + if (($stat = $this->updateCache($id, $row)) && empty($stat['hidden'])) { + $result[] = $stat; + } + } + } + return $result; + } + + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dirname($path) + { + return ($stat = $this->stat($path)) ? (!empty($stat['phash']) ? $this->decode($stat['phash']) : $this->root) : false; + } + + /** + * Return file name + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _basename($path) + { + return (($stat = $this->stat($path)) && isset($stat['name'])) ? $stat['name'] : false; + } + + /** + * Join dir name and file name and return full path + * + * @param string $dir + * @param string $name + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _joinPath($dir, $name) + { + $sql = 'SELECT id FROM ' . $this->tbf . ' WHERE parent_id=\'' . $dir . '\' AND name=\'' . $this->db->real_escape_string($name) . '\''; + + if (($res = $this->query($sql)) && ($r = $res->fetch_assoc())) { + $this->updateCache($r['id'], $this->_stat($r['id'])); + return $r['id']; + } + return -1; + } + + /** + * Return normalized path, this works the same as os.path.normpath() in Python + * + * @param string $path path + * + * @return string + * @author Troex Nevelin + **/ + protected function _normpath($path) + { + return $path; + } + + /** + * Return file path related to root dir + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _relpath($path) + { + return $path; + } + + /** + * Convert path related to root dir into real path + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _abspath($path) + { + return $path; + } + + /** + * Return fake path started from root dir + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _path($path) + { + if (($file = $this->stat($path)) == false) { + return ''; + } + + $parentsIds = $this->getParents($path); + $path = ''; + foreach ($parentsIds as $id) { + $dir = $this->stat($id); + $path .= $dir['name'] . $this->separator; + } + return $path . $file['name']; + } + + /** + * Return true if $path is children of $parent + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _inpath($path, $parent) + { + return $path == $parent + ? true + : in_array($parent, $this->getParents($path)); + } + + /***************** file stat ********************/ + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + protected function _stat($path) + { + $sql = 'SELECT f.id, f.parent_id, f.name, f.size, f.mtime AS ts, f.mime, f.read, f.write, f.locked, f.hidden, f.width, f.height, IF(ch.id, 1, 0) AS dirs + FROM ' . $this->tbf . ' AS f + LEFT JOIN ' . $this->tbf . ' AS ch ON ch.parent_id=f.id AND ch.mime=\'directory\' + WHERE f.id=\'' . $path . '\' + GROUP BY f.id, ch.id'; + + $res = $this->query($sql); + + if ($res) { + $stat = $res->fetch_assoc(); + if ($stat['id'] == $this->root) { + $this->rootHasParent = true; + $stat['parent_id'] = ''; + } + if ($stat['parent_id']) { + $stat['phash'] = $this->encode($stat['parent_id']); + } + if ($stat['mime'] == 'directory') { + unset($stat['width']); + unset($stat['height']); + $stat['size'] = 0; + } else { + if (!$stat['mime']) { + unset($stat['mime']); + } + unset($stat['dirs']); + } + unset($stat['id']); + unset($stat['parent_id']); + return $stat; + + } + return array(); + } + + /** + * Return true if path is dir and has at least one childs directory + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _subdirs($path) + { + return ($stat = $this->stat($path)) && isset($stat['dirs']) ? $stat['dirs'] : false; + } + + /** + * Return object width and height + * Usualy used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dimensions($path, $mime) + { + return ($stat = $this->stat($path)) && isset($stat['width']) && isset($stat['height']) ? $stat['width'] . 'x' . $stat['height'] : ''; + } + + /******************** file/dir content *********************/ + + /** + * Return files list in directory. + * + * @param string $path dir path + * + * @return array + * @author Dmitry (dio) Levashov + **/ + protected function _scandir($path) + { + return isset($this->dirsCache[$path]) + ? $this->dirsCache[$path] + : $this->cacheDir($path); + } + + /** + * Open file and return file pointer + * + * @param string $path file path + * @param string $mode open file mode (ignored in this driver) + * + * @return resource|false + * @author Dmitry (dio) Levashov + **/ + protected function _fopen($path, $mode = 'rb') + { + $fp = $this->tmpPath + ? fopen($this->getTempFile($path), 'w+') + : $this->tmpfile(); + + + if ($fp) { + if (($res = $this->query('SELECT content FROM ' . $this->tbf . ' WHERE id=\'' . $path . '\'')) + && ($r = $res->fetch_assoc())) { + fwrite($fp, $r['content']); + rewind($fp); + return $fp; + } else { + $this->_fclose($fp, $path); + } + } + + return false; + } + + /** + * Close opened file + * + * @param resource $fp file pointer + * @param string $path + * + * @return void + * @author Dmitry (dio) Levashov + */ + protected function _fclose($fp, $path = '') + { + is_resource($fp) && fclose($fp); + if ($path) { + $file = $this->getTempFile($path); + is_file($file) && unlink($file); + } + } + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed + * + * @param string $path parent dir path + * @param string $name new directory name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkdir($path, $name) + { + return $this->make($path, $name, 'directory') ? $this->_joinPath($path, $name) : false; + } + + /** + * Create file and return it's path or false on failed + * + * @param string $path parent dir path + * @param string $name new file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkfile($path, $name) + { + return $this->make($path, $name, '') ? $this->_joinPath($path, $name) : false; + } + + /** + * Create symlink. FTP driver does not support symlinks. + * + * @param string $target link target + * @param string $path symlink path + * @param string $name + * + * @return bool + * @author Dmitry (dio) Levashov + */ + protected function _symlink($target, $path, $name) + { + return false; + } + + /** + * Copy file into another file + * + * @param string $source source file path + * @param string $targetDir target directory path + * @param string $name new file name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _copy($source, $targetDir, $name) + { + $this->clearcache(); + $id = $this->_joinPath($targetDir, $name); + + $sql = $id > 0 + ? sprintf('REPLACE INTO %s (id, parent_id, name, content, size, mtime, mime, width, height, `read`, `write`, `locked`, `hidden`) (SELECT %d, %d, name, content, size, mtime, mime, width, height, `read`, `write`, `locked`, `hidden` FROM %s WHERE id=%d)', $this->tbf, $id, $this->_dirname($id), $this->tbf, $source) + : sprintf('INSERT INTO %s (parent_id, name, content, size, mtime, mime, width, height, `read`, `write`, `locked`, `hidden`) SELECT %d, \'%s\', content, size, %d, mime, width, height, `read`, `write`, `locked`, `hidden` FROM %s WHERE id=%d', $this->tbf, $targetDir, $this->db->real_escape_string($name), time(), $this->tbf, $source); + + return $this->query($sql); + } + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param $targetDir + * @param string $name file name + * + * @return bool|string + * @internal param string $target target dir path + * @author Dmitry (dio) Levashov + */ + protected function _move($source, $targetDir, $name) + { + $sql = 'UPDATE %s SET parent_id=%d, name=\'%s\' WHERE id=%d LIMIT 1'; + $sql = sprintf($sql, $this->tbf, $targetDir, $this->db->real_escape_string($name), $source); + return $this->query($sql) && $this->db->affected_rows > 0 ? $source : false; + } + + /** + * Remove file + * + * @param string $path file path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _unlink($path) + { + return $this->query(sprintf('DELETE FROM %s WHERE id=%d AND mime!=\'directory\' LIMIT 1', $this->tbf, $path)) && $this->db->affected_rows; + } + + /** + * Remove dir + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _rmdir($path) + { + return $this->query(sprintf('DELETE FROM %s WHERE id=%d AND mime=\'directory\' LIMIT 1', $this->tbf, $path)) && $this->db->affected_rows; + } + + /** + * undocumented function + * + * @param $path + * @param $fp + * + * @author Dmitry Levashov + */ + protected function _setContent($path, $fp) + { + elFinder::rewind($fp); + $fstat = fstat($fp); + $size = $fstat['size']; + + + } + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param string $dir target dir path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Dmitry (dio) Levashov + **/ + protected function _save($fp, $dir, $name, $stat) + { + $this->clearcache(); + + $mime = !empty($stat['mime']) ? $stat['mime'] : $this->mimetype($name, true); + $w = !empty($stat['width']) ? $stat['width'] : 0; + $h = !empty($stat['height']) ? $stat['height'] : 0; + $ts = !empty($stat['ts']) ? $stat['ts'] : time(); + + $id = $this->_joinPath($dir, $name); + if (!isset($stat['size'])) { + $stat = fstat($fp); + $size = $stat['size']; + } else { + $size = $stat['size']; + } + + if ($this->isLocalhost && ($tmpfile = tempnam($this->tmpPath, $this->id))) { + if (($trgfp = fopen($tmpfile, 'wb')) == false) { + unlink($tmpfile); + } else { + elFinder::rewind($fp); + stream_copy_to_stream($fp, $trgfp); + fclose($trgfp); + chmod($tmpfile, 0644); + + $sql = $id > 0 + ? 'REPLACE INTO %s (id, parent_id, name, content, size, mtime, mime, width, height) VALUES (' . $id . ', %d, \'%s\', LOAD_FILE(\'%s\'), %d, %d, \'%s\', %d, %d)' + : 'INSERT INTO %s (parent_id, name, content, size, mtime, mime, width, height) VALUES (%d, \'%s\', LOAD_FILE(\'%s\'), %d, %d, \'%s\', %d, %d)'; + $sql = sprintf($sql, $this->tbf, $dir, $this->db->real_escape_string($name), $this->loadFilePath($tmpfile), $size, $ts, $mime, $w, $h); + + $res = $this->query($sql); + unlink($tmpfile); + + if ($res) { + return $id > 0 ? $id : $this->db->insert_id; + } + } + } + + + $content = ''; + elFinder::rewind($fp); + while (!feof($fp)) { + $content .= fread($fp, 8192); + } + + $sql = $id > 0 + ? 'REPLACE INTO %s (id, parent_id, name, content, size, mtime, mime, width, height) VALUES (' . $id . ', %d, \'%s\', \'%s\', %d, %d, \'%s\', %d, %d)' + : 'INSERT INTO %s (parent_id, name, content, size, mtime, mime, width, height) VALUES (%d, \'%s\', \'%s\', %d, %d, \'%s\', %d, %d)'; + $sql = sprintf($sql, $this->tbf, $dir, $this->db->real_escape_string($name), $this->db->real_escape_string($content), $size, $ts, $mime, $w, $h); + + unset($content); + + if ($this->query($sql)) { + return $id > 0 ? $id : $this->db->insert_id; + } + + return false; + } + + /** + * Get file contents + * + * @param string $path file path + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function _getContents($path) + { + return ($res = $this->query(sprintf('SELECT content FROM %s WHERE id=%d', $this->tbf, $path))) && ($r = $res->fetch_assoc()) ? $r['content'] : false; + } + + /** + * Write a string to a file + * + * @param string $path file path + * @param string $content new file content + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _filePutContents($path, $content) + { + return $this->query(sprintf('UPDATE %s SET content=\'%s\', size=%d, mtime=%d WHERE id=%d LIMIT 1', $this->tbf, $this->db->real_escape_string($content), strlen($content), time(), $path)); + } + + /** + * Detect available archivers + * + * @return void + **/ + protected function _checkArchivers() + { + return; + } + + /** + * chmod implementation + * + * @param string $path + * @param string $mode + * + * @return bool + */ + protected function _chmod($path, $mode) + { + return false; + } + + /** + * Unpack archive + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return void + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + **/ + protected function _unpack($path, $arc) + { + return; + } + + /** + * Extract files from archive + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return true + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _extract($path, $arc) + { + return false; + } + + /** + * Create archive and return its path + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _archive($dir, $files, $name, $arc) + { + return false; + } + +} // END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeOneDrive.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeOneDrive.class.php new file mode 100644 index 0000000..4b208b1 --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeOneDrive.class.php @@ -0,0 +1,2152 @@ + '', + 'client_secret' => '', + 'accessToken' => '', + 'root' => 'OneDrive.com', + 'OneDriveApiClient' => '', + 'path' => '/', + 'separator' => '/', + 'tmbPath' => '', + 'tmbURL' => '', + 'tmpPath' => '', + 'acceptedName' => '#^[^/\\?*:|"<>]*[^./\\?*:|"<>]$#', + 'rootCssClass' => 'elfinder-navbar-root-onedrive', + 'useApiThumbnail' => true, + ); + $this->options = array_merge($this->options, $opts); + $this->options['mimeDetect'] = 'internal'; + } + + /*********************************************************************/ + /* ORIGINAL FUNCTIONS */ + /*********************************************************************/ + + /** + * Obtains a new access token from OAuth. This token is valid for one hour. + * + * @param $client_id + * @param $client_secret + * @param string $code The code returned by OneDrive after + * successful log in + * + * @return object|string + * @throws Exception Thrown if the redirect URI of this Client instance's + * state is not set + */ + protected function _od_obtainAccessToken($client_id, $client_secret, $code, $nodeid) + { + if (null === $client_id) { + return 'The client ID must be set to call obtainAccessToken()'; + } + + if (null === $client_secret) { + return 'The client Secret must be set to call obtainAccessToken()'; + } + + $redirect = elFinder::getConnectorUrl(); + if (strpos($redirect, '/netmount/onedrive/') === false) { + $redirect .= '/netmount/onedrive/' . ($nodeid === 'elfinder'? '1' : $nodeid); + } + + $url = self::TOKEN_URL; + + $curl = curl_init(); + + $fields = http_build_query( + array( + 'client_id' => $client_id, + 'redirect_uri' => $redirect, + 'client_secret' => $client_secret, + 'code' => $code, + 'grant_type' => 'authorization_code', + ) + ); + + curl_setopt_array($curl, array( + // General options. + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $fields, + + CURLOPT_HTTPHEADER => array( + 'Content-Length: ' . strlen($fields), + ), + + CURLOPT_URL => $url, + )); + + $result = curl_exec($curl); + + if (false === $result) { + if (curl_errno($curl)) { + throw new \Exception('curl_setopt_array() failed: ' + . curl_error($curl)); + } else { + throw new \Exception('curl_setopt_array(): empty response'); + } + } + curl_close($curl); + + $decoded = json_decode($result); + + if (null === $decoded) { + throw new \Exception('json_decode() failed'); + } + + $res = (object)array( + 'expires' => time() + $decoded->expires_in - 30, + 'initialToken' => '', + 'data' => $decoded + ); + if (!empty($decoded->refresh_token)) { + $res->initialToken = md5($client_id . $decoded->refresh_token); + } + return $res; + } + + /** + * Get token and auto refresh. + * + * @return true + * @throws Exception + */ + protected function _od_refreshToken() + { + if (!property_exists($this->token, 'expires') || $this->token->expires < time()) { + if (!$this->options['client_id']) { + $this->options['client_id'] = ELFINDER_ONEDRIVE_CLIENTID; + } + + if (!$this->options['client_secret']) { + $this->options['client_secret'] = ELFINDER_ONEDRIVE_CLIENTSECRET; + } + + if (empty($this->token->data->refresh_token)) { + throw new \Exception(elFinder::ERROR_REAUTH_REQUIRE); + } else { + $refresh_token = $this->token->data->refresh_token; + $initialToken = $this->_od_getInitialToken(); + } + + $url = self::TOKEN_URL; + + $curl = curl_init(); + + curl_setopt_array($curl, array( + // General options. + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, // i am sending post data + CURLOPT_POSTFIELDS => 'client_id=' . urlencode($this->options['client_id']) + . '&client_secret=' . urlencode($this->options['client_secret']) + . '&grant_type=refresh_token' + . '&refresh_token=' . urlencode($this->token->data->refresh_token), + + CURLOPT_URL => $url, + )); + + $result = curl_exec($curl); + + if (!$result) { + if (curl_errno($curl)) { + throw new \Exception('curl_setopt_array() failed: ' . curl_error($curl)); + } else { + throw new \Exception('curl_setopt_array(): empty response'); + } + } + curl_close($curl); + + $decoded = json_decode($result); + + if (!$decoded) { + throw new \Exception('json_decode() failed'); + } + + if (empty($decoded->access_token)) { + if ($this->aTokenFile) { + if (is_file($this->aTokenFile)) { + unlink($this->aTokenFile); + } + } + $err = property_exists($decoded, 'error')? ' ' . $decoded->error : ''; + $err .= property_exists($decoded, 'error_description')? ' ' . $decoded->error_description : ''; + throw new \Exception($err? $err : elFinder::ERROR_REAUTH_REQUIRE); + } + + $token = (object)array( + 'expires' => time() + $decoded->expires_in - 30, + 'initialToken' => $initialToken, + 'data' => $decoded, + ); + + $this->token = $token; + $json = json_encode($token); + + if (!empty($decoded->refresh_token)) { + if (empty($this->options['netkey']) && $this->aTokenFile) { + file_put_contents($this->aTokenFile, json_encode($token)); + $this->options['accessToken'] = $json; + } else if (!empty($this->options['netkey'])) { + // OAuth2 refresh token can be used only once, + // so update it if it is the same as the token file + $aTokenFile = $this->_od_getATokenFile(); + if ($aTokenFile && is_file($aTokenFile)) { + if ($_token = json_decode(file_get_contents($aTokenFile))) { + if ($_token->data->refresh_token === $refresh_token) { + file_put_contents($aTokenFile, $json); + } + } + } + $this->options['accessToken'] = $json; + // update session value + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'accessToken', $this->options['accessToken']); + $this->session->set('OneDriveTokens', $token); + } else { + throw new \Exception(elFinder::ERROR_CREATING_TEMP_DIR); + } + } + } + + return true; + } + + /** + * Get Parent ID, Item ID, Parent Path as an array from path. + * + * @param string $path + * + * @return array + */ + protected function _od_splitPath($path) + { + $path = trim($path, '/'); + $pid = ''; + if ($path === '') { + $id = 'root'; + $parent = ''; + } else { + $paths = explode('/', trim($path, '/')); + $id = array_pop($paths); + if ($paths) { + $parent = '/' . implode('/', $paths); + $pid = array_pop($paths); + } else { + $pid = 'root'; + $parent = '/'; + } + } + + return array($pid, $id, $parent); + } + + /** + * Creates a base cURL object which is compatible with the OneDrive API. + * + * @return resource A compatible cURL object + */ + protected function _od_prepareCurl($url = null) + { + $curl = curl_init($url); + + $defaultOptions = array( + // General options. + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => array( + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->token->data->access_token, + ), + ); + + curl_setopt_array($curl, $defaultOptions); + + return $curl; + } + + /** + * Creates a base cURL object which is compatible with the OneDrive API. + * + * @param string $path The path of the API call (eg. me/skydrive) + * @param bool $contents + * + * @return resource A compatible cURL object + * @throws elFinderAbortException + */ + protected function _od_createCurl($path, $contents = false) + { + elFinder::checkAborted(); + $curl = $this->_od_prepareCurl($path); + + if ($contents) { + $res = elFinder::curlExec($curl); + } else { + $result = json_decode(curl_exec($curl)); + curl_close($curl); + if (isset($result->value)) { + $res = $result->value; + unset($result->value); + $result = (array)$result; + if (!empty($result['@odata.nextLink'])) { + $nextRes = $this->_od_createCurl($result['@odata.nextLink'], false); + if (is_array($nextRes)) { + $res = array_merge($res, $nextRes); + } + } + } else { + $res = $result; + } + } + + return $res; + } + + /** + * Drive query and fetchAll. + * + * @param $itemId + * @param bool $fetch_self + * @param bool $recursive + * @param array $options + * + * @return object|array + * @throws elFinderAbortException + */ + protected function _od_query($itemId, $fetch_self = false, $recursive = false, $options = array()) + { + $result = array(); + + if (null === $itemId) { + $itemId = 'root'; + } + + if ($fetch_self == true) { + $path = $itemId; + } else { + $path = $itemId . '/children'; + } + + if (isset($options['query'])) { + $path .= '?' . http_build_query($options['query']); + } + + $url = self::API_URL . $path; + + $res = $this->_od_createCurl($url); + if (!$fetch_self && $recursive && is_array($res)) { + foreach ($res as $file) { + $result[] = $file; + if (!empty($file->folder)) { + $result = array_merge($result, $this->_od_query($file->id, false, true, $options)); + } + } + } else { + $result = $res; + } + + return isset($result->error) ? array() : $result; + } + + /** + * Parse line from onedrive metadata output and return file stat (array). + * + * @param object $raw line from ftp_rawlist() output + * + * @return array + * @author Dmitry Levashov + **/ + protected function _od_parseRaw($raw) + { + $stat = array(); + + $folder = isset($raw->folder) ? $raw->folder : null; + + $stat['rev'] = isset($raw->id) ? $raw->id : 'root'; + $stat['name'] = $raw->name; + if (isset($raw->lastModifiedDateTime)) { + $stat['ts'] = strtotime($raw->lastModifiedDateTime); + } + + if ($folder) { + $stat['mime'] = 'directory'; + $stat['size'] = 0; + if (empty($folder->childCount)) { + $stat['dirs'] = 0; + } else { + $stat['dirs'] = -1; + } + } else { + if (isset($raw->file->mimeType)) { + $stat['mime'] = $raw->file->mimeType; + } + $stat['size'] = (int)$raw->size; + if (!$this->disabledGetUrl) { + $stat['url'] = '1'; + } + if (isset($raw->image) && $img = $raw->image) { + isset($img->width) ? $stat['width'] = $img->width : $stat['width'] = 0; + isset($img->height) ? $stat['height'] = $img->height : $stat['height'] = 0; + } + if (!empty($raw->thumbnails)) { + if ($raw->thumbnails[0]->small->url) { + $stat['tmb'] = substr($raw->thumbnails[0]->small->url, 8); // remove "https://" + } + } elseif (!empty($raw->file->processingMetadata)) { + $stat['tmb'] = '1'; + } + } + + return $stat; + } + + /** + * Get raw data(onedrive metadata) from OneDrive. + * + * @param string $path + * + * @return array|object onedrive metadata + */ + protected function _od_getFileRaw($path) + { + list(, $itemId) = $this->_od_splitPath($path); + try { + $res = $this->_od_query($itemId, true, false, $this->queryOptions); + + return $res; + } catch (Exception $e) { + return array(); + } + } + + /** + * Get thumbnail from OneDrive.com. + * + * @param string $path + * + * @return string | boolean + */ + protected function _od_getThumbnail($path) + { + list(, $itemId) = $this->_od_splitPath($path); + + try { + $url = self::API_URL . $itemId . '/thumbnails/0/medium/content'; + + return $this->_od_createCurl($url, $contents = true); + } catch (Exception $e) { + return false; + } + } + + /** + * Upload large files with an upload session. + * + * @param resource $fp source file pointer + * @param number $size total size + * @param string $name item name + * @param string $itemId item identifier + * @param string $parent parent + * @param string $parentId parent identifier + * + * @return string The item path + */ + protected function _od_uploadSession($fp, $size, $name, $itemId, $parent, $parentId) + { + try { + $send = $this->_od_getChunkData($fp); + if ($send === false) { + throw new Exception('Data can not be acquired from the source.'); + } + + // create upload session + if ($itemId) { + $url = self::API_URL . $itemId . '/createUploadSession'; + } else { + $url = self::API_URL . $parentId . ':/' . rawurlencode($name) . ':/createUploadSession'; + } + $curl = $this->_od_prepareCurl($url); + curl_setopt_array($curl, array( + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => '{}', + )); + $sess = json_decode(curl_exec($curl)); + curl_close($curl); + + if ($sess) { + if (isset($sess->error)) { + throw new Exception($sess->error->message); + } + $next = strlen($send); + $range = '0-' . ($next - 1) . '/' . $size; + } else { + throw new Exception('API response can not be obtained.'); + } + + $id = null; + $retry = 0; + while ($sess) { + elFinder::extendTimeLimit(); + $putFp = tmpfile(); + fwrite($putFp, $send); + rewind($putFp); + $_size = strlen($send); + $url = $sess->uploadUrl; + $curl = curl_init(); + $options = array( + CURLOPT_URL => $url, + CURLOPT_PUT => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_INFILE => $putFp, + CURLOPT_INFILESIZE => $_size, + CURLOPT_HTTPHEADER => array( + 'Content-Length: ' . $_size, + 'Content-Range: bytes ' . $range, + ), + ); + curl_setopt_array($curl, $options); + $sess = json_decode(curl_exec($curl)); + curl_close($curl); + if ($sess) { + if (isset($sess->error)) { + throw new Exception($sess->error->message); + } + if (isset($sess->id)) { + $id = $sess->id; + break; + } + if (isset($sess->nextExpectedRanges)) { + list($_next) = explode('-', $sess->nextExpectedRanges[0]); + if ($next == $_next) { + $send = $this->_od_getChunkData($fp); + if ($send === false) { + throw new Exception('Data can not be acquired from the source.'); + } + $next += strlen($send); + $range = $_next . '-' . ($next - 1) . '/' . $size; + $retry = 0; + } else { + if (++$retry > 3) { + throw new Exception('Retry limit exceeded with uploadSession API call.'); + } + } + $sess->uploadUrl = $url; + } + } else { + throw new Exception('API response can not be obtained.'); + } + } + + if ($id) { + return $this->_joinPath($parent, $id); + } else { + throw new Exception('An error occurred in the uploadSession API call.'); + } + } catch (Exception $e) { + return $this->setError('OneDrive error: ' . $e->getMessage()); + } + } + + /** + * Get chunk data by file pointer to upload session. + * + * @param resource $fp source file pointer + * + * @return bool|string chunked data + */ + protected function _od_getChunkData($fp) + { + static $chunkSize = null; + if ($chunkSize === null) { + $mem = elFinder::getIniBytes('memory_limit'); + if ($mem < 1) { + $mem = 10485760; // 10 MiB + } else { + $mem -= memory_get_usage() - 1061548; + $mem = min($mem, 10485760); + } + if ($mem > 327680) { + $chunkSize = floor($mem / 327680) * 327680; + } else { + $chunkSize = $mem; + } + } + if ($chunkSize < 8192) { + return false; + } + + $contents = ''; + while (!feof($fp) && strlen($contents) < $chunkSize) { + $contents .= fread($fp, 8192); + } + + return $contents; + } + + /** + * Get AccessToken file path + * + * @return string ( description_of_the_return_value ) + */ + protected function _od_getATokenFile() + { + $tmp = $aTokenFile = ''; + if (!empty($this->token->data->refresh_token)) { + if (!$this->tmp) { + $tmp = elFinder::getStaticVar('commonTempPath'); + if (!$tmp) { + $tmp = $this->getTempPath(); + } + $this->tmp = $tmp; + } + if ($tmp) { + $aTokenFile = $tmp . DIRECTORY_SEPARATOR . $this->_od_getInitialToken() . '.otoken'; + } + } + return $aTokenFile; + } + + /** + * Get Initial Token (MD5 hash) + * + * @return string + */ + protected function _od_getInitialToken() + { + return (empty($this->token->initialToken)? md5($this->options['client_id'] . (!empty($this->token->data->refresh_token)? $this->token->data->refresh_token : $this->token->data->access_token)) : $this->token->initialToken); + } + + /*********************************************************************/ + /* OVERRIDE FUNCTIONS */ + /*********************************************************************/ + + /** + * Prepare + * Call from elFinder::netmout() before volume->mount(). + * + * @return array + * @author Naoki Sawada + * @author Raja Sharma updating for OneDrive + **/ + public function netmountPrepare($options) + { + if (empty($options['client_id']) && defined('ELFINDER_ONEDRIVE_CLIENTID')) { + $options['client_id'] = ELFINDER_ONEDRIVE_CLIENTID; + } + if (empty($options['client_secret']) && defined('ELFINDER_ONEDRIVE_CLIENTSECRET')) { + $options['client_secret'] = ELFINDER_ONEDRIVE_CLIENTSECRET; + } + + if (isset($options['pass']) && $options['pass'] === 'reauth') { + $options['user'] = 'init'; + $options['pass'] = ''; + $this->session->remove('OneDriveTokens'); + } + + if (isset($options['id'])) { + $this->session->set('nodeId', $options['id']); + } elseif ($_id = $this->session->get('nodeId')) { + $options['id'] = $_id; + $this->session->set('nodeId', $_id); + } + + if (!empty($options['tmpPath'])) { + if ((is_dir($options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($options['tmpPath'])) { + $this->tmp = $options['tmpPath']; + } + } + + try { + if (empty($options['client_id']) || empty($options['client_secret'])) { + return array('exit' => true, 'body' => '{msg:errNetMountNoDriver}'); + } + + $itpCare = isset($options['code']); + $code = $itpCare? $options['code'] : (isset($_GET['code'])? $_GET['code'] : ''); + if ($code) { + try { + if (!empty($options['id'])) { + // Obtain the token using the code received by the OneDrive API + $this->session->set('OneDriveTokens', + $this->_od_obtainAccessToken($options['client_id'], $options['client_secret'], $code, $options['id'])); + + $out = array( + 'node' => $options['id'], + 'json' => '{"protocol": "onedrive", "mode": "done", "reset": 1}', + 'bind' => 'netmount', + ); + } else { + $nodeid = ($_GET['host'] === '1')? 'elfinder' : $_GET['host']; + $out = array( + 'node' => $nodeid, + 'json' => json_encode(array( + 'protocol' => 'onedrive', + 'host' => $nodeid, + 'mode' => 'redirect', + 'options' => array( + 'id' => $nodeid, + 'code'=> $code + ) + )), + 'bind' => 'netmount' + ); + } + if (!$itpCare) { + return array('exit' => 'callback', 'out' => $out); + } else { + return array('exit' => true, 'body' => $out['json']); + } + } catch (Exception $e) { + $out = array( + 'node' => $options['id'], + 'json' => json_encode(array('error' => elFinder::ERROR_ACCESS_DENIED . ' ' . $e->getMessage())), + ); + + return array('exit' => 'callback', 'out' => $out); + } + } elseif (!empty($_GET['error'])) { + $out = array( + 'node' => $options['id'], + 'json' => json_encode(array('error' => elFinder::ERROR_ACCESS_DENIED)), + ); + + return array('exit' => 'callback', 'out' => $out); + } + + if ($options['user'] === 'init') { + $this->token = $this->session->get('OneDriveTokens'); + + if ($this->token) { + try { + $this->_od_refreshToken(); + } catch (Exception $e) { + $this->setError($e->getMessage()); + $this->token = null; + $this->session->remove('OneDriveTokens'); + } + } + + if (empty($this->token)) { + $result = false; + } else { + $path = $options['path']; + if ($path === '/') { + $path = 'root'; + } + $result = $this->_od_query($path, false, false, array( + 'query' => array( + 'select' => 'id,name', + 'filter' => 'folder ne null', + ), + )); + } + + if ($result === false) { + try { + $this->session->set('OneDriveTokens', (object)array('token' => null)); + + $offline = ''; + // Gets a log in URL with sufficient privileges from the OneDrive API + if (!empty($options['offline'])) { + $offline = ' offline_access'; + } + + $redirect_uri = elFinder::getConnectorUrl() . '/netmount/onedrive/' . ($options['id'] === 'elfinder'? '1' : $options['id']); + $url = self::AUTH_URL + . '?client_id=' . urlencode($options['client_id']) + . '&scope=' . urlencode('files.readwrite.all' . $offline) + . '&response_type=code' + . '&redirect_uri=' . urlencode($redirect_uri); + + } catch (Exception $e) { + return array('exit' => true, 'body' => '{msg:errAccess}'); + } + + $html = ''; + $html .= ''; + + return array('exit' => true, 'body' => $html); + } else { + $folders = []; + + if ($result) { + foreach ($result as $res) { + $folders[$res->id] = $res->name; + } + natcasesort($folders); + } + + if ($options['pass'] === 'folders') { + return ['exit' => true, 'folders' => $folders]; + } + + $folders = ['root' => 'My OneDrive'] + $folders; + $folders = json_encode($folders); + + $expires = empty($this->token->data->refresh_token) ? (int)$this->token->expires : 0; + $mnt2res = empty($this->token->data->refresh_token) ? '' : ', "mnt2res": 1'; + $json = '{"protocol": "onedrive", "mode": "done", "folders": ' . $folders . ', "expires": ' . $expires . $mnt2res .'}'; + $html = 'OneDrive.com'; + $html .= ''; + + return array('exit' => true, 'body' => $html); + } + } + } catch (Exception $e) { + return array('exit' => true, 'body' => '{msg:errNetMountNoDriver}'); + } + + if ($_aToken = $this->session->get('OneDriveTokens')) { + $options['accessToken'] = json_encode($_aToken); + if ($this->options['path'] === 'root' || !$this->options['path']) { + $this->options['path'] = '/'; + } + } else { + $this->session->remove('OneDriveTokens'); + $this->setError(elFinder::ERROR_NETMOUNT, $options['host'], implode(' ', $this->error())); + + return array('exit' => true, 'error' => $this->error()); + } + + $this->session->remove('nodeId'); + unset($options['user'], $options['pass'], $options['id']); + + return $options; + } + + /** + * process of on netunmount + * Drop `onedrive` & rm thumbs. + * + * @param array $options + * + * @return bool + */ + public function netunmount($netVolumes, $key) + { + if (!$this->options['useApiThumbnail'] && ($tmbs = glob(rtrim($this->options['tmbPath'], '\\/') . DIRECTORY_SEPARATOR . $this->tmbPrefix . '*.png'))) { + foreach ($tmbs as $file) { + unlink($file); + } + } + + return true; + } + + /** + * Return debug info for client. + * + * @return array + **/ + public function debug() + { + $res = parent::debug(); + if (!empty($this->options['netkey']) && !empty($this->options['accessToken'])) { + $res['accessToken'] = $this->options['accessToken']; + } + + return $res; + } + + /*********************************************************************/ + /* INIT AND CONFIGURE */ + /*********************************************************************/ + + /** + * Prepare FTP connection + * Connect to remote server and check if credentials are correct, if so, store the connection id in $ftp_conn. + * + * @return bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + */ + protected function init() + { + if (!$this->options['accessToken']) { + return $this->setError('Required option `accessToken` is undefined.'); + } + + if (!empty($this->options['tmpPath'])) { + if ((is_dir($this->options['tmpPath']) || mkdir($this->options['tmpPath'])) && is_writable($this->options['tmpPath'])) { + $this->tmp = $this->options['tmpPath']; + } + } + + $error = false; + try { + $this->token = json_decode($this->options['accessToken']); + if (!is_object($this->token)) { + throw new Exception('Required option `accessToken` is invalid JSON.'); + } + + // make net mount key + if (empty($this->options['netkey'])) { + $this->netMountKey = $this->_od_getInitialToken(); + } else { + $this->netMountKey = $this->options['netkey']; + } + + if ($this->aTokenFile = $this->_od_getATokenFile()) { + if (empty($this->options['netkey'])) { + if ($this->aTokenFile) { + if (is_file($this->aTokenFile)) { + $this->token = json_decode(file_get_contents($this->aTokenFile)); + if (!is_object($this->token)) { + unlink($this->aTokenFile); + throw new Exception('Required option `accessToken` is invalid JSON.'); + } + } else { + file_put_contents($this->aTokenFile, $this->token); + } + } + } else if (is_file($this->aTokenFile)) { + // If the refresh token is the same as the permanent volume + $this->token = json_decode(file_get_contents($this->aTokenFile)); + } + } + + if ($this->needOnline) { + $this->_od_refreshToken(); + + $this->expires = empty($this->token->data->refresh_token) ? (int)$this->token->expires : 0; + } + } catch (Exception $e) { + $this->token = null; + $error = true; + $this->setError($e->getMessage()); + } + + if ($this->netMountKey) { + $this->tmbPrefix = 'onedrive' . base_convert($this->netMountKey, 16, 32); + } + + if ($error) { + if (empty($this->options['netkey']) && $this->tmbPrefix) { + // for delete thumbnail + $this->netunmount(null, null); + } + return false; + } + + // normalize root path + if ($this->options['path'] == 'root') { + $this->options['path'] = '/'; + } + + $this->root = $this->options['path'] = $this->_normpath($this->options['path']); + + $this->options['root'] = ($this->options['root'] == '')? 'OneDrive.com' : $this->options['root']; + + if (empty($this->options['alias'])) { + if ($this->needOnline) { + $this->options['alias'] = ($this->options['path'] === '/') ? $this->options['root'] : + $this->_od_query(basename($this->options['path']), $fetch_self = true)->name . '@OneDrive'; + if (!empty($this->options['netkey'])) { + elFinder::$instance->updateNetVolumeOption($this->options['netkey'], 'alias', $this->options['alias']); + } + } else { + $this->options['alias'] = $this->options['root']; + } + } + + $this->rootName = $this->options['alias']; + + // This driver dose not support `syncChkAsTs` + $this->options['syncChkAsTs'] = false; + + // 'lsPlSleep' minmum 10 sec + $this->options['lsPlSleep'] = max(10, $this->options['lsPlSleep']); + + $this->queryOptions = array( + 'query' => array( + 'select' => 'id,name,lastModifiedDateTime,file,folder,size,image', + ), + ); + + if ($this->options['useApiThumbnail']) { + $this->options['tmbURL'] = 'https://'; + $this->options['tmbPath'] = ''; + $this->queryOptions['query']['expand'] = 'thumbnails(select=small)'; + } + + // enable command archive + $this->options['useRemoteArchive'] = true; + + return true; + } + + /** + * Configure after successfull mount. + * + * @author Dmitry (dio) Levashov + **/ + protected function configure() + { + parent::configure(); + + // fallback of $this->tmp + if (!$this->tmp && $this->tmbPathWritable) { + $this->tmp = $this->tmbPath; + } + } + + /*********************************************************************/ + /* FS API */ + /*********************************************************************/ + + /** + * Close opened connection. + * + * @author Dmitry (dio) Levashov + **/ + public function umount() + { + } + + protected function isNameExists($path) + { + list($pid, $name) = $this->_od_splitPath($path); + + $raw = $this->_od_query($pid . '/children/' . rawurlencode($name), true); + return $raw ? $this->_od_parseRaw($raw) : false; + } + + /** + * Cache dir contents. + * + * @param string $path dir path + * + * @return array + * @throws elFinderAbortException + * @author Dmitry Levashov + */ + protected function cacheDir($path) + { + $this->dirsCache[$path] = array(); + $hasDir = false; + + list(, $itemId) = $this->_od_splitPath($path); + + $res = $this->_od_query($itemId, false, false, $this->queryOptions); + + if ($res) { + foreach ($res as $raw) { + if ($stat = $this->_od_parseRaw($raw)) { + $itemPath = $this->_joinPath($path, $raw->id); + $stat = $this->updateCache($itemPath, $stat); + if (empty($stat['hidden'])) { + if (!$hasDir && $stat['mime'] === 'directory') { + $hasDir = true; + } + $this->dirsCache[$path][] = $itemPath; + } + } + } + } + + if (isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'][$path] = $hasDir; + } + + return $this->dirsCache[$path]; + } + + /** + * Copy file/recursive copy dir only in current volume. + * Return new file path or false. + * + * @param string $src source path + * @param string $dst destination dir path + * @param string $name new file name (optionaly) + * + * @return string|false + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function copy($src, $dst, $name) + { + $itemId = ''; + if ($this->options['copyJoin']) { + $test = $this->joinPathCE($dst, $name); + if ($testStat = $this->isNameExists($test)) { + $this->remove($test); + } + } + + if ($path = $this->_copy($src, $dst, $name)) { + $this->added[] = $this->stat($path); + } else { + $this->setError(elFinder::ERROR_COPY, $this->_path($src)); + } + + return $path; + } + + /** + * Remove file/ recursive remove dir. + * + * @param string $path file path + * @param bool $force try to remove even if file locked + * + * @return bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function remove($path, $force = false) + { + $stat = $this->stat($path); + $stat['realpath'] = $path; + $this->rmTmb($stat); + $this->clearcache(); + + if (empty($stat)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path), elFinder::ERROR_FILE_NOT_FOUND); + } + + if (!$force && !empty($stat['locked'])) { + return $this->setError(elFinder::ERROR_LOCKED, $this->_path($path)); + } + + if ($stat['mime'] == 'directory') { + if (!$this->_rmdir($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } else { + if (!$this->_unlink($path)) { + return $this->setError(elFinder::ERROR_RM, $this->_path($path)); + } + } + + $this->removed[] = $stat; + + return true; + } + + /** + * Create thumnbnail and return it's URL on success. + * + * @param string $path file path + * @param $stat + * + * @return string|false + * @throws ImagickException + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Naoki Sawada + */ + protected function createTmb($path, $stat) + { + if ($this->options['useApiThumbnail']) { + if (func_num_args() > 2) { + list(, , $count) = func_get_args(); + } else { + $count = 0; + } + if ($count < 10) { + if (isset($stat['tmb']) && $stat['tmb'] != '1') { + return $stat['tmb']; + } else { + sleep(2); + elFinder::extendTimeLimit(); + $this->clearcache(); + $stat = $this->stat($path); + + return $this->createTmb($path, $stat, ++$count); + } + } + + return false; + } + if (!$stat || !$this->canCreateTmb($path, $stat)) { + return false; + } + + $name = $this->tmbname($stat); + $tmb = $this->tmbPath . DIRECTORY_SEPARATOR . $name; + + // copy image into tmbPath so some drivers does not store files on local fs + if (!$data = $this->_od_getThumbnail($path)) { + return false; + } + if (!file_put_contents($tmb, $data)) { + return false; + } + + $result = false; + + $tmbSize = $this->tmbSize; + + if (($s = getimagesize($tmb)) == false) { + return false; + } + + /* If image smaller or equal thumbnail size - just fitting to thumbnail square */ + if ($s[0] <= $tmbSize && $s[1] <= $tmbSize) { + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } else { + if ($this->options['tmbCrop']) { + + /* Resize and crop if image bigger than thumbnail */ + if (!(($s[0] > $tmbSize && $s[1] <= $tmbSize) || ($s[0] <= $tmbSize && $s[1] > $tmbSize)) || ($s[0] > $tmbSize && $s[1] > $tmbSize)) { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, false, 'png'); + } + + if (($s = getimagesize($tmb)) != false) { + $x = $s[0] > $tmbSize ? intval(($s[0] - $tmbSize) / 2) : 0; + $y = $s[1] > $tmbSize ? intval(($s[1] - $tmbSize) / 2) : 0; + $result = $this->imgCrop($tmb, $tmbSize, $tmbSize, $x, $y, 'png'); + } + } else { + $result = $this->imgResize($tmb, $tmbSize, $tmbSize, true, true, 'png'); + } + + $result = $this->imgSquareFit($tmb, $tmbSize, $tmbSize, 'center', 'middle', $this->options['tmbBgColor'], 'png'); + } + + if (!$result) { + unlink($tmb); + + return false; + } + + return $name; + } + + /** + * Return thumbnail file name for required file. + * + * @param array $stat file stat + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function tmbname($stat) + { + return $this->tmbPrefix . $stat['rev'] . $stat['ts'] . '.png'; + } + + /** + * Return content URL. + * + * @param string $hash file hash + * @param array $options options + * + * @return string + * @author Naoki Sawada + **/ + public function getContentUrl($hash, $options = array()) + { + if (!empty($options['onetime']) && $this->options['onetimeUrl']) { + return parent::getContentUrl($hash, $options); + } + if (!empty($options['temporary'])) { + // try make temporary file + $url = parent::getContentUrl($hash, $options); + if ($url) { + return $url; + } + } + $res = ''; + if (($file = $this->file($hash)) == false || !$file['url'] || $file['url'] == 1) { + $path = $this->decode($hash); + + list(, $itemId) = $this->_od_splitPath($path); + try { + $url = self::API_URL . $itemId . '/createLink'; + $data = (object)array( + 'type' => 'embed', + 'scope' => 'anonymous', + ); + $curl = $this->_od_prepareCurl($url); + curl_setopt_array($curl, array( + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($data), + )); + + $result = curl_exec($curl); + curl_close($curl); + if ($result) { + $result = json_decode($result); + if (isset($result->link)) { + list(, $res) = explode('?', $result->link->webUrl); + $res = 'https://onedrive.live.com/download.aspx?' . $res; + } + } + } catch (Exception $e) { + $res = ''; + } + } + + return $res; + } + + /*********************** paths/urls *************************/ + + /** + * Return parent directory path. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _dirname($path) + { + list(, , $dirname) = $this->_od_splitPath($path); + + return $dirname; + } + + /** + * Return file name. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _basename($path) + { + list(, $basename) = $this->_od_splitPath($path); + + return $basename; + } + + /** + * Join dir name and file name and retur full path. + * + * @param string $dir + * @param string $name + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _joinPath($dir, $name) + { + if ($dir === 'root') { + $dir = ''; + } + + return $this->_normpath($dir . '/' . $name); + } + + /** + * Return normalized path, this works the same as os.path.normpath() in Python. + * + * @param string $path path + * + * @return string + * @author Troex Nevelin + **/ + protected function _normpath($path) + { + if (DIRECTORY_SEPARATOR !== '/') { + $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + } + $path = '/' . ltrim($path, '/'); + + return $path; + } + + /** + * Return file path related to root dir. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _relpath($path) + { + return $path; + } + + /** + * Convert path related to root dir into real path. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _abspath($path) + { + return $path; + } + + /** + * Return fake path started from root dir. + * + * @param string $path file path + * + * @return string + * @author Dmitry (dio) Levashov + **/ + protected function _path($path) + { + return $this->rootName . $this->_normpath(substr($path, strlen($this->root))); + } + + /** + * Return true if $path is children of $parent. + * + * @param string $path path to check + * @param string $parent parent path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _inpath($path, $parent) + { + return $path == $parent || strpos($path, $parent . '/') === 0; + } + + /***************** file stat ********************/ + /** + * Return stat for given path. + * Stat contains following fields: + * - (int) size file size in b. required + * - (int) ts file modification time in unix time. required + * - (string) mime mimetype. required for folders, others - optionally + * - (bool) read read permissions. required + * - (bool) write write permissions. required + * - (bool) locked is object locked. optionally + * - (bool) hidden is object hidden. optionally + * - (string) alias for symlinks - link target path relative to root path. optionally + * - (string) target for symlinks - link target path. optionally. + * If file does not exists - returns empty array or false. + * + * @param string $path file path + * + * @return array|false + * @author Dmitry (dio) Levashov + **/ + protected function _stat($path) + { + if ($raw = $this->_od_getFileRaw($path)) { + $stat = $this->_od_parseRaw($raw); + if ($path === $this->root) { + $stat['expires'] = $this->expires; + } + return $stat; + } + + return false; + } + + /** + * Return true if path is dir and has at least one childs directory. + * + * @param string $path dir path + * + * @return bool + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function _subdirs($path) + { + list(, $itemId) = $this->_od_splitPath($path); + + return (bool)$this->_od_query($itemId, false, false, array( + 'query' => array( + 'top' => 1, + 'select' => 'id', + 'filter' => 'folder ne null', + ), + )); + } + + /** + * Return object width and height + * Ususaly used for images, but can be realize for video etc... + * + * @param string $path file path + * @param string $mime file mime type + * + * @return string + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + */ + protected function _dimensions($path, $mime) + { + if (strpos($mime, 'image') !== 0) { + return ''; + } + + //$cache = $this->_od_getFileRaw($path); + if (func_num_args() > 2) { + $args = func_get_arg(2); + } else { + $args = array(); + } + if (!empty($args['substitute'])) { + $tmbSize = intval($args['substitute']); + } else { + $tmbSize = null; + } + list(, $itemId) = $this->_od_splitPath($path); + $options = array( + 'query' => array( + 'select' => 'id,image', + ), + ); + if ($tmbSize) { + $tmb = 'c' . $tmbSize . 'x' . $tmbSize; + $options['query']['expand'] = 'thumbnails(select=' . $tmb . ')'; + } + $raw = $this->_od_query($itemId, true, false, $options); + + if ($raw && property_exists($raw, 'image') && $img = $raw->image) { + if (isset($img->width) && isset($img->height)) { + $ret = array('dim' => $img->width . 'x' . $img->height); + if ($tmbSize) { + $srcSize = explode('x', $ret['dim']); + if (min(($tmbSize / $srcSize[0]), ($tmbSize / $srcSize[1])) < 1) { + if (!empty($raw->thumbnails)) { + $tmbArr = (array)$raw->thumbnails[0]; + if (!empty($tmbArr[$tmb]->url)) { + $ret['url'] = $tmbArr[$tmb]->url; + } + } + } + } + + return $ret; + } + } + + $ret = ''; + if ($work = $this->getWorkFile($path)) { + if ($size = @getimagesize($work)) { + $cache['width'] = $size[0]; + $cache['height'] = $size[1]; + $ret = $size[0] . 'x' . $size[1]; + } + } + is_file($work) && @unlink($work); + + return $ret; + } + + /******************** file/dir content *********************/ + + /** + * Return files list in directory. + * + * @param string $path dir path + * + * @return array + * @throws elFinderAbortException + * @author Dmitry (dio) Levashov + * @author Cem (DiscoFever) + */ + protected function _scandir($path) + { + return isset($this->dirsCache[$path]) + ? $this->dirsCache[$path] + : $this->cacheDir($path); + } + + /** + * Open file and return file pointer. + * + * @param string $path file path + * @param bool $write open file for writing + * + * @return resource|false + * @author Dmitry (dio) Levashov + **/ + protected function _fopen($path, $mode = 'rb') + { + if ($mode === 'rb' || $mode === 'r') { + list(, $itemId) = $this->_od_splitPath($path); + $data = array( + 'target' => self::API_URL . $itemId . '/content', + 'headers' => array('Authorization: Bearer ' . $this->token->data->access_token), + ); + + // to support range request + if (func_num_args() > 2) { + $opts = func_get_arg(2); + } else { + $opts = array(); + } + if (!empty($opts['httpheaders'])) { + $data['headers'] = array_merge($opts['httpheaders'], $data['headers']); + } + + return elFinder::getStreamByUrl($data); + } + + return false; + } + + /** + * Close opened file. + * + * @param resource $fp file pointer + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _fclose($fp, $path = '') + { + is_resource($fp) && fclose($fp); + if ($path) { + unlink($this->getTempFile($path)); + } + } + + /******************** file/dir manipulations *************************/ + + /** + * Create dir and return created dir path or false on failed. + * + * @param string $path parent dir path + * @param string $name new directory name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkdir($path, $name) + { + $namePath = $this->_joinPath($path, $name); + list($parentId) = $this->_od_splitPath($namePath); + + try { + $properties = array( + 'name' => (string)$name, + 'folder' => (object)array(), + ); + + $data = (object)$properties; + + $url = self::API_URL . $parentId . '/children'; + + $curl = $this->_od_prepareCurl($url); + + curl_setopt_array($curl, array( + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($data), + )); + + //create the Folder in the Parent + $result = curl_exec($curl); + curl_close($curl); + $folder = json_decode($result); + + return $this->_joinPath($path, $folder->id); + } catch (Exception $e) { + return $this->setError('OneDrive error: ' . $e->getMessage()); + } + } + + /** + * Create file and return it's path or false on failed. + * + * @param string $path parent dir path + * @param string $name new file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + **/ + protected function _mkfile($path, $name) + { + return $this->_save($this->tmpfile(), $path, $name, array()); + } + + /** + * Create symlink. FTP driver does not support symlinks. + * + * @param string $target link target + * @param string $path symlink path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _symlink($target, $path, $name) + { + return false; + } + + /** + * Copy file into another file. + * + * @param string $source source file path + * @param string $targetDir target directory path + * @param string $name new file name + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _copy($source, $targetDir, $name) + { + $path = $this->_joinPath($targetDir, $name); + + try { + //Set the Parent id + list(, $parentId) = $this->_od_splitPath($targetDir); + list(, $itemId) = $this->_od_splitPath($source); + + $url = self::API_URL . $itemId . '/copy'; + + $properties = array( + 'name' => (string)$name, + ); + if ($parentId === 'root') { + $properties['parentReference'] = (object)array('path' => '/drive/root:'); + } else { + $properties['parentReference'] = (object)array('id' => (string)$parentId); + } + $data = (object)$properties; + $curl = $this->_od_prepareCurl($url); + curl_setopt_array($curl, array( + CURLOPT_POST => true, + CURLOPT_HEADER => true, + CURLOPT_HTTPHEADER => array( + 'Content-Type: application/json', + 'Authorization: Bearer ' . $this->token->data->access_token, + 'Prefer: respond-async', + ), + CURLOPT_POSTFIELDS => json_encode($data), + )); + $result = curl_exec($curl); + curl_close($curl); + + $res = new stdClass(); + if (preg_match('/Location: (.+)/', $result, $m)) { + $monUrl = trim($m[1]); + while ($res) { + usleep(200000); + $curl = curl_init($monUrl); + curl_setopt_array($curl, array( + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => array( + 'Content-Type: application/json', + ), + )); + $res = json_decode(curl_exec($curl)); + curl_close($curl); + if (isset($res->status)) { + if ($res->status === 'completed' || $res->status === 'failed') { + break; + } + } elseif (isset($res->error)) { + return $this->setError('OneDrive error: ' . $res->error->message); + } + } + } + + if ($res && isset($res->resourceId)) { + if (isset($res->folder) && isset($this->sessionCache['subdirs'])) { + $this->sessionCache['subdirs'][$targetDir] = true; + } + + return $this->_joinPath($targetDir, $res->resourceId); + } + + return false; + } catch (Exception $e) { + return $this->setError('OneDrive error: ' . $e->getMessage()); + } + + return true; + } + + /** + * Move file into another parent dir. + * Return new file path or false. + * + * @param string $source source file path + * @param $targetDir + * @param string $name file name + * + * @return string|bool + * @author Dmitry (dio) Levashov + */ + protected function _move($source, $targetDir, $name) + { + try { + list(, $targetParentId) = $this->_od_splitPath($targetDir); + list($sourceParentId, $itemId, $srcParent) = $this->_od_splitPath($source); + + $properties = array( + 'name' => (string)$name, + ); + if ($targetParentId !== $sourceParentId) { + $properties['parentReference'] = (object)array('id' => (string)$targetParentId); + } + + $url = self::API_URL . $itemId; + $data = (object)$properties; + + $curl = $this->_od_prepareCurl($url); + + curl_setopt_array($curl, array( + CURLOPT_CUSTOMREQUEST => 'PATCH', + CURLOPT_POSTFIELDS => json_encode($data), + )); + + $result = json_decode(curl_exec($curl)); + curl_close($curl); + if ($result && isset($result->id)) { + return $targetDir . '/' . $result->id; + } else { + return false; + } + } catch (Exception $e) { + return $this->setError('OneDrive error: ' . $e->getMessage()); + } + + return false; + } + + /** + * Remove file. + * + * @param string $path file path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _unlink($path) + { + $stat = $this->stat($path); + try { + list(, $itemId) = $this->_od_splitPath($path); + + $url = self::API_URL . $itemId; + + $curl = $this->_od_prepareCurl($url); + curl_setopt_array($curl, array( + CURLOPT_CUSTOMREQUEST => 'DELETE', + )); + + //unlink or delete File or Folder in the Parent + $result = curl_exec($curl); + curl_close($curl); + } catch (Exception $e) { + return $this->setError('OneDrive error: ' . $e->getMessage()); + } + + return true; + } + + /** + * Remove dir. + * + * @param string $path dir path + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _rmdir($path) + { + return $this->_unlink($path); + } + + /** + * Create new file and write into it from file pointer. + * Return new file path or false on error. + * + * @param resource $fp file pointer + * @param $path + * @param string $name file name + * @param array $stat file stat (required by some virtual fs) + * + * @return bool|string + * @author Dmitry (dio) Levashov + */ + protected function _save($fp, $path, $name, $stat) + { + $itemId = ''; + $size = null; + if ($name === '') { + list($parentId, $itemId, $parent) = $this->_od_splitPath($path); + } else { + if ($stat) { + if (isset($stat['name'])) { + $name = $stat['name']; + } + if (isset($stat['rev']) && strpos($stat['hash'], $this->id) === 0) { + $itemId = $stat['rev']; + } + } + list(, $parentId) = $this->_od_splitPath($path); + $parent = $path; + } + + if ($stat && isset($stat['size'])) { + $size = $stat['size']; + } else { + $stats = fstat($fp); + if (isset($stats[7])) { + $size = $stats[7]; + } + } + + if ($size > 4194304) { + return $this->_od_uploadSession($fp, $size, $name, $itemId, $parent, $parentId); + } + + try { + // for unseekable file pointer + if (!elFinder::isSeekableStream($fp)) { + if ($tfp = tmpfile()) { + if (stream_copy_to_stream($fp, $tfp, $size? $size : -1) !== false) { + rewind($tfp); + $fp = $tfp; + } + } + } + + //Create or Update a file + if ($itemId === '') { + $url = self::API_URL . $parentId . ':/' . rawurlencode($name) . ':/content'; + } else { + $url = self::API_URL . $itemId . '/content'; + } + $curl = $this->_od_prepareCurl(); + + $options = array( + CURLOPT_URL => $url, + CURLOPT_PUT => true, + CURLOPT_INFILE => $fp, + ); + // Size + if ($size !== null) { + $options[CURLOPT_INFILESIZE] = $size; + } + + curl_setopt_array($curl, $options); + + //create or update File in the Target + $file = json_decode(curl_exec($curl)); + curl_close($curl); + + return $this->_joinPath($parent, $file->id); + } catch (Exception $e) { + return $this->setError('OneDrive error: ' . $e->getMessage()); + } + } + + /** + * Get file contents. + * + * @param string $path file path + * + * @return string|false + * @author Dmitry (dio) Levashov + **/ + protected function _getContents($path) + { + $contents = ''; + + try { + list(, $itemId) = $this->_od_splitPath($path); + $url = self::API_URL . $itemId . '/content'; + $contents = $this->_od_createCurl($url, $contents = true); + } catch (Exception $e) { + return $this->setError('OneDrive error: ' . $e->getMessage()); + } + + return $contents; + } + + /** + * Write a string to a file. + * + * @param string $path file path + * @param string $content new file content + * + * @return bool + * @author Dmitry (dio) Levashov + **/ + protected function _filePutContents($path, $content) + { + $res = false; + + if ($local = $this->getTempFile($path)) { + if (file_put_contents($local, $content, LOCK_EX) !== false + && ($fp = fopen($local, 'rb'))) { + clearstatcache(); + $res = $this->_save($fp, $path, '', array()); + fclose($fp); + } + file_exists($local) && unlink($local); + } + + return $res; + } + + /** + * Detect available archivers. + **/ + protected function _checkArchivers() + { + // die('Not yet implemented. (_checkArchivers)'); + return array(); + } + + /** + * chmod implementation. + * + * @return bool + **/ + protected function _chmod($path, $mode) + { + return false; + } + + /** + * Unpack archive. + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return void + * @author Dmitry (dio) Levashov + * @author Alexey Sukhotin + */ + protected function _unpack($path, $arc) + { + die('Not yet implemented. (_unpack)'); + //return false; + } + + /** + * Extract files from archive. + * + * @param string $path archive path + * @param array $arc archiver command and arguments (same as in $this->archivers) + * + * @return void + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + */ + protected function _extract($path, $arc) + { + die('Not yet implemented. (_extract)'); + } + + /** + * Create archive and return its path. + * + * @param string $dir target dir + * @param array $files files names list + * @param string $name archive name + * @param array $arc archiver options + * + * @return string|bool + * @author Dmitry (dio) Levashov, + * @author Alexey Sukhotin + **/ + protected function _archive($dir, $files, $name, $arc) + { + die('Not yet implemented. (_archive)'); + } +} // END class diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeTrash.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeTrash.class.php new file mode 100644 index 0000000..5140e7d --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeTrash.class.php @@ -0,0 +1,51 @@ +options['lockEverything'] = false; // Lock all items in the trash to disable delete, move, rename. + + // common options as the volume driver + $this->options['alias'] = 'Trash'; + $this->options['quarantine'] = ''; + $this->options['rootCssClass'] = 'elfinder-navbar-root-trash'; + $this->options['copyOverwrite'] = false; + $this->options['uiCmdMap'] = array('paste' => 'hidden', 'mkdir' => 'hidden', 'copy' => 'restore'); + $this->options['disabled'] = array('archive', 'duplicate', 'edit', 'extract', 'mkfile', 'places', 'put', 'rename', 'resize', 'upload'); + } + + public function mount(array $opts) + { + if ($this->options['lockEverything']) { + if (!is_array($opts['attributes'])) { + $opts['attributes'] = array(); + } + $attr = array( + 'pattern' => '/./', + 'locked' => true, + ); + array_unshift($opts['attributes'], $attr); + } + // force set `copyJoin` to true + $opts['copyJoin'] = true; + + return parent::mount($opts); + } +} diff --git a/upload/system/library/sitecreator/elFinder/php/elFinderVolumeTrashMySQL.class.php b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeTrashMySQL.class.php new file mode 100644 index 0000000..6d6307e --- /dev/null +++ b/upload/system/library/sitecreator/elFinder/php/elFinderVolumeTrashMySQL.class.php @@ -0,0 +1,51 @@ +options['lockEverything'] = false; // Lock all items in the trash to disable delete, move, rename. + + // common options as the volume driver + $this->options['alias'] = 'Trash'; + $this->options['quarantine'] = ''; + $this->options['rootCssClass'] = 'elfinder-navbar-root-trash'; + $this->options['copyOverwrite'] = false; + $this->options['uiCmdMap'] = array('paste' => 'hidden', 'mkdir' => 'hidden', 'copy' => 'restore'); + $this->options['disabled'] = array('archive', 'duplicate', 'edit', 'extract', 'mkfile', 'places', 'put', 'rename', 'resize', 'upload'); + } + + public function mount(array $opts) + { + if ($this->options['lockEverything']) { + if (!is_array($opts['attributes'])) { + $opts['attributes'] = array(); + } + $attr = array( + 'pattern' => '/./', + 'locked' => true, + ); + array_unshift($opts['attributes'], $attr); + } + // force set `copyJoin` to true + $opts['copyJoin'] = true; + + return parent::mount($opts); + } +} diff --git a/upload/system/library/sitecreator/trueFileManager/ControllerCommonTrueFileManager.php b/upload/system/library/sitecreator/trueFileManager/ControllerCommonTrueFileManager.php new file mode 100644 index 0000000..88eadf7 --- /dev/null +++ b/upload/system/library/sitecreator/trueFileManager/ControllerCommonTrueFileManager.php @@ -0,0 +1,134 @@ +="))? true:false; + $oc15 = (version_compare(VERSION, "2.0", "<"))? true:false; + $oc30 = (version_compare(VERSION, "3.0", ">="))? true:false; + + $data = []; + $data['module_ver'] = '1.3.1'; + + if($oc30) $data['token'] = $this->session->data['user_token']; // opencart 3 + else $data['token'] = $this->session->data['token']; + + // Return the target ID for the file manager to set the value + if (isset($this->request->get['target'])) { + $data['target'] = $this->request->get['target']; + } else { + $data['target'] = ''; + } + + // CKEditor + if (isset($this->request->get['cke'])) { + $data['cke'] = $this->request->get['cke']; + } else { + $data['cke'] = ''; + } + + // summernote_id + if (isset($this->request->get['summernote_id'])) { + $data['summernote_id'] = $this->request->get['summernote_id']; + } else { + $data['summernote_id'] = ''; + } + + // Return the thumbnail for the file manager to show a thumbnail + if (isset($this->request->get['thumb'])) { + $data['thumb'] = $this->request->get['thumb']; + } else { + $data['thumb'] = ''; + } + + $tpl = "truefilemanager.tpl"; + if($oc23) $tpl = "truefilemanager"; + if($oc30) $tpl = "truefilemanager_for_oc3"; + $tpl = 'extension/module/'. $tpl; + + $this->response->setOutput($this->load->view($tpl, $data)); + } + + public function connector() { + + require_once DIR_SYSTEM. "library/sitecreator/elFinder/php/elFinderConnector.class.php"; + require_once DIR_SYSTEM. "library/sitecreator/elFinder/php/elFinder.class.php"; + require_once DIR_SYSTEM. "library/sitecreator/elFinder/php/elFinderVolumeDriver.class.php"; + require_once DIR_SYSTEM. "library/sitecreator/elFinder/php/elFinderVolumeLocalFileSystem.class.php"; + + if (isset($this->request->server['HTTPS']) && (($this->request->server['HTTPS'] == 'on') || ($this->request->server['HTTPS'] == '1'))) + $domain = (defined('HTTPS_CATALOG'))? HTTPS_CATALOG: HTTPS_SERVER; + else {$domain = (defined('HTTP_CATALOG'))? HTTP_CATALOG: HTTP_SERVER;} + + $this->load->model('setting/setting'); + $img_opti_status = $this->config->get('module_img_opti_status'); + $upload_allow = array('image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/pjpeg', 'image/x-png'); + if ($img_opti_status) { + $upload_allow[] = 'image/webp'; + } + + $opts = array( + 'roots' => array( + array( + 'driver' => 'LocalFileSystem', // driver for accessing file system (REQUIRED) + 'path' => DIR_IMAGE. 'catalog/', // path to files (REQUIRED) + 'URL' => $domain. 'image/catalog/', + 'tmbPath'=> DIR_IMAGE. 'elfinder_tmb', + 'tmbURL' => $domain. 'image/elfinder_tmb/', + 'tmbSize' => 100, + 'tmbCrop' => false, + 'tmbBgColor' => '#ffffff', + 'mimeDetect' => 'internal', + 'imgLib' => 'auto', + 'winHashFix' => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too + 'uploadAllow' => $upload_allow, + 'uploadDeny' => array('all'), + 'uploadOrder' => array('allow, deny'), + ) + + ) + ); + + + $connector = new elFinderConnector(new elFinder($opts), true); + $connector->run(); + + } + +} + +?> \ No newline at end of file