commit c764f4a26e61ce6d226a159749168566edef4a02 Author: nertyog Date: Thu Jul 30 15:11:58 2026 +0300 Updated Image Optimizer [NAT] for OpenCart 2.0-2.2 (v1.2.0) 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 0000000..18cee30 Binary files /dev/null and b/image_optimizer_nat_OC_20_22_1_1.ocmod.zip differ 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'] = ''; +$_['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'] = ''; +$_['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'] = ''; +$_['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'] = ''; +$_['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'] = ''; +$_['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'] = ''; +$_['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 0000000..ddee473 Binary files /dev/null and b/upload/system/library/font/Roboto-Regular.ttf differ 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