From 96ebd2efe70ee4b83d8c5ce798e6e1f3064a74d9 Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Tue, 18 Mar 2025 14:29:38 +0100 Subject: [PATCH] Add core admin controllers for order management and search engines --- .vscode/ftp-kr.sync.cache.json | 31625 +++++++++++++++- config/defines.inc.php | 203 + controllers/admin/AdminAccessController.php | 226 + .../admin/AdminAddressesController.php | 551 + .../admin/AdminAttachmentsController.php | 262 + .../AdminAttributeGeneratorController.php | 273 + .../admin/AdminAttributesGroupsController.php | 967 + .../admin/AdminCarrierWizardController.php | 983 + controllers/admin/AdminCarriersController.php | 737 + .../admin/AdminCartRulesController.php | 724 + controllers/admin/AdminCartsController.php | 937 + .../admin/AdminCategoriesController.php | 1104 + .../admin/AdminCmsCategoriesController.php | 311 + .../admin/AdminCmsContentController.php | 304 + controllers/admin/AdminCmsController.php | 459 + controllers/admin/AdminContactsController.php | 156 + .../admin/AdminCountriesController.php | 528 + .../admin/AdminCurrenciesController.php | 340 + .../admin/AdminCustomerThreadsController.php | 1199 + .../admin/AdminCustomersController.php | 1111 + .../admin/AdminDashboardController.php | 538 + .../admin/AdminEmployeesController.php | 670 + controllers/admin/AdminFeaturesController.php | 637 + controllers/admin/AdminGendersController.php | 232 + controllers/admin/AdminGroupsController.php | 645 + controllers/admin/AdminImagesController.php | 748 + controllers/admin/AdminImportController.php | 4705 +++ .../admin/AdminLanguagesController.php | 593 + .../admin/AdminLegacyLayoutController.php | 123 + controllers/admin/AdminLoginController.php | 391 + .../admin/AdminManufacturersController.php | 863 + controllers/admin/AdminMetaController.php | 678 + controllers/admin/AdminModulesController.php | 1553 + .../admin/AdminModulesPositionsController.php | 636 + controllers/admin/AdminNotFoundController.php | 51 + .../admin/AdminOrderMessageController.php | 112 + controllers/admin/AdminOrdersController.php | 3048 ++ .../admin/AdminOutstandingController.php | 180 + controllers/admin/AdminPatternsController.php | 687 + controllers/admin/AdminPdfController.php | 211 + .../admin/AdminPreferencesController.php | 254 + controllers/admin/AdminProductsController.php | 3217 ++ controllers/admin/AdminProfilesController.php | 119 + .../admin/AdminQuickAccessesController.php | 251 + .../admin/AdminReferrersController.php | 522 + .../admin/AdminRequestSqlController.php | 535 + controllers/admin/AdminReturnController.php | 289 + .../admin/AdminSearchConfController.php | 400 + controllers/admin/AdminSearchController.php | 465 + .../admin/AdminSearchEnginesController.php | 102 + controllers/admin/AdminShopController.php | 862 + .../admin/AdminShopGroupController.php | 362 + controllers/admin/AdminShopUrlController.php | 569 + controllers/admin/AdminSlipController.php | 201 + .../AdminSpecificPriceRuleController.php | 377 + controllers/admin/AdminStatesController.php | 301 + controllers/admin/AdminStatsController.php | 1098 + controllers/admin/AdminStatsTabController.php | 309 + controllers/admin/AdminStatusesController.php | 692 + controllers/admin/AdminStoresController.php | 594 + .../admin/AdminSuppliersController.php | 557 + controllers/admin/AdminTabsController.php | 382 + controllers/admin/AdminTagsController.php | 168 + .../admin/AdminTaxRulesGroupController.php | 577 + controllers/admin/AdminTaxesController.php | 296 + controllers/admin/AdminThemesController.php | 1120 + controllers/admin/AdminTrackingController.php | 460 + .../admin/AdminTranslationsController.php | 3308 ++ .../admin/AdminWebserviceController.php | 302 + controllers/admin/AdminZonesController.php | 143 + controllers/admin/BoOrder.php | 35 + controllers/admin/index.php | 34 + 72 files changed, 79197 insertions(+), 5 deletions(-) create mode 100644 config/defines.inc.php create mode 100644 controllers/admin/AdminAccessController.php create mode 100644 controllers/admin/AdminAddressesController.php create mode 100644 controllers/admin/AdminAttachmentsController.php create mode 100644 controllers/admin/AdminAttributeGeneratorController.php create mode 100644 controllers/admin/AdminAttributesGroupsController.php create mode 100644 controllers/admin/AdminCarrierWizardController.php create mode 100644 controllers/admin/AdminCarriersController.php create mode 100644 controllers/admin/AdminCartRulesController.php create mode 100644 controllers/admin/AdminCartsController.php create mode 100644 controllers/admin/AdminCategoriesController.php create mode 100644 controllers/admin/AdminCmsCategoriesController.php create mode 100644 controllers/admin/AdminCmsContentController.php create mode 100644 controllers/admin/AdminCmsController.php create mode 100644 controllers/admin/AdminContactsController.php create mode 100644 controllers/admin/AdminCountriesController.php create mode 100644 controllers/admin/AdminCurrenciesController.php create mode 100644 controllers/admin/AdminCustomerThreadsController.php create mode 100644 controllers/admin/AdminCustomersController.php create mode 100644 controllers/admin/AdminDashboardController.php create mode 100644 controllers/admin/AdminEmployeesController.php create mode 100644 controllers/admin/AdminFeaturesController.php create mode 100644 controllers/admin/AdminGendersController.php create mode 100644 controllers/admin/AdminGroupsController.php create mode 100644 controllers/admin/AdminImagesController.php create mode 100644 controllers/admin/AdminImportController.php create mode 100644 controllers/admin/AdminLanguagesController.php create mode 100644 controllers/admin/AdminLegacyLayoutController.php create mode 100644 controllers/admin/AdminLoginController.php create mode 100644 controllers/admin/AdminManufacturersController.php create mode 100644 controllers/admin/AdminMetaController.php create mode 100644 controllers/admin/AdminModulesController.php create mode 100644 controllers/admin/AdminModulesPositionsController.php create mode 100644 controllers/admin/AdminNotFoundController.php create mode 100644 controllers/admin/AdminOrderMessageController.php create mode 100644 controllers/admin/AdminOrdersController.php create mode 100644 controllers/admin/AdminOutstandingController.php create mode 100644 controllers/admin/AdminPatternsController.php create mode 100644 controllers/admin/AdminPdfController.php create mode 100644 controllers/admin/AdminPreferencesController.php create mode 100644 controllers/admin/AdminProductsController.php create mode 100644 controllers/admin/AdminProfilesController.php create mode 100644 controllers/admin/AdminQuickAccessesController.php create mode 100644 controllers/admin/AdminReferrersController.php create mode 100644 controllers/admin/AdminRequestSqlController.php create mode 100644 controllers/admin/AdminReturnController.php create mode 100644 controllers/admin/AdminSearchConfController.php create mode 100644 controllers/admin/AdminSearchController.php create mode 100644 controllers/admin/AdminSearchEnginesController.php create mode 100644 controllers/admin/AdminShopController.php create mode 100644 controllers/admin/AdminShopGroupController.php create mode 100644 controllers/admin/AdminShopUrlController.php create mode 100644 controllers/admin/AdminSlipController.php create mode 100644 controllers/admin/AdminSpecificPriceRuleController.php create mode 100644 controllers/admin/AdminStatesController.php create mode 100644 controllers/admin/AdminStatsController.php create mode 100644 controllers/admin/AdminStatsTabController.php create mode 100644 controllers/admin/AdminStatusesController.php create mode 100644 controllers/admin/AdminStoresController.php create mode 100644 controllers/admin/AdminSuppliersController.php create mode 100644 controllers/admin/AdminTabsController.php create mode 100644 controllers/admin/AdminTagsController.php create mode 100644 controllers/admin/AdminTaxRulesGroupController.php create mode 100644 controllers/admin/AdminTaxesController.php create mode 100644 controllers/admin/AdminThemesController.php create mode 100644 controllers/admin/AdminTrackingController.php create mode 100644 controllers/admin/AdminTranslationsController.php create mode 100644 controllers/admin/AdminWebserviceController.php create mode 100644 controllers/admin/AdminZonesController.php create mode 100644 controllers/admin/BoOrder.php create mode 100644 controllers/admin/index.php diff --git a/.vscode/ftp-kr.sync.cache.json b/.vscode/ftp-kr.sync.cache.json index dd9fe291..72e46174 100644 --- a/.vscode/ftp-kr.sync.cache.json +++ b/.vscode/ftp-kr.sync.cache.json @@ -3512,15 +3512,15 @@ }, "google-merchant_id-1.xml": { "type": "-", - "size": 73460111, + "size": 73443780, "lmtime": 0, - "modified": false + "modified": true }, "google-merchant_id-2.xml": { "type": "-", - "size": 2945129, + "size": 2974603, "lmtime": 0, - "modified": false + "modified": true }, ".htaccess": { "type": "-", @@ -3651,7 +3651,31622 @@ "lmtime": 0, "modified": false }, - "themes": {}, + "themes": { + "child_leo_gstore.zip": { + "type": "-", + "size": 103444, + "lmtime": 0, + "modified": false + }, + "classic": {}, + "classic-backup": {}, + "classic-theme-2.0.5.zip": { + "type": "-", + "size": 835743, + "lmtime": 0, + "modified": false + }, + "classic.zip": { + "type": "-", + "size": 6605606, + "lmtime": 0, + "modified": false + }, + "core.js": { + "type": "-", + "size": 115961, + "lmtime": 0, + "modified": false + }, + "debug.tpl": { + "type": "-", + "size": 4976, + "lmtime": 0, + "modified": false + }, + ".htaccess": { + "type": "-", + "size": 254, + "lmtime": 0, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1281, + "lmtime": 0, + "modified": false + }, + "javascript.tpl": { + "type": "-", + "size": 1495, + "lmtime": 0, + "modified": false + }, + "leo_gstore": {}, + "leo_gstore.zip": { + "type": "-", + "size": 199072224, + "lmtime": 0, + "modified": false + }, + "leo_lulandia": { + "assets": { + "cache": { + "bottom-1c39f9583.js": { + "type": "-", + "size": 1390666, + "lmtime": 1741161144399, + "modified": false + }, + "bottom-1ea995583.js": { + "type": "-", + "size": 1388313, + "lmtime": 1741161144619, + "modified": false + }, + "bottom-25b323583.js": { + "type": "-", + "size": 1389472, + "lmtime": 1741161144831, + "modified": false + }, + "bottom-2985a5583.js": { + "type": "-", + "size": 1391242, + "lmtime": 1741161145048, + "modified": false + }, + "bottom-2c9a98583.js": { + "type": "-", + "size": 1388313, + "lmtime": 1741161145240, + "modified": false + }, + "bottom-34d028583.js": { + "type": "-", + "size": 1392436, + "lmtime": 1741161145452, + "modified": false + }, + "bottom-373f10583.js": { + "type": "-", + "size": 1388401, + "lmtime": 1741161145663, + "modified": false + }, + "bottom-3c3f8d583.js": { + "type": "-", + "size": 1396154, + "lmtime": 1741161145879, + "modified": false + }, + "bottom-5e4e65583.js": { + "type": "-", + "size": 1391125, + "lmtime": 1741161146093, + "modified": false + }, + "bottom-6b344f583.js": { + "type": "-", + "size": 1396648, + "lmtime": 1741161146288, + "modified": false + }, + "bottom-7490aa583.js": { + "type": "-", + "size": 1391599, + "lmtime": 1741161146497, + "modified": false + }, + "bottom-834d2e583.js": { + "type": "-", + "size": 1390832, + "lmtime": 1741161146709, + "modified": false + }, + "bottom-875307583.js": { + "type": "-", + "size": 1389966, + "lmtime": 1741161146918, + "modified": false + }, + "bottom-90b5e9583.js": { + "type": "-", + "size": 1395909, + "lmtime": 1741161147125, + "modified": false + }, + "bottom-9bfb1f583.js": { + "type": "-", + "size": 1390440, + "lmtime": 1741161147338, + "modified": false + }, + "bottom-a33f93583.js": { + "type": "-", + "size": 1389673, + "lmtime": 1741161147556, + "modified": false + }, + "bottom-a69bdd583.js": { + "type": "-", + "size": 1393595, + "lmtime": 1741161147771, + "modified": false + }, + "bottom-a6c760583.js": { + "type": "-", + "size": 1336401, + "lmtime": 1741161147988, + "modified": false + }, + "bottom-af74d6583.js": { + "type": "-", + "size": 1335242, + "lmtime": 1741161148207, + "modified": false + }, + "bottom-b59369583.js": { + "type": "-", + "size": 1389507, + "lmtime": 1741161148409, + "modified": false + }, + "bottom-b67f46583.js": { + "type": "-", + "size": 1394995, + "lmtime": 1741161148645, + "modified": false + }, + "bottom-b7d969583.js": { + "type": "-", + "size": 1397807, + "lmtime": 1741161148850, + "modified": false + }, + "bottom-bc9be1583.js": { + "type": "-", + "size": 1390347, + "lmtime": 1741161149071, + "modified": false + }, + "bottom-ca2705583.js": { + "type": "-", + "size": 1374547, + "lmtime": 1741161149295, + "modified": false + }, + "bottom-dc89f1583.js": { + "type": "-", + "size": 1389472, + "lmtime": 1741161149507, + "modified": false + }, + "bottom-e1907a583.js": { + "type": "-", + "size": 1390473, + "lmtime": 1741161149730, + "modified": false + }, + "bottom-e99923583.js": { + "type": "-", + "size": 1392401, + "lmtime": 1741161150009, + "modified": false + }, + "bottom-ed98db583.js": { + "type": "-", + "size": 1389560, + "lmtime": 1741161150230, + "modified": false + }, + "bottom-fc5c98583.js": { + "type": "-", + "size": 1389314, + "lmtime": 1741161150443, + "modified": false + }, + "theme-0b4f8b584.css": { + "type": "-", + "size": 1579611, + "lmtime": 1741161150652, + "modified": false + }, + "theme-107c74584.css": { + "type": "-", + "size": 1558848, + "lmtime": 1741161150881, + "modified": false + }, + "theme-16c20a584.css": { + "type": "-", + "size": 1526890, + "lmtime": 1741161151106, + "modified": false + }, + "theme-bc4248584.css": { + "type": "-", + "size": 1570798, + "lmtime": 1741161151337, + "modified": false + }, + "theme-c93da4584.css": { + "type": "-", + "size": 1570093, + "lmtime": 1741161151560, + "modified": false + }, + "theme-d8e223584.css": { + "type": "-", + "size": 1527090, + "lmtime": 1741161151795, + "modified": false + }, + "theme-da39a3584.css": { + "type": "-", + "size": 0, + "lmtime": 1741161151889, + "modified": false + }, + "theme-e204d4584.css": { + "type": "-", + "size": 1570093, + "lmtime": 1741161152113, + "modified": false + }, + "theme-eaa5da584.css": { + "type": "-", + "size": 1570293, + "lmtime": 1741161152341, + "modified": false + } + }, + "css": { + "components": { + "alert.css": { + "type": "-", + "size": 1659, + "lmtime": 1741161154286, + "modified": false + }, + "block-reassurance.css": { + "type": "-", + "size": 7754, + "lmtime": 1741161154398, + "modified": false + }, + "cart.css": { + "type": "-", + "size": 6605, + "lmtime": 1741161154480, + "modified": false + }, + "categories.css": { + "type": "-", + "size": 18202, + "lmtime": 1741161154583, + "modified": false + }, + "checkout.css": { + "type": "-", + "size": 6764, + "lmtime": 1741161154684, + "modified": false + }, + "contact.css": { + "type": "-", + "size": 5915, + "lmtime": 1741161154786, + "modified": false + }, + "customer.css": { + "type": "-", + "size": 7807, + "lmtime": 1741161154874, + "modified": false + }, + "customization-modal.css": { + "type": "-", + "size": 7685, + "lmtime": 1741161154966, + "modified": false + }, + "custom-text.css": { + "type": "-", + "size": 7238, + "lmtime": 1741161155063, + "modified": false + }, + "drop-down.css": { + "type": "-", + "size": 7139, + "lmtime": 1741161155144, + "modified": false + }, + "errors.css": { + "type": "-", + "size": 7910, + "lmtime": 1741161155239, + "modified": false + }, + "featuredproducts.css": { + "type": "-", + "size": 7820, + "lmtime": 1741161155329, + "modified": false + }, + "footer.css": { + "type": "-", + "size": 10029, + "lmtime": 1741161155422, + "modified": false + }, + "forgotten-password.css": { + "type": "-", + "size": 548, + "lmtime": 1741161155513, + "modified": false + }, + "imageslider.css": { + "type": "-", + "size": 7733, + "lmtime": 1741161155600, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1277, + "lmtime": 1741161155683, + "modified": false + }, + "mainmenu.css": { + "type": "-", + "size": 7223, + "lmtime": 1741161155773, + "modified": false + }, + "products.css": { + "type": "-", + "size": 55735, + "lmtime": 1741161155898, + "modified": false + }, + "quickview.css": { + "type": "-", + "size": 2595, + "lmtime": 1741161155997, + "modified": false + }, + "renew-password.css": { + "type": "-", + "size": 211, + "lmtime": 1741161156086, + "modified": false + }, + "search-widget.css": { + "type": "-", + "size": 6071, + "lmtime": 1741161156182, + "modified": false + }, + "sitemap.css": { + "type": "-", + "size": 5400, + "lmtime": 1741161156285, + "modified": false + }, + "stores.css": { + "type": "-", + "size": 9046, + "lmtime": 1741161156381, + "modified": false + } + }, + "dr_materac.css": { + "type": "-", + "size": 79857, + "lmtime": 1741161152469, + "modified": false + }, + "dr_materac.css.map": { + "type": "-", + "size": 225827, + "lmtime": 1741161152614, + "modified": false + }, + "dr_materac.css.sync-conflict-20231027-195928-EDGUH2C.map": { + "type": "-", + "size": 27783, + "lmtime": 1741161152744, + "modified": false + }, + "dr_materac.scss": { + "type": "-", + "size": 102781, + "lmtime": 1741161152886, + "modified": false + }, + "dr_materac.sync-conflict-20231027-195844-EDGUH2C.css": { + "type": "-", + "size": 69817, + "lmtime": 1741161152995, + "modified": false + }, + "dr_materac.sync-conflict-20231027-195912-EDGUH2C.scss": { + "type": "-", + "size": 88595, + "lmtime": 1741161153143, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1277, + "lmtime": 1741161153282, + "modified": false + }, + "rtl.css": { + "type": "-", + "size": 61948, + "lmtime": 1741161153407, + "modified": false + }, + "theme.css": { + "type": "-", + "size": 433653, + "lmtime": 1741161153571, + "modified": false + }, + "theme.css.map": { + "type": "-", + "size": 1362237, + "lmtime": 1741161153795, + "modified": false + }, + "theme.scss": { + "type": "-", + "size": 640864, + "lmtime": 1741161153983, + "modified": false + } + }, + "fonts": { + "FontAwesome.otf": { + "type": "-", + "size": 134808, + "lmtime": 1741161156521, + "modified": false + }, + "fontawesome-webfont.eot": { + "type": "-", + "size": 165742, + "lmtime": 1741161156684, + "modified": false + }, + "fontawesome-webfont.svg": { + "type": "-", + "size": 444379, + "lmtime": 1741161156866, + "modified": false + }, + "fontawesome-webfont.ttf": { + "type": "-", + "size": 165548, + "lmtime": 1741161157015, + "modified": false + }, + "fontawesome-webfont.woff": { + "type": "-", + "size": 98024, + "lmtime": 1741161157152, + "modified": false + }, + "fontawesome-webfont.woff2": { + "type": "-", + "size": 77160, + "lmtime": 1741161157273, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1277, + "lmtime": 1741161157370, + "modified": false + }, + "MaterialIcons-Regular.eot": { + "type": "-", + "size": 143258, + "lmtime": 1741161157517, + "modified": false + }, + "MaterialIcons-Regular.ijmap": { + "type": "-", + "size": 28416, + "lmtime": 1741161157646, + "modified": false + }, + "MaterialIcons-Regular.svg": { + "type": "-", + "size": 281658, + "lmtime": 1741161157805, + "modified": false + }, + "MaterialIcons-Regular.ttf": { + "type": "-", + "size": 128180, + "lmtime": 1741161157948, + "modified": false + }, + "MaterialIcons-Regular.woff": { + "type": "-", + "size": 57620, + "lmtime": 1741161158071, + "modified": false + }, + "MaterialIcons-Regular.woff2": { + "type": "-", + "size": 44300, + "lmtime": 1741161158190, + "modified": false + }, + "Simple-Line-Icons.dev.svg": { + "type": "-", + "size": 182652, + "lmtime": 1741161158340, + "modified": false + }, + "Simple-Line-Icons.eot": { + "type": "-", + "size": 35514, + "lmtime": 1741161158475, + "modified": false + }, + "Simple-Line-Icons.svg": { + "type": "-", + "size": 179404, + "lmtime": 1741161158617, + "modified": false + }, + "Simple-Line-Icons.ttf": { + "type": "-", + "size": 35304, + "lmtime": 1741161158730, + "modified": false + }, + "Simple-Line-Icons.woff": { + "type": "-", + "size": 59324, + "lmtime": 1741161158850, + "modified": false + } + }, + "img": { + "drMaterac_sygnet_32px.png": { + "type": "-", + "size": 1228, + "lmtime": 1741161158950, + "modified": false + }, + "envelope-svgrepo-com.svg": { + "type": "-", + "size": 831, + "lmtime": 1741161159056, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161159133, + "modified": false + }, + "layout": { + "add_opinion_container.svg": { + "type": "-", + "size": 1287, + "lmtime": 1741161160043, + "modified": false + }, + "alior.png": { + "type": "-", + "size": 18396, + "lmtime": 1741161160151, + "modified": false + }, + "alior.webp": { + "type": "-", + "size": 3580, + "lmtime": 1741161160253, + "modified": false + }, + "chevron_green.svg": { + "type": "-", + "size": 281, + "lmtime": 1741161160350, + "modified": false + }, + "chevron_violet.svg": { + "type": "-", + "size": 423, + "lmtime": 1741161160433, + "modified": false + }, + "envelope.svg": { + "type": "-", + "size": 587, + "lmtime": 1741161160534, + "modified": false + }, + "karta_duzej_rodziny.png": { + "type": "-", + "size": 3252, + "lmtime": 1741161160632, + "modified": false + }, + "karta_duzej_rodziny.webp": { + "type": "-", + "size": 2412, + "lmtime": 1741161160722, + "modified": false + }, + "magnifier-hover.svg": { + "type": "-", + "size": 639, + "lmtime": 1741161160804, + "modified": false + }, + "magnifier.svg": { + "type": "-", + "size": 491, + "lmtime": 1741161160885, + "modified": false + }, + "santander2.png": { + "type": "-", + "size": 3497, + "lmtime": 1741161160974, + "modified": false + }, + "santander.png": { + "type": "-", + "size": 3803, + "lmtime": 1741161161076, + "modified": false + }, + "santander.webp": { + "type": "-", + "size": 1244, + "lmtime": 1741161161169, + "modified": false + }, + "share_icon.svg": { + "type": "-", + "size": 1830, + "lmtime": 1741161161266, + "modified": false + } + }, + "logo.png": { + "type": "-", + "size": 0, + "lmtime": 1741161159232, + "modified": false + }, + "lulandia-favicon.png": { + "type": "-", + "size": 2817, + "lmtime": 1741161159315, + "modified": false + }, + "lulandia-favicon.webp": { + "type": "-", + "size": 790, + "lmtime": 1741161159400, + "modified": false + }, + "modules": { + "appagebuilder": { + "icon": { + "cate-1.jpg": { + "type": "-", + "size": 20102, + "lmtime": 1741161161578, + "modified": false + }, + "cate-1.png": { + "type": "-", + "size": 34811, + "lmtime": 1741161161694, + "modified": false + }, + "cate-2.jpg": { + "type": "-", + "size": 18976, + "lmtime": 1741161161792, + "modified": false + }, + "cate-2.png": { + "type": "-", + "size": 31338, + "lmtime": 1741161161895, + "modified": false + }, + "cate-3.jpg": { + "type": "-", + "size": 22825, + "lmtime": 1741161162004, + "modified": false + }, + "cate-3.png": { + "type": "-", + "size": 38257, + "lmtime": 1741161162129, + "modified": false + }, + "cate-4.png": { + "type": "-", + "size": 43052, + "lmtime": 1741161162242, + "modified": false + }, + "cate-5.png": { + "type": "-", + "size": 34596, + "lmtime": 1741161162348, + "modified": false + }, + "cate-6.png": { + "type": "-", + "size": 26761, + "lmtime": 1741161162457, + "modified": false + }, + "img-footer-1.png": { + "type": "-", + "size": 7983, + "lmtime": 1741161162559, + "modified": false + }, + "img-footer-2.png": { + "type": "-", + "size": 6115, + "lmtime": 1741161162662, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161162763, + "modified": false + } + }, + "images": { + "Akcesoria.jpg": { + "type": "-", + "size": 61373, + "lmtime": 1741161162897, + "modified": false + }, + "Akcesoria.webp": { + "type": "-", + "size": 7920, + "lmtime": 1741161162982, + "modified": false + }, + "banner-1-h1.jpg": { + "type": "-", + "size": 10801, + "lmtime": 1741161163077, + "modified": false + }, + "banner-1-h2.jpg": { + "type": "-", + "size": 35729, + "lmtime": 1741161163200, + "modified": false + }, + "banner-1-h3.jpg": { + "type": "-", + "size": 28905, + "lmtime": 1741161163311, + "modified": false + }, + "banner-1-h4.jpg": { + "type": "-", + "size": 11964, + "lmtime": 1741161163422, + "modified": false + }, + "banner-2-h1.jpg": { + "type": "-", + "size": 12865, + "lmtime": 1741161163505, + "modified": false + }, + "banner-2-h3.jpg": { + "type": "-", + "size": 23229, + "lmtime": 1741161163612, + "modified": false + }, + "banner-3-h1.jpg": { + "type": "-", + "size": 10083, + "lmtime": 1741161163699, + "modified": false + }, + "banner-4-h1.jpg": { + "type": "-", + "size": 18303, + "lmtime": 1741161163805, + "modified": false + }, + "banner-5-h1.jpg": { + "type": "-", + "size": 31040, + "lmtime": 1741161163912, + "modified": false + }, + "banner-5-h3.jpg": { + "type": "-", + "size": 16480, + "lmtime": 1741161164013, + "modified": false + }, + "banner-6-h3.jpg": { + "type": "-", + "size": 20444, + "lmtime": 1741161164119, + "modified": false + }, + "banner-7-h3.jpg": { + "type": "-", + "size": 21202, + "lmtime": 1741161164224, + "modified": false + }, + "bg-discount.jpg": { + "type": "-", + "size": 4820, + "lmtime": 1741161164327, + "modified": false + }, + "bg-newletter.jpg": { + "type": "-", + "size": 4111, + "lmtime": 1741161164427, + "modified": false + }, + "category_akcesoria.jpg": { + "type": "-", + "size": 80740, + "lmtime": 1741161164560, + "modified": false + }, + "category_akcesoria.webp": { + "type": "-", + "size": 10990, + "lmtime": 1741161164648, + "modified": false + }, + "category_lozko.jpg": { + "type": "-", + "size": 64816, + "lmtime": 1741161164778, + "modified": false + }, + "category_lozko.webp": { + "type": "-", + "size": 8226, + "lmtime": 1741161164865, + "modified": false + }, + "category_materace.jpg": { + "type": "-", + "size": 72817, + "lmtime": 1741161164996, + "modified": false + }, + "category_materace.webp": { + "type": "-", + "size": 17082, + "lmtime": 1741161165114, + "modified": false + }, + "cta_image.png": { + "type": "-", + "size": 105739, + "lmtime": 1741161165261, + "modified": false + }, + "cta_image.webp": { + "type": "-", + "size": 25034, + "lmtime": 1741161165394, + "modified": false + }, + "icon-free-shipping.png": { + "type": "-", + "size": 4462, + "lmtime": 1741161165490, + "modified": false + }, + "icon-online-support.png": { + "type": "-", + "size": 5245, + "lmtime": 1741161165592, + "modified": false + }, + "icon-safe-shoping.png": { + "type": "-", + "size": 5437, + "lmtime": 1741161165694, + "modified": false + }, + "img-footer-1.png": { + "type": "-", + "size": 7983, + "lmtime": 1741161165776, + "modified": false + }, + "img-footer-2.png": { + "type": "-", + "size": 6115, + "lmtime": 1741161165925, + "modified": false + }, + "img-payment.png": { + "type": "-", + "size": 14054, + "lmtime": 1741161166014, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161166098, + "modified": false + }, + "Łóżka.jpg": { + "type": "-", + "size": 70002, + "lmtime": 1741161166236, + "modified": false + }, + "Łóżka.jpg": { + "type": "-", + "size": 70002, + "lmtime": 1741161166236, + "modified": false + }, + "Łóżka.webp": { + "type": "-", + "size": 8488, + "lmtime": 1741161166315, + "modified": false + }, + "Łóżka.webp": { + "type": "-", + "size": 8488, + "lmtime": 1741161166315, + "modified": false + }, + "Materace-1.jpg": { + "type": "-", + "size": 83848, + "lmtime": 1741161166450, + "modified": false + }, + "Materace-1.webp": { + "type": "-", + "size": 13712, + "lmtime": 1741161166535, + "modified": false + }, + "Materace-3.jpg": { + "type": "-", + "size": 81071, + "lmtime": 1741161166662, + "modified": false + }, + "poduszki 1.jpg": { + "type": "-", + "size": 161101, + "lmtime": 1741161166816, + "modified": false + }, + "recepta_na_zdrowy_sen.png": { + "type": "-", + "size": 72756, + "lmtime": 1741161166937, + "modified": false + }, + "recepta_na_zdrowy_sen.webp": { + "type": "-", + "size": 20508, + "lmtime": 1741161167051, + "modified": false + }, + "wirtualny_spacer.jpg": { + "type": "-", + "size": 39379, + "lmtime": 1741161167178, + "modified": false + }, + "wirtualny_spacer.webp": { + "type": "-", + "size": 28522, + "lmtime": 1741161167282, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161161465, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161161360, + "modified": false + }, + "leoblog": { + "1": { + "b": { + "b-10.jpg": { + "type": "-", + "size": 58446, + "lmtime": 1741161167613, + "modified": false + }, + "b-10.webp": { + "type": "-", + "size": 28732, + "lmtime": 1741161167719, + "modified": false + }, + "b-11.jpg": { + "type": "-", + "size": 86661, + "lmtime": 1741161167858, + "modified": false + }, + "b-11.webp": { + "type": "-", + "size": 57354, + "lmtime": 1741161167973, + "modified": false + }, + "b-12.jpg": { + "type": "-", + "size": 78353, + "lmtime": 1741161168092, + "modified": false + }, + "b-13.jpg": { + "type": "-", + "size": 57738, + "lmtime": 1741161168224, + "modified": false + }, + "b-14.jpg": { + "type": "-", + "size": 50431, + "lmtime": 1741161168347, + "modified": false + }, + "b-15.jpg": { + "type": "-", + "size": 55687, + "lmtime": 1741161168471, + "modified": false + }, + "b-16.jpg": { + "type": "-", + "size": 49593, + "lmtime": 1741161168593, + "modified": false + }, + "b-17.jpg": { + "type": "-", + "size": 59149, + "lmtime": 1741161168717, + "modified": false + }, + "b-18.jpg": { + "type": "-", + "size": 38386, + "lmtime": 1741161168833, + "modified": false + }, + "b-18.webp": { + "type": "-", + "size": 19854, + "lmtime": 1741161168947, + "modified": false + }, + "b-19.jpg": { + "type": "-", + "size": 47324, + "lmtime": 1741161169066, + "modified": false + }, + "b-1.jpg": { + "type": "-", + "size": 57819, + "lmtime": 1741161169188, + "modified": false + }, + "b-20.jpg": { + "type": "-", + "size": 64720, + "lmtime": 1741161169311, + "modified": false + }, + "b-20.webp": { + "type": "-", + "size": 36116, + "lmtime": 1741161169428, + "modified": false + }, + "b-2.jpg": { + "type": "-", + "size": 51688, + "lmtime": 1741161169552, + "modified": false + }, + "b-2.webp": { + "type": "-", + "size": 24758, + "lmtime": 1741161169660, + "modified": false + }, + "b-3.jpg": { + "type": "-", + "size": 31190, + "lmtime": 1741161169780, + "modified": false + }, + "b-3.webp": { + "type": "-", + "size": 14410, + "lmtime": 1741161169868, + "modified": false + }, + "b-4.jpg": { + "type": "-", + "size": 39851, + "lmtime": 1741161169979, + "modified": false + }, + "b-5.jpg": { + "type": "-", + "size": 47158, + "lmtime": 1741161170100, + "modified": false + }, + "b-6.jpg": { + "type": "-", + "size": 47907, + "lmtime": 1741161170219, + "modified": false + }, + "b-7.jpg": { + "type": "-", + "size": 64451, + "lmtime": 1741161170339, + "modified": false + }, + "b-8.jpg": { + "type": "-", + "size": 56513, + "lmtime": 1741161170466, + "modified": false + }, + "b-9.jpg": { + "type": "-", + "size": 43725, + "lmtime": 1741161170587, + "modified": false + }, + "b-b-10.jpg": { + "type": "-", + "size": 58350, + "lmtime": 1741161170717, + "modified": false + }, + "b-b-10.webp": { + "type": "-", + "size": 26764, + "lmtime": 1741161170817, + "modified": false + }, + "b-b-18.jpg": { + "type": "-", + "size": 39650, + "lmtime": 1741161170935, + "modified": false + }, + "b-b-18.webp": { + "type": "-", + "size": 19070, + "lmtime": 1741161171029, + "modified": false + }, + "b-b-20.jpg": { + "type": "-", + "size": 66404, + "lmtime": 1741161171160, + "modified": false + }, + "b-b-20.webp": { + "type": "-", + "size": 35412, + "lmtime": 1741161171270, + "modified": false + }, + "b-b-2.jpg": { + "type": "-", + "size": 52619, + "lmtime": 1741161171387, + "modified": false + }, + "b-b-2.webp": { + "type": "-", + "size": 23788, + "lmtime": 1741161171491, + "modified": false + }, + "b-b-drmaterac_zdj_blog_1_compressed.jpg": { + "type": "-", + "size": 61469, + "lmtime": 1741161171630, + "modified": false + }, + "b-b-drmaterac_zdj_blog_1_compressed.webp": { + "type": "-", + "size": 29968, + "lmtime": 1741161171748, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2-_1_.jpg": { + "type": "-", + "size": 67810, + "lmtime": 1741161171877, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2-_1_.webp": { + "type": "-", + "size": 32816, + "lmtime": 1741161171995, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2-_2_.jpg": { + "type": "-", + "size": 67810, + "lmtime": 1741161172115, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2-_2_.webp": { + "type": "-", + "size": 32816, + "lmtime": 1741161172243, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2-_3_.jpg": { + "type": "-", + "size": 67810, + "lmtime": 1741161172371, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2-_3_.webp": { + "type": "-", + "size": 32816, + "lmtime": 1741161172493, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2_compressed.jpg": { + "type": "-", + "size": 66777, + "lmtime": 1741161172615, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2_compressed.webp": { + "type": "-", + "size": 31610, + "lmtime": 1741161172727, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2.jpg": { + "type": "-", + "size": 13467, + "lmtime": 1741161172829, + "modified": false + }, + "b-b-drmaterac_zdj_blog_2.webp": { + "type": "-", + "size": 7166, + "lmtime": 1741161172920, + "modified": false + }, + "b-blog-1.jpg": { + "type": "-", + "size": 110384, + "lmtime": 1741161173055, + "modified": false + }, + "b-blog-2.jpg": { + "type": "-", + "size": 134121, + "lmtime": 1741161173181, + "modified": false + }, + "b-blog-3.jpg": { + "type": "-", + "size": 76701, + "lmtime": 1741161173304, + "modified": false + }, + "b-blog-4.jpg": { + "type": "-", + "size": 66547, + "lmtime": 1741161173426, + "modified": false + }, + "b-blog-5.jpg": { + "type": "-", + "size": 88324, + "lmtime": 1741161173558, + "modified": false + }, + "b-blog-6.jpg": { + "type": "-", + "size": 213971, + "lmtime": 1741161173716, + "modified": false + }, + "b-blog-7.jpg": { + "type": "-", + "size": 71750, + "lmtime": 1741161173847, + "modified": false + }, + "b-drmaterac_zdj_blog_1_compressed.jpg": { + "type": "-", + "size": 62366, + "lmtime": 1741161173967, + "modified": false + }, + "b-drmaterac_zdj_blog_1_compressed.webp": { + "type": "-", + "size": 31148, + "lmtime": 1741161174080, + "modified": false + }, + "b-drmaterac_zdj_blog_1.png": { + "type": "-", + "size": 80230, + "lmtime": 1741161174208, + "modified": false + }, + "b-drmaterac_zdj_blog_1.webp": { + "type": "-", + "size": 49680, + "lmtime": 1741161174338, + "modified": false + }, + "b-drmaterac_zdj_blog_2_compressed.jpg": { + "type": "-", + "size": 67723, + "lmtime": 1741161174468, + "modified": false + }, + "b-drmaterac_zdj_blog_2_compressed.webp": { + "type": "-", + "size": 32896, + "lmtime": 1741161174590, + "modified": false + }, + "b-drmaterac_zdj_blog_2.png": { + "type": "-", + "size": 70749, + "lmtime": 1741161174712, + "modified": false + }, + "b-drmaterac_zdj_blog_2.webp": { + "type": "-", + "size": 34298, + "lmtime": 1741161174822, + "modified": false + }, + "b-dyskopatia-ledzwiowa-blog.jpg": { + "type": "-", + "size": 44529, + "lmtime": 1741161174946, + "modified": false + }, + "b-dyskopatia-ledzwiowa-blog.webp": { + "type": "-", + "size": 21752, + "lmtime": 1741161175056, + "modified": false + }, + "b-lozka.png": { + "type": "-", + "size": 60009, + "lmtime": 1741161175179, + "modified": false + }, + "b-łóżka.png": { + "type": "-", + "size": 60009, + "lmtime": 1741161175305, + "modified": false + }, + "b-materac chlodzacy.png": { + "type": "-", + "size": 61184, + "lmtime": 1741161175420, + "modified": false + }, + "b-materac chłodzacy.png": { + "type": "-", + "size": 61184, + "lmtime": 1741161175537, + "modified": false + }, + "b-meble z drewna litego.png": { + "type": "-", + "size": 89012, + "lmtime": 1741161175671, + "modified": false + }, + "b-meble z drewna litego.webp": { + "type": "-", + "size": 53454, + "lmtime": 1741161175794, + "modified": false + }, + "b-poduszka-na-bole-glowy-min.jpg": { + "type": "-", + "size": 45208, + "lmtime": 1741161175919, + "modified": false + }, + "b-poduszka-na-bole-glowy-min.webp": { + "type": "-", + "size": 21992, + "lmtime": 1741161176024, + "modified": false + }, + "b-stelaze.png": { + "type": "-", + "size": 64536, + "lmtime": 1741161176150, + "modified": false + }, + "b-stelaże.png": { + "type": "-", + "size": 64536, + "lmtime": 1741161176272, + "modified": false + }, + "b-temperatura do spania.png": { + "type": "-", + "size": 52026, + "lmtime": 1741161176391, + "modified": false + }, + "b-temperatura do spania.webp": { + "type": "-", + "size": 23662, + "lmtime": 1741161176496, + "modified": false + }, + "b-utwardzic materac.png": { + "type": "-", + "size": 52844, + "lmtime": 1741161176619, + "modified": false + }, + "b-utwardzić materac.png": { + "type": "-", + "size": 52844, + "lmtime": 1741161176749, + "modified": false + }, + "b-utwardzic materac.webp": { + "type": "-", + "size": 23622, + "lmtime": 1741161176846, + "modified": false + }, + "b-utwardzić materac.webp": { + "type": "-", + "size": 23622, + "lmtime": 1741161176963, + "modified": false + }, + "fileType": { + "type": "-", + "size": 3, + "lmtime": 1741161177061, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161177157, + "modified": false + } + }, + "c": { + "category-1.jpg": { + "type": "-", + "size": 46913, + "lmtime": 1741161177279, + "modified": false + }, + "category-2.jpg": { + "type": "-", + "size": 12288, + "lmtime": 1741161177373, + "modified": false + }, + "category-3.jpg": { + "type": "-", + "size": 17936, + "lmtime": 1741161177489, + "modified": false + }, + "category-3.webp": { + "type": "-", + "size": 8838, + "lmtime": 1741161177588, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161177696, + "modified": false + }, + "[themeforest]_Floral_banner.jpg": { + "type": "-", + "size": 41518, + "lmtime": 1741161177816, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161167479, + "modified": false + } + }, + "2": { + "b": { + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161177917, + "modified": false + } + }, + "c": { + "1200x500 - Blog - Akcesoria.png": { + "type": "-", + "size": 296750, + "lmtime": 1741161178091, + "modified": false + }, + "1200x500 - Blog - Akcesoria.webp": { + "type": "-", + "size": 36970, + "lmtime": 1741161178204, + "modified": false + }, + "1200x500 - Blog - Łóżka.png": { + "type": "-", + "size": 390580, + "lmtime": 1741161178369, + "modified": false + }, + "1200x500 - Blog - Łóżka.png": { + "type": "-", + "size": 390580, + "lmtime": 1741161178369, + "modified": false + }, + "1200x500 - Blog - Łóżka.webp": { + "type": "-", + "size": 48762, + "lmtime": 1741161178478, + "modified": false + }, + "1200x500 - Blog - Łóżka.webp": { + "type": "-", + "size": 48762, + "lmtime": 1741161178478, + "modified": false + }, + "1200x500 - Blog - Materace sprężynowe.jpg": { + "type": "-", + "size": 62023, + "lmtime": 1741161178604, + "modified": false + }, + "1200x500 - Blog - Materace sprężynowe.jpg": { + "type": "-", + "size": 62023, + "lmtime": 1741161178604, + "modified": false + }, + "1200x500 - Blog - Materace sprężynowe.png": { + "type": "-", + "size": 380182, + "lmtime": 1741161178782, + "modified": false + }, + "1200x500 - Blog - Materace sprężynowe.png": { + "type": "-", + "size": 380182, + "lmtime": 1741161178782, + "modified": false + }, + "1200x500 - Blog - Materace sprężynowe.webp": { + "type": "-", + "size": 35278, + "lmtime": 1741161178899, + "modified": false + }, + "1200x500 - Blog - Materace sprężynowe.webp": { + "type": "-", + "size": 35278, + "lmtime": 1741161178899, + "modified": false + }, + "1200x500 - Blog.png": { + "type": "-", + "size": 486586, + "lmtime": 1741161179071, + "modified": false + }, + "1200x500 - Blog - Stelaże.png": { + "type": "-", + "size": 367983, + "lmtime": 1741161179235, + "modified": false + }, + "1200x500 - Blog - Stelaże.png": { + "type": "-", + "size": 367983, + "lmtime": 1741161179235, + "modified": false + }, + "1200x500 - Blog - Stelaże.webp": { + "type": "-", + "size": 40838, + "lmtime": 1741161179365, + "modified": false + }, + "1200x500 - Blog - Stelaże.webp": { + "type": "-", + "size": 40838, + "lmtime": 1741161179365, + "modified": false + }, + "1200x500 - Blog.webp": { + "type": "-", + "size": 57706, + "lmtime": 1741161179490, + "modified": false + }, + "Akcesoria.jpg": { + "type": "-", + "size": 18306, + "lmtime": 1741161179597, + "modified": false + }, + "fileType": { + "type": "-", + "size": 3, + "lmtime": 1741161179680, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161179762, + "modified": false + }, + "Łóżka.jpg": { + "type": "-", + "size": 19919, + "lmtime": 1741161179872, + "modified": false + }, + "Łóżka.jpg": { + "type": "-", + "size": 19919, + "lmtime": 1741161179872, + "modified": false + }, + "Materace.jpg": { + "type": "-", + "size": 27871, + "lmtime": 1741161179986, + "modified": false + }, + "Materace.webp": { + "type": "-", + "size": 15440, + "lmtime": 1741161180086, + "modified": false + } + } + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161167373, + "modified": false + }, + "sample": { + "b": { + "b-blog-1.jpg": { + "type": "-", + "size": 110384, + "lmtime": 1741161180311, + "modified": false + }, + "b-blog-2.jpg": { + "type": "-", + "size": 134121, + "lmtime": 1741161180503, + "modified": false + }, + "b-blog-3.jpg": { + "type": "-", + "size": 76701, + "lmtime": 1741161180616, + "modified": false + }, + "b-blog-4.jpg": { + "type": "-", + "size": 66547, + "lmtime": 1741161180746, + "modified": false + }, + "b-blog-5.jpg": { + "type": "-", + "size": 88324, + "lmtime": 1741161180889, + "modified": false + }, + "b-blog-6.jpg": { + "type": "-", + "size": 213971, + "lmtime": 1741161181025, + "modified": false + }, + "b-blog-7.jpg": { + "type": "-", + "size": 71750, + "lmtime": 1741161181157, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161181259, + "modified": false + } + }, + "c": { + "a412e41a63f312d1e01cb3b183646855.jpg": { + "type": "-", + "size": 344940, + "lmtime": 1741161181421, + "modified": false + }, + "category-1.jpg": { + "type": "-", + "size": 46913, + "lmtime": 1741161181554, + "modified": false + }, + "category-2.jpg": { + "type": "-", + "size": 12288, + "lmtime": 1741161181655, + "modified": false + }, + "category-3.jpg": { + "type": "-", + "size": 17936, + "lmtime": 1741161181758, + "modified": false + }, + "en-forma.jpg": { + "type": "-", + "size": 182125, + "lmtime": 1741161181906, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161181988, + "modified": false + }, + "[themeforest]_Floral_banner.jpg": { + "type": "-", + "size": 41518, + "lmtime": 1741161182097, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161180179, + "modified": false + } + } + }, + "leobootstrapmenu": { + "banner-2.jpg": { + "type": "-", + "size": 82313, + "lmtime": 1741161182219, + "modified": false + }, + "icons": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161182415, + "modified": false + } + }, + "img": { + "icons": { + "index.php": { + "type": "-", + "size": 1287, + "lmtime": 1741161182605, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1287, + "lmtime": 1741161182515, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161182321, + "modified": false + } + }, + "leoslideshow": { + "112312.jpg": { + "type": "-", + "size": 850740, + "lmtime": 1741161182782, + "modified": false + }, + "112312.webp": { + "type": "-", + "size": 115958, + "lmtime": 1741161182929, + "modified": false + }, + "1170x600 sleepmed.png": { + "type": "-", + "size": 624079, + "lmtime": 1741161183109, + "modified": false + }, + "1170x600 sleepmed.webp": { + "type": "-", + "size": 56494, + "lmtime": 1741161183229, + "modified": false + }, + "1200x628.webp": { + "type": "-", + "size": 44932, + "lmtime": 1741161183349, + "modified": false + }, + "121619077_796073391170617_822438870655195868_n-1.gif": { + "type": "-", + "size": 235794, + "lmtime": 1741161183494, + "modified": false + }, + "20 na argento.png": { + "type": "-", + "size": 313815, + "lmtime": 1741161184659, + "modified": false + }, + "20 na argento.webp": { + "type": "-", + "size": 58042, + "lmtime": 1741161184775, + "modified": false + }, + "20namoreno_mollyflex.png": { + "type": "-", + "size": 292988, + "lmtime": 1741161184932, + "modified": false + }, + "20namoreno_mollyflex.webp": { + "type": "-", + "size": 53984, + "lmtime": 1741161185063, + "modified": false + }, + "2.jpg": { + "type": "-", + "size": 411386, + "lmtime": 1741161185233, + "modified": false + }, + "2.webp": { + "type": "-", + "size": 41970, + "lmtime": 1741161185348, + "modified": false + }, + "alleluja.png": { + "type": "-", + "size": 479522, + "lmtime": 1741161185539, + "modified": false + }, + "alleluja.webp": { + "type": "-", + "size": 59306, + "lmtime": 1741161185664, + "modified": false + }, + "banerdrewmaxok.jpg": { + "type": "-", + "size": 404446, + "lmtime": 1741161185840, + "modified": false + }, + "banerdrewmaxok.webp": { + "type": "-", + "size": 92756, + "lmtime": 1741161185987, + "modified": false + }, + "Baner Janpol.webp": { + "type": "-", + "size": 36336, + "lmtime": 1741161186092, + "modified": false + }, + "banermollyflexjesien2020.webp": { + "type": "-", + "size": 128200, + "lmtime": 1741161186209, + "modified": false + }, + "Baner promocja.webp": { + "type": "-", + "size": 88098, + "lmtime": 1741161186344, + "modified": false + }, + "baner_www_przedwiosnie1.png": { + "type": "-", + "size": 478718, + "lmtime": 1741161186502, + "modified": false + }, + "baner_www_przedwiosnie1.webp": { + "type": "-", + "size": 61172, + "lmtime": 1741161186633, + "modified": false + }, + "baner_www_przedwiosnie.png": { + "type": "-", + "size": 1035350, + "lmtime": 1741161186830, + "modified": false + }, + "baner_www_przedwiosnie.webp": { + "type": "-", + "size": 160156, + "lmtime": 1741161186979, + "modified": false + }, + "baner_www_v2_baner_www.png": { + "type": "-", + "size": 143769, + "lmtime": 1741161187109, + "modified": false + }, + "baner_www_v2_baner_www.webp": { + "type": "-", + "size": 56032, + "lmtime": 1741161187242, + "modified": false + }, + "banner.jpg": { + "type": "-", + "size": 295318, + "lmtime": 1741161187417, + "modified": false + }, + "bg-slider-h1.png": { + "type": "-", + "size": 4988, + "lmtime": 1741161187521, + "modified": false + }, + "bg-slider-h1.webp": { + "type": "-", + "size": 238, + "lmtime": 1741161187625, + "modified": false + }, + "black_week_drmaterac.gif": { + "type": "-", + "size": 3997189, + "lmtime": 1741161188044, + "modified": false + }, + "Black Week - SMaterace 2021.png": { + "type": "-", + "size": 425366, + "lmtime": 1741161188212, + "modified": false + }, + "Black Week - SMaterace 2021.webp": { + "type": "-", + "size": 55424, + "lmtime": 1741161188319, + "modified": false + }, + "BWDrewmax1.jpg": { + "type": "-", + "size": 467392, + "lmtime": 1741161188476, + "modified": false + }, + "BWDrewmax1.webp": { + "type": "-", + "size": 94740, + "lmtime": 1741161188600, + "modified": false + }, + "darmowa dostawa.gif": { + "type": "-", + "size": 428861, + "lmtime": 1741161188752, + "modified": false + }, + "Dni Wloskie - 1121.png": { + "type": "-", + "size": 764404, + "lmtime": 1741161188951, + "modified": false + }, + "Dni Włoskie - 1121.png": { + "type": "-", + "size": 764404, + "lmtime": 1741161189119, + "modified": false + }, + "Dni Wloskie - 1121.webp": { + "type": "-", + "size": 92938, + "lmtime": 1741161189262, + "modified": false + }, + "Dni Włoskie - 1121.webp": { + "type": "-", + "size": 92938, + "lmtime": 1741161189381, + "modified": false + }, + "drewmax powrot do szkoly jpg.jpg": { + "type": "-", + "size": 549491, + "lmtime": 1741161189571, + "modified": false + }, + "drewmax powrot do szkoly jpg.webp": { + "type": "-", + "size": 115980, + "lmtime": 1741161189711, + "modified": false + }, + "drewmax powrot do szkoly.webp": { + "type": "-", + "size": 219302, + "lmtime": 1741161189860, + "modified": false + }, + "Drewmax promocja_baner_dr.webp": { + "type": "-", + "size": 75010, + "lmtime": 1741161189988, + "modified": false + }, + "drmaterac_4urodziny.png": { + "type": "-", + "size": 391229, + "lmtime": 1741161190156, + "modified": false + }, + "drmaterac_4urodziny.webp": { + "type": "-", + "size": 44082, + "lmtime": 1741161190278, + "modified": false + }, + "drMaterac_baner_www_23.03.21.png": { + "type": "-", + "size": 481723, + "lmtime": 1741161190447, + "modified": false + }, + "drMaterac_baner_www_23.03.21.webp": { + "type": "-", + "size": 55284, + "lmtime": 1741161190567, + "modified": false + }, + "Dr Materac projekty SM_listopad_Janpol PROMO_baner www v2.jpg": { + "type": "-", + "size": 581506, + "lmtime": 1741161190739, + "modified": false + }, + "Dr Materac projekty SM_listopad_Janpol PROMO_baner www v2.webp": { + "type": "-", + "size": 123960, + "lmtime": 1741161190874, + "modified": false + }, + "drmaterac_swieta.png": { + "type": "-", + "size": 233849, + "lmtime": 1741161191018, + "modified": false + }, + "drmaterac_swieta.webp": { + "type": "-", + "size": 61620, + "lmtime": 1741161191146, + "modified": false + }, + "drMaterac_walentynki_baner_wwwok.png": { + "type": "-", + "size": 465664, + "lmtime": 1741161191327, + "modified": false + }, + "drMaterac_walentynki_baner_wwwok.webp": { + "type": "-", + "size": 70606, + "lmtime": 1741161191450, + "modified": false + }, + "drmaterac_www.banner_.png": { + "type": "-", + "size": 155133, + "lmtime": 1741161191602, + "modified": false + }, + "drmaterac_www.banner_.webp": { + "type": "-", + "size": 49726, + "lmtime": 1741161191731, + "modified": false + }, + "Eko i natura dr materac slider a.png": { + "type": "-", + "size": 711972, + "lmtime": 1741161191915, + "modified": false + }, + "Eko i natura dr materac slider a.webp": { + "type": "-", + "size": 50530, + "lmtime": 1741161192039, + "modified": false + }, + "Eko i natura dr materac slider.png": { + "type": "-", + "size": 710565, + "lmtime": 1741161192227, + "modified": false + }, + "Eko i natura dr materac slider.webp": { + "type": "-", + "size": 49924, + "lmtime": 1741161192352, + "modified": false + }, + "fizjo Tarnow.png": { + "type": "-", + "size": 632855, + "lmtime": 1741161192538, + "modified": false + }, + "fizjo Tarnów.png": { + "type": "-", + "size": 632855, + "lmtime": 1741161192718, + "modified": false + }, + "fizjo Tarnow.webp": { + "type": "-", + "size": 39790, + "lmtime": 1741161192841, + "modified": false + }, + "fizjo Tarnów.webp": { + "type": "-", + "size": 39790, + "lmtime": 1741161192943, + "modified": false + }, + "h1.jpg": { + "type": "-", + "size": 232997, + "lmtime": 1741161193095, + "modified": false + }, + "h1.webp": { + "type": "-", + "size": 98646, + "lmtime": 1741161193229, + "modified": false + }, + "hila.webp": { + "type": "-", + "size": 168614, + "lmtime": 1741161193379, + "modified": false + }, + "Hilding - Slider z data.png": { + "type": "-", + "size": 228466, + "lmtime": 1741161193532, + "modified": false + }, + "Hilding - Slider z datą.png": { + "type": "-", + "size": 228466, + "lmtime": 1741161193684, + "modified": false + }, + "Hilding - Slider z data.webp": { + "type": "-", + "size": 31514, + "lmtime": 1741161193819, + "modified": false + }, + "Hilding - Slider z datą.webp": { + "type": "-", + "size": 31514, + "lmtime": 1741161193925, + "modified": false + }, + "hilding.webp": { + "type": "-", + "size": 62410, + "lmtime": 1741161194045, + "modified": false + }, + "hil.webp": { + "type": "-", + "size": 58526, + "lmtime": 1741161194174, + "modified": false + }, + "h jesien 2022.png": { + "type": "-", + "size": 705028, + "lmtime": 1741161194341, + "modified": false + }, + "h jesien 2022.webp": { + "type": "-", + "size": 46476, + "lmtime": 1741161194463, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161194550, + "modified": false + }, + "janpol_15.png": { + "type": "-", + "size": 740938, + "lmtime": 1741161194743, + "modified": false + }, + "janpol_15.webp": { + "type": "-", + "size": 124020, + "lmtime": 1741161194887, + "modified": false + }, + "Janpol baner jesien.png": { + "type": "-", + "size": 574808, + "lmtime": 1741161195053, + "modified": false + }, + "Janpol baner jesien.webp": { + "type": "-", + "size": 54750, + "lmtime": 1741161195170, + "modified": false + }, + "Janpol baner www png.png": { + "type": "-", + "size": 574808, + "lmtime": 1741161195351, + "modified": false + }, + "Janpol baner www png.webp": { + "type": "-", + "size": 57884, + "lmtime": 1741161195468, + "modified": false + }, + "Janpol baner www tablet - data + materace - 1280 x 640 px.jpg": { + "type": "-", + "size": 173039, + "lmtime": 1741161195706, + "modified": false + }, + "Janpol promocja wiosenna 20222 na materace.webp": { + "type": "-", + "size": 42096, + "lmtime": 1741161195823, + "modified": false + }, + "Loteria - Slider.png": { + "type": "-", + "size": 281354, + "lmtime": 1741161195978, + "modified": false + }, + "Loteria - Slider.webp": { + "type": "-", + "size": 49082, + "lmtime": 1741161196093, + "modified": false + }, + "lulandia-slide-1.jpg": { + "type": "-", + "size": 681257, + "lmtime": 1741161196262, + "modified": false + }, + "lulandia-slide-1.webp": { + "type": "-", + "size": 60828, + "lmtime": 1741161196400, + "modified": false + }, + "Magniflex.jpg": { + "type": "-", + "size": 186049, + "lmtime": 1741161196552, + "modified": false + }, + "Magniflex.webp": { + "type": "-", + "size": 72700, + "lmtime": 1741161196679, + "modified": false + }, + "masaz mollyflex.jpg": { + "type": "-", + "size": 179578, + "lmtime": 1741161196819, + "modified": false + }, + "masaz mollyflex.webp": { + "type": "-", + "size": 23706, + "lmtime": 1741161196915, + "modified": false + }, + "Mielec - Slider.png": { + "type": "-", + "size": 666973, + "lmtime": 1741161197088, + "modified": false + }, + "Mielec - Slider.webp": { + "type": "-", + "size": 49154, + "lmtime": 1741161197209, + "modified": false + }, + "molly_11.webp": { + "type": "-", + "size": 68104, + "lmtime": 1741161197339, + "modified": false + }, + "mollyflex_40lat.png": { + "type": "-", + "size": 478014, + "lmtime": 1741161197505, + "modified": false + }, + "mollyflex_40lat.webp": { + "type": "-", + "size": 52638, + "lmtime": 1741161197615, + "modified": false + }, + "mollyflex_promocja.png": { + "type": "-", + "size": 723167, + "lmtime": 1741161197782, + "modified": false + }, + "mollyflex_promocja.webp": { + "type": "-", + "size": 50296, + "lmtime": 1741161197891, + "modified": false + }, + "mollyflex_przedwiosnie1.png": { + "type": "-", + "size": 1203911, + "lmtime": 1741161198091, + "modified": false + }, + "mollyflex_przedwiosnie1.webp": { + "type": "-", + "size": 133318, + "lmtime": 1741161198212, + "modified": false + }, + "molly wiosenne gratisy slider2.png": { + "type": "-", + "size": 275639, + "lmtime": 1741161198364, + "modified": false + }, + "molly wiosenne gratisy slider2.webp": { + "type": "-", + "size": 33452, + "lmtime": 1741161198470, + "modified": false + }, + "molly wiosenne gratisy slider.png": { + "type": "-", + "size": 268367, + "lmtime": 1741161198639, + "modified": false + }, + "molly wiosenne gratisy slider.webp": { + "type": "-", + "size": 39894, + "lmtime": 1741161198760, + "modified": false + }, + "Myslenice Slider Otwarcie ok.png": { + "type": "-", + "size": 438695, + "lmtime": 1741161198922, + "modified": false + }, + "Myślenice Slider Otwarcie ok.png": { + "type": "-", + "size": 438695, + "lmtime": 1741161199080, + "modified": false + }, + "Myslenice Slider Otwarcie ok.webp": { + "type": "-", + "size": 75142, + "lmtime": 1741161199207, + "modified": false + }, + "Myślenice Slider Otwarcie ok.webp": { + "type": "-", + "size": 75142, + "lmtime": 1741161199348, + "modified": false + }, + "Myslenice Slider Otwarcie.png": { + "type": "-", + "size": 438695, + "lmtime": 1741161199520, + "modified": false + }, + "Myślenice Slider Otwarcie.png": { + "type": "-", + "size": 438695, + "lmtime": 1741161199691, + "modified": false + }, + "Myslenice Slider Otwarcie.webp": { + "type": "-", + "size": 74808, + "lmtime": 1741161199820, + "modified": false + }, + "Myślenice Slider Otwarcie.webp": { + "type": "-", + "size": 74808, + "lmtime": 1741161199953, + "modified": false + }, + "naturalne H 25 jpg.jpg": { + "type": "-", + "size": 590317, + "lmtime": 1741161200113, + "modified": false + }, + "naturalne H 25 jpg.webp": { + "type": "-", + "size": 123698, + "lmtime": 1741161200261, + "modified": false + }, + "ND-Silider_Taniej-o-Vat.jpg": { + "type": "-", + "size": 342646, + "lmtime": 1741161200421, + "modified": false + }, + "ND-Silider_Taniej-o-Vat.webp": { + "type": "-", + "size": 67092, + "lmtime": 1741161200537, + "modified": false + }, + "newsletter5.gif": { + "type": "-", + "size": 4373483, + "lmtime": 1741161200960, + "modified": false + }, + "newsletter.gif": { + "type": "-", + "size": 221024, + "lmtime": 1741161201113, + "modified": false + }, + "Nowy Sacz otwarcie - Slider.png": { + "type": "-", + "size": 247366, + "lmtime": 1741161201281, + "modified": false + }, + "Nowy Sącz otwarcie - Slider.png": { + "type": "-", + "size": 247366, + "lmtime": 1741161201447, + "modified": false + }, + "Nowy Sącz otwarcie - Slider.png": { + "type": "-", + "size": 247366, + "lmtime": 1741161201619, + "modified": false + }, + "Nowy Sacz otwarcie - Slider.webp": { + "type": "-", + "size": 58030, + "lmtime": 1741161201849, + "modified": false + }, + "Nowy Sącz otwarcie - Slider.webp": { + "type": "-", + "size": 58030, + "lmtime": 1741161202024, + "modified": false + }, + "Nowy Sącz otwarcie - Slider.webp": { + "type": "-", + "size": 58030, + "lmtime": 1741161202175, + "modified": false + }, + "ok.png": { + "type": "-", + "size": 710565, + "lmtime": 1741161202350, + "modified": false + }, + "ok.webp": { + "type": "-", + "size": 49998, + "lmtime": 1741161202477, + "modified": false + }, + "prezenty_baner_slider_drmaterac.png": { + "type": "-", + "size": 407858, + "lmtime": 1741161202647, + "modified": false + }, + "prezenty_baner_slider_drmaterac.webp": { + "type": "-", + "size": 48344, + "lmtime": 1741161202774, + "modified": false + }, + "promocja-hilding-20.jpg": { + "type": "-", + "size": 720647, + "lmtime": 1741161202953, + "modified": false + }, + "Promocja Hilding -20 jpg.webp": { + "type": "-", + "size": 80380, + "lmtime": 1741161203077, + "modified": false + }, + "promocja-hilding-20.webp": { + "type": "-", + "size": 114164, + "lmtime": 1741161203208, + "modified": false + }, + "Promocja Hilding -20.webp": { + "type": "-", + "size": 131510, + "lmtime": 1741161203356, + "modified": false + }, + "promocja+hilding+drmaterac.jpg": { + "type": "-", + "size": 138613, + "lmtime": 1741161203510, + "modified": false + }, + "promocja_hilding_materace.jpg": { + "type": "-", + "size": 330109, + "lmtime": 1741161203669, + "modified": false + }, + "promocja_hilding_materace.webp": { + "type": "-", + "size": 56002, + "lmtime": 1741161203795, + "modified": false + }, + "promocja na dzien matki tempur.jpg": { + "type": "-", + "size": 263586, + "lmtime": 1741161203956, + "modified": false + }, + "promocja na dzień matki tempur.jpg": { + "type": "-", + "size": 263586, + "lmtime": 1741161204097, + "modified": false + }, + "promocja na dzien matki tempur.webp": { + "type": "-", + "size": 31318, + "lmtime": 1741161204196, + "modified": false + }, + "promocja na dzień matki tempur.webp": { + "type": "-", + "size": 31318, + "lmtime": 1741161204318, + "modified": false + }, + "Promocja na Magnistretch materace rozciazgajace kregoslup.png": { + "type": "-", + "size": 708337, + "lmtime": 1741161204497, + "modified": false + }, + "Promocja na Magnistretch materace rozciazgajace kregoslup.webp": { + "type": "-", + "size": 58572, + "lmtime": 1741161204618, + "modified": false + }, + "promocja_na_materace_hilding.png": { + "type": "-", + "size": 555531, + "lmtime": 1741161204795, + "modified": false + }, + "promocja_na_materace_hilding.webp": { + "type": "-", + "size": 43710, + "lmtime": 1741161204909, + "modified": false + }, + "Promocja na materace Janpol.png": { + "type": "-", + "size": 756409, + "lmtime": 1741161205092, + "modified": false + }, + "Promocja na materace Janpol.webp": { + "type": "-", + "size": 68352, + "lmtime": 1741161205221, + "modified": false + }, + "Promocja na wloskie materace Mollyflex jpg.jpg": { + "type": "-", + "size": 178646, + "lmtime": 1741161205368, + "modified": false + }, + "Promocja na włoskie materace Mollyflex jpg.jpg": { + "type": "-", + "size": 178646, + "lmtime": 1741161205519, + "modified": false + }, + "Promocja na wloskie materace Mollyflex jpg.webp": { + "type": "-", + "size": 68494, + "lmtime": 1741161205646, + "modified": false + }, + "Promocja na włoskie materace Mollyflex jpg.webp": { + "type": "-", + "size": 68494, + "lmtime": 1741161205777, + "modified": false + }, + "Promocja www hilding lato 2021 a.png": { + "type": "-", + "size": 200196, + "lmtime": 1741161205917, + "modified": false + }, + "Promocja www hilding lato 2021 a.webp": { + "type": "-", + "size": 25124, + "lmtime": 1741161206034, + "modified": false + }, + "Przyjemne ochlodzenie.png": { + "type": "-", + "size": 753688, + "lmtime": 1741161206221, + "modified": false + }, + "Przyjemne ochłodzenie.png": { + "type": "-", + "size": 753688, + "lmtime": 1741161206402, + "modified": false + }, + "Przyjemne ochlodzenie.webp": { + "type": "-", + "size": 67060, + "lmtime": 1741161206527, + "modified": false + }, + "Przyjemne ochłodzenie.webp": { + "type": "-", + "size": 67060, + "lmtime": 1741161206667, + "modified": false + }, + "Rabat na materace Hilding.png": { + "type": "-", + "size": 621578, + "lmtime": 1741161206837, + "modified": false + }, + "Rabat na materace Hilding.webp": { + "type": "-", + "size": 44596, + "lmtime": 1741161206954, + "modified": false + }, + "Rabat na materace polskiego porducenta Janpol.png": { + "type": "-", + "size": 504582, + "lmtime": 1741161207116, + "modified": false + }, + "Rabat na materace polskiego porducenta Janpol.webp": { + "type": "-", + "size": 57712, + "lmtime": 1741161207243, + "modified": false + }, + "Sertasealy.png": { + "type": "-", + "size": 275408, + "lmtime": 1741161207389, + "modified": false + }, + "Sertasealy.webp": { + "type": "-", + "size": 34986, + "lmtime": 1741161207501, + "modified": false + }, + "sleepmedpromo11.png": { + "type": "-", + "size": 265841, + "lmtime": 1741161207668, + "modified": false + }, + "sleepmedpromo11.webp": { + "type": "-", + "size": 37380, + "lmtime": 1741161207794, + "modified": false + }, + "SleepmedPromocja.png": { + "type": "-", + "size": 292725, + "lmtime": 1741161207956, + "modified": false + }, + "SleepmedPromocja.webp": { + "type": "-", + "size": 35098, + "lmtime": 1741161208071, + "modified": false + }, + "slide-1-h1.webp": { + "type": "-", + "size": 30486, + "lmtime": 1741161208189, + "modified": false + }, + "slide-1-h4.jpg": { + "type": "-", + "size": 182570, + "lmtime": 1741161208338, + "modified": false + }, + "slide-1-h4.webp": { + "type": "-", + "size": 97650, + "lmtime": 1741161208482, + "modified": false + }, + "slide-1.webp": { + "type": "-", + "size": 16450, + "lmtime": 1741161208586, + "modified": false + }, + "slide-2-h1.webp": { + "type": "-", + "size": 25772, + "lmtime": 1741161208699, + "modified": false + }, + "slide-2-h4.jpg": { + "type": "-", + "size": 217194, + "lmtime": 1741161208843, + "modified": false + }, + "slide-2-h4.webp": { + "type": "-", + "size": 130130, + "lmtime": 1741161208992, + "modified": false + }, + "slider 1170x600 sleepmed.png": { + "type": "-", + "size": 670150, + "lmtime": 1741161209178, + "modified": false + }, + "slider 1170x600 sleepmed.webp": { + "type": "-", + "size": 57766, + "lmtime": 1741161209279, + "modified": false + }, + "slider dr materac (1).png": { + "type": "-", + "size": 911355, + "lmtime": 1741161209440, + "modified": false + }, + "slider dr materac (1).webp": { + "type": "-", + "size": 57242, + "lmtime": 1741161209544, + "modified": false + }, + "slider dr materac wiosna 2022.png": { + "type": "-", + "size": 632064, + "lmtime": 1741161209701, + "modified": false + }, + "slider dr materac wiosna 2022.webp": { + "type": "-", + "size": 43432, + "lmtime": 1741161209795, + "modified": false + }, + "slider hiszpanskie wakacje.png": { + "type": "-", + "size": 542380, + "lmtime": 1741161209958, + "modified": false + }, + "slider hiszpańskie wakacje.png": { + "type": "-", + "size": 542380, + "lmtime": 1741161210106, + "modified": false + }, + "slider hiszpanskie wakacje.webp": { + "type": "-", + "size": 45426, + "lmtime": 1741161210202, + "modified": false + }, + "slider hiszpańskie wakacje.webp": { + "type": "-", + "size": 45426, + "lmtime": 1741161210304, + "modified": false + }, + "slider janpol 20.png": { + "type": "-", + "size": 699472, + "lmtime": 1741161210467, + "modified": false + }, + "slider janpol 20.webp": { + "type": "-", + "size": 79960, + "lmtime": 1741161210571, + "modified": false + }, + "slider.png": { + "type": "-", + "size": 663, + "lmtime": 1741161210641, + "modified": false + }, + "Slider - Promocje od serca.png": { + "type": "-", + "size": 770190, + "lmtime": 1741161210841, + "modified": false + }, + "Slider - Promocje od serca.webp": { + "type": "-", + "size": 96740, + "lmtime": 1741161210954, + "modified": false + }, + "Slider Tempur poduszka.png": { + "type": "-", + "size": 312548, + "lmtime": 1741161211090, + "modified": false + }, + "Slider Tempur poduszka.webp": { + "type": "-", + "size": 42448, + "lmtime": 1741161211185, + "modified": false + }, + "Slider - Walentynki.png": { + "type": "-", + "size": 703653, + "lmtime": 1741161211341, + "modified": false + }, + "Slider - Walentynki.webp": { + "type": "-", + "size": 73914, + "lmtime": 1741161211447, + "modified": false + }, + "slider.webp": { + "type": "-", + "size": 90, + "lmtime": 1741161211649, + "modified": false + }, + "slideshow.png": { + "type": "-", + "size": 50365, + "lmtime": 1741161211760, + "modified": false + }, + "slideshow.webp": { + "type": "-", + "size": 24194, + "lmtime": 1741161211845, + "modified": false + }, + "splendid.jpg": { + "type": "-", + "size": 143841, + "lmtime": 1741161211963, + "modified": false + }, + "splendid.webp": { + "type": "-", + "size": 53540, + "lmtime": 1741161212066, + "modified": false + }, + "swmolly.webp": { + "type": "-", + "size": 38614, + "lmtime": 1741161212159, + "modified": false + }, + "szalona promocja.gif": { + "type": "-", + "size": 256747, + "lmtime": 1741161212293, + "modified": false + }, + "taniej o VAT slider www new design.jpg": { + "type": "-", + "size": 207862, + "lmtime": 1741161212425, + "modified": false + }, + "taniej o VAT slider www new design.webp": { + "type": "-", + "size": 110170, + "lmtime": 1741161212542, + "modified": false + }, + "Tarnow - Slider.png": { + "type": "-", + "size": 663623, + "lmtime": 1741161212695, + "modified": false + }, + "Tarnów - Slider.png": { + "type": "-", + "size": 663623, + "lmtime": 1741161212858, + "modified": false + }, + "Tarnow - Slider.webp": { + "type": "-", + "size": 67658, + "lmtime": 1741161212964, + "modified": false + }, + "Tarnów - Slider.webp": { + "type": "-", + "size": 67658, + "lmtime": 1741161213074, + "modified": false + }, + "tempurok.webp": { + "type": "-", + "size": 54190, + "lmtime": 1741161213172, + "modified": false + }, + "tempur ost.png": { + "type": "-", + "size": 846598, + "lmtime": 1741161213343, + "modified": false + }, + "tempur ost.webp": { + "type": "-", + "size": 56070, + "lmtime": 1741161213449, + "modified": false + }, + "TEMPUR.webp": { + "type": "-", + "size": 54190, + "lmtime": 1741161213549, + "modified": false + }, + "Urodziny dr materac rabaty na materace.png": { + "type": "-", + "size": 523158, + "lmtime": 1741161213697, + "modified": false + }, + "Urodziny dr materac rabaty na materace.webp": { + "type": "-", + "size": 49238, + "lmtime": 1741161213805, + "modified": false + }, + "urodziny_janpol_baner1.png": { + "type": "-", + "size": 226730, + "lmtime": 1741161213943, + "modified": false + }, + "urodziny_janpol_baner1.webp": { + "type": "-", + "size": 39860, + "lmtime": 1741161214041, + "modified": false + }, + "Urodziny - Slider.png": { + "type": "-", + "size": 226435, + "lmtime": 1741161214180, + "modified": false + }, + "Urodziny - Slider.webp": { + "type": "-", + "size": 39846, + "lmtime": 1741161214275, + "modified": false + }, + "voucher_drmaterac_karta_upominkowa.png": { + "type": "-", + "size": 548535, + "lmtime": 1741161214421, + "modified": false + }, + "voucher_drmaterac_karta_upominkowa.webp": { + "type": "-", + "size": 36906, + "lmtime": 1741161214512, + "modified": false + }, + "Wietrzenie Magazynow - Slider.png": { + "type": "-", + "size": 250095, + "lmtime": 1741161214645, + "modified": false + }, + "Wietrzenie Magazynów - Slider.png": { + "type": "-", + "size": 250095, + "lmtime": 1741161214778, + "modified": false + }, + "Wietrzenie Magazynów - Slider.png": { + "type": "-", + "size": 250095, + "lmtime": 1741161214910, + "modified": false + }, + "Wietrzenie Magazynow - Slider.webp": { + "type": "-", + "size": 62424, + "lmtime": 1741161215010, + "modified": false + }, + "Wietrzenie Magazynów - Slider.webp": { + "type": "-", + "size": 62424, + "lmtime": 1741161215115, + "modified": false + }, + "Wietrzenie Magazynów - Slider.webp": { + "type": "-", + "size": 62424, + "lmtime": 1741161215221, + "modified": false + }, + "Wiosenne promocje w dr materac.png": { + "type": "-", + "size": 354750, + "lmtime": 1741161215360, + "modified": false + }, + "Wiosenne promocje w dr materac.webp": { + "type": "-", + "size": 72722, + "lmtime": 1741161215472, + "modified": false + }, + "ZG otwarcie sklepu z materacami w Zielonej Gorze.png": { + "type": "-", + "size": 458581, + "lmtime": 1741161215621, + "modified": false + }, + "ZG otwarcie sklepu z materacami w Zielonej Górze.png": { + "type": "-", + "size": 458581, + "lmtime": 1741161215778, + "modified": false + }, + "ZG otwarcie sklepu z materacami w Zielonej Gorze.webp": { + "type": "-", + "size": 64776, + "lmtime": 1741161215879, + "modified": false + }, + "ZG otwarcie sklepu z materacami w Zielonej Górze.webp": { + "type": "-", + "size": 64776, + "lmtime": 1741161215987, + "modified": false + }, + "Zielona gora Slider Otwarcie (2).png": { + "type": "-", + "size": 494414, + "lmtime": 1741161216143, + "modified": false + }, + "Zielona góra Slider Otwarcie (2).png": { + "type": "-", + "size": 494414, + "lmtime": 1741161216285, + "modified": false + }, + "Zielona gora Slider Otwarcie (2).webp": { + "type": "-", + "size": 70396, + "lmtime": 1741161216389, + "modified": false + }, + "Zielona góra Slider Otwarcie (2).webp": { + "type": "-", + "size": 70396, + "lmtime": 1741161216497, + "modified": false + }, + "Zory - Slider.png": { + "type": "-", + "size": 517502, + "lmtime": 1741161216660, + "modified": false + }, + "Żory - Slider.png": { + "type": "-", + "size": 517502, + "lmtime": 1741161216809, + "modified": false + }, + "Żory - Slider.png": { + "type": "-", + "size": 517502, + "lmtime": 1741161216959, + "modified": false + }, + "Zory - Slider.webp": { + "type": "-", + "size": 70598, + "lmtime": 1741161217078, + "modified": false + }, + "Żory - Slider.webp": { + "type": "-", + "size": 70598, + "lmtime": 1741161217201, + "modified": false + }, + "Żory - Slider.webp": { + "type": "-", + "size": 70598, + "lmtime": 1741161217313, + "modified": false + }, + "Zyczenia 2021 - Slider.png": { + "type": "-", + "size": 194400, + "lmtime": 1741161217437, + "modified": false + }, + "Życzenia 2021 - Slider.png": { + "type": "-", + "size": 194400, + "lmtime": 1741161217563, + "modified": false + }, + "Życzenia 2021 - Slider.png": { + "type": "-", + "size": 194400, + "lmtime": 1741161217692, + "modified": false + }, + "Zyczenia 2021 - Slider.webp": { + "type": "-", + "size": 47802, + "lmtime": 1741161217803, + "modified": false + }, + "Życzenia 2021 - Slider.webp": { + "type": "-", + "size": 47802, + "lmtime": 1741161217904, + "modified": false + }, + "Życzenia 2021 - Slider.webp": { + "type": "-", + "size": 47802, + "lmtime": 1741161218002, + "modified": false + } + } + }, + "[my]ikona_account_15.svg": { + "type": "-", + "size": 908, + "lmtime": 1741161159494, + "modified": false + }, + "[my]ikona_account_20.svg": { + "type": "-", + "size": 902, + "lmtime": 1741161159578, + "modified": false + }, + "[my]ikona_account_30.svg": { + "type": "-", + "size": 893, + "lmtime": 1741161159670, + "modified": false + }, + "[my]ikona_account.svg": { + "type": "-", + "size": 887, + "lmtime": 1741161159762, + "modified": false + }, + "[my]ikona_call_center.svg": { + "type": "-", + "size": 1737, + "lmtime": 1741161159837, + "modified": false + }, + "patterns": { + "full-screen": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161220459, + "modified": false + }, + "style-1.jpg": { + "type": "-", + "size": 73294, + "lmtime": 1741161220563, + "modified": false + }, + "style-2.jpg": { + "type": "-", + "size": 73294, + "lmtime": 1741161220672, + "modified": false + }, + "style-3.jpg": { + "type": "-", + "size": 73294, + "lmtime": 1741161220787, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161218073, + "modified": false + }, + "pattern10.png": { + "type": "-", + "size": 10582, + "lmtime": 1741161218148, + "modified": false + }, + "pattern10.webp": { + "type": "-", + "size": 10586, + "lmtime": 1741161218232, + "modified": false + }, + "pattern11.png": { + "type": "-", + "size": 14426, + "lmtime": 1741161218305, + "modified": false + }, + "pattern11.webp": { + "type": "-", + "size": 12300, + "lmtime": 1741161218377, + "modified": false + }, + "pattern12.png": { + "type": "-", + "size": 20145, + "lmtime": 1741161218460, + "modified": false + }, + "pattern12.webp": { + "type": "-", + "size": 9276, + "lmtime": 1741161218533, + "modified": false + }, + "pattern13.png": { + "type": "-", + "size": 11163, + "lmtime": 1741161218611, + "modified": false + }, + "pattern13.webp": { + "type": "-", + "size": 2496, + "lmtime": 1741161218679, + "modified": false + }, + "pattern1.png": { + "type": "-", + "size": 10498, + "lmtime": 1741161218757, + "modified": false + }, + "pattern1.webp": { + "type": "-", + "size": 5278, + "lmtime": 1741161218831, + "modified": false + }, + "pattern2.png": { + "type": "-", + "size": 6128, + "lmtime": 1741161218903, + "modified": false + }, + "pattern2.webp": { + "type": "-", + "size": 5806, + "lmtime": 1741161218979, + "modified": false + }, + "pattern3.png": { + "type": "-", + "size": 6092, + "lmtime": 1741161219045, + "modified": false + }, + "pattern3.webp": { + "type": "-", + "size": 3888, + "lmtime": 1741161219136, + "modified": false + }, + "pattern4.png": { + "type": "-", + "size": 1722, + "lmtime": 1741161219218, + "modified": false + }, + "pattern4.webp": { + "type": "-", + "size": 1560, + "lmtime": 1741161219306, + "modified": false + }, + "pattern5.png": { + "type": "-", + "size": 162617, + "lmtime": 1741161219437, + "modified": false + }, + "pattern5.webp": { + "type": "-", + "size": 159672, + "lmtime": 1741161219573, + "modified": false + }, + "pattern6.png": { + "type": "-", + "size": 23732, + "lmtime": 1741161219677, + "modified": false + }, + "pattern6.webp": { + "type": "-", + "size": 3046, + "lmtime": 1741161219761, + "modified": false + }, + "pattern7.png": { + "type": "-", + "size": 171, + "lmtime": 1741161219837, + "modified": false + }, + "pattern7.webp": { + "type": "-", + "size": 120, + "lmtime": 1741161219907, + "modified": false + }, + "pattern8.png": { + "type": "-", + "size": 9141, + "lmtime": 1741161219989, + "modified": false + }, + "pattern8.webp": { + "type": "-", + "size": 8548, + "lmtime": 1741161220069, + "modified": false + }, + "pattern9.png": { + "type": "-", + "size": 16189, + "lmtime": 1741161220154, + "modified": false + }, + "pattern9.webp": { + "type": "-", + "size": 16402, + "lmtime": 1741161220238, + "modified": false + }, + "pattern.png": { + "type": "-", + "size": 117, + "lmtime": 1741161220313, + "modified": false + }, + "pattern.webp": { + "type": "-", + "size": 34, + "lmtime": 1741161220380, + "modified": false + } + }, + "phone-call-svgrepo-com.svg": { + "type": "-", + "size": 783, + "lmtime": 1741161159937, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161144182, + "modified": false + }, + "js": { + "custom.js": { + "type": "-", + "size": 27851, + "lmtime": 1741161220878, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161220953, + "modified": false + }, + "paypo.png": { + "type": "-", + "size": 3700, + "lmtime": 1741161221023, + "modified": false + }, + "theme.js": { + "type": "-", + "size": 173541, + "lmtime": 1741161221147, + "modified": false + } + }, + "spacery": { + "drmateracchelm": { + "build0.xml": { + "type": "-", + "size": 5844, + "lmtime": 1741161221232, + "modified": false + }, + "builddata": { + "build0": { + "0": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 109142, + "lmtime": 1741161222578, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 76302, + "lmtime": 1741161222683, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 26622, + "lmtime": 1741161222768, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 86535, + "lmtime": 1741161222884, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 33738, + "lmtime": 1741161222974, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 92448, + "lmtime": 1741161223091, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 36594, + "lmtime": 1741161223194, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 116470, + "lmtime": 1741161223310, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 56482, + "lmtime": 1741161223412, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 48881, + "lmtime": 1741161223510, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 51917, + "lmtime": 1741161223614, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 60019, + "lmtime": 1741161223724, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 65351, + "lmtime": 1741161223835, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 85036, + "lmtime": 1741161223945, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 76624, + "lmtime": 1741161224053, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 84704, + "lmtime": 1741161224161, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 93630, + "lmtime": 1741161224274, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 92462, + "lmtime": 1741161224384, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 95320, + "lmtime": 1741161224502, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 110252, + "lmtime": 1741161224623, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 113944, + "lmtime": 1741161224741, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 63510, + "lmtime": 1741161224853, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 75383, + "lmtime": 1741161224964, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 81984, + "lmtime": 1741161225071, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 91585, + "lmtime": 1741161225191, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 48696, + "lmtime": 1741161222124, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 24320, + "lmtime": 1741161222214, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 136390, + "lmtime": 1741161222347, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 70152, + "lmtime": 1741161222465, + "modified": false + } + }, + "1": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 118067, + "lmtime": 1741161225740, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 73258, + "lmtime": 1741161225853, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 23392, + "lmtime": 1741161225956, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 77981, + "lmtime": 1741161226064, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 29610, + "lmtime": 1741161226155, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 127489, + "lmtime": 1741161226281, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 68478, + "lmtime": 1741161226395, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 164984, + "lmtime": 1741161226519, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 109308, + "lmtime": 1741161226650, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 72071, + "lmtime": 1741161226770, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 74906, + "lmtime": 1741161226884, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 77010, + "lmtime": 1741161226991, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 53382, + "lmtime": 1741161227090, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 71678, + "lmtime": 1741161227197, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 67997, + "lmtime": 1741161227311, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 66465, + "lmtime": 1741161227419, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 81471, + "lmtime": 1741161227528, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 112533, + "lmtime": 1741161227641, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 123555, + "lmtime": 1741161227798, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 123185, + "lmtime": 1741161227979, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 134527, + "lmtime": 1741161228105, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 101992, + "lmtime": 1741161228216, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 122208, + "lmtime": 1741161228333, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 145048, + "lmtime": 1741161228450, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 158381, + "lmtime": 1741161228572, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 49884, + "lmtime": 1741161225295, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 25522, + "lmtime": 1741161225390, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 157421, + "lmtime": 1741161225514, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 97832, + "lmtime": 1741161225625, + "modified": false + } + }, + "2": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 110846, + "lmtime": 1741161229125, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 67023, + "lmtime": 1741161229266, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 20994, + "lmtime": 1741161229376, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 53571, + "lmtime": 1741161229498, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 14474, + "lmtime": 1741161229572, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 151457, + "lmtime": 1741161229686, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 91932, + "lmtime": 1741161229804, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 130197, + "lmtime": 1741161229928, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 67494, + "lmtime": 1741161230040, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 46148, + "lmtime": 1741161230144, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 41642, + "lmtime": 1741161230238, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 41612, + "lmtime": 1741161230348, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 50648, + "lmtime": 1741161230445, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 73160, + "lmtime": 1741161230547, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 80296, + "lmtime": 1741161230655, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 62512, + "lmtime": 1741161230761, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 54362, + "lmtime": 1741161230864, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 122388, + "lmtime": 1741161230991, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 130199, + "lmtime": 1741161231129, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 118233, + "lmtime": 1741161231255, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 85330, + "lmtime": 1741161231380, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 135077, + "lmtime": 1741161231499, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 128887, + "lmtime": 1741161231627, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 128806, + "lmtime": 1741161231748, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 110413, + "lmtime": 1741161231861, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 47821, + "lmtime": 1741161228675, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 21772, + "lmtime": 1741161228765, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 142069, + "lmtime": 1741161228883, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 74424, + "lmtime": 1741161228988, + "modified": false + } + }, + "3": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 106652, + "lmtime": 1741161232398, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 67295, + "lmtime": 1741161232512, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 22568, + "lmtime": 1741161232597, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 70424, + "lmtime": 1741161232704, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 28034, + "lmtime": 1741161232802, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 130952, + "lmtime": 1741161232926, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 69730, + "lmtime": 1741161233054, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 113620, + "lmtime": 1741161233178, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 53180, + "lmtime": 1741161233295, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 48263, + "lmtime": 1741161233397, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 49000, + "lmtime": 1741161233522, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 45935, + "lmtime": 1741161233635, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 46398, + "lmtime": 1741161233758, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 72376, + "lmtime": 1741161233891, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 63258, + "lmtime": 1741161234051, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 72846, + "lmtime": 1741161234158, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 83721, + "lmtime": 1741161234272, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 116605, + "lmtime": 1741161234385, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 136217, + "lmtime": 1741161234512, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 140567, + "lmtime": 1741161234648, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 110698, + "lmtime": 1741161234768, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 94364, + "lmtime": 1741161234879, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 115417, + "lmtime": 1741161235003, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 89309, + "lmtime": 1741161235110, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 67521, + "lmtime": 1741161235213, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 47534, + "lmtime": 1741161231967, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 23298, + "lmtime": 1741161232062, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 137360, + "lmtime": 1741161232183, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 71012, + "lmtime": 1741161232287, + "modified": false + } + }, + "4": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 51813, + "lmtime": 1741161235686, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 40618, + "lmtime": 1741161235796, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 7264, + "lmtime": 1741161235884, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 62855, + "lmtime": 1741161236013, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 16222, + "lmtime": 1741161236122, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 38541, + "lmtime": 1741161236249, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 6438, + "lmtime": 1741161236341, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 69669, + "lmtime": 1741161236472, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 17548, + "lmtime": 1741161236590, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 37414, + "lmtime": 1741161236706, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 55351, + "lmtime": 1741161236833, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 56238, + "lmtime": 1741161236950, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 55271, + "lmtime": 1741161237071, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 33888, + "lmtime": 1741161237161, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 69626, + "lmtime": 1741161237287, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 99923, + "lmtime": 1741161237423, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 84569, + "lmtime": 1741161237552, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 35114, + "lmtime": 1741161237663, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 61286, + "lmtime": 1741161237778, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 81844, + "lmtime": 1741161237979, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 91888, + "lmtime": 1741161238111, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 36489, + "lmtime": 1741161238200, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 52877, + "lmtime": 1741161238301, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 59024, + "lmtime": 1741161238400, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 93099, + "lmtime": 1741161238514, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 23514, + "lmtime": 1741161235306, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 7490, + "lmtime": 1741161235378, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 71503, + "lmtime": 1741161235493, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 19760, + "lmtime": 1741161235577, + "modified": false + } + }, + "5": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 101668, + "lmtime": 1741161239138, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 72371, + "lmtime": 1741161239361, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 23950, + "lmtime": 1741161239475, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 88749, + "lmtime": 1741161239598, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 37038, + "lmtime": 1741161239692, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 98153, + "lmtime": 1741161239806, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 40724, + "lmtime": 1741161239908, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 114324, + "lmtime": 1741161240023, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 59644, + "lmtime": 1741161240123, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 70177, + "lmtime": 1741161240237, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 67378, + "lmtime": 1741161240348, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 77143, + "lmtime": 1741161240460, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 76382, + "lmtime": 1741161240568, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 77297, + "lmtime": 1741161240675, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 54053, + "lmtime": 1741161240784, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 57649, + "lmtime": 1741161240886, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 106327, + "lmtime": 1741161241000, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 95379, + "lmtime": 1741161241110, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 56513, + "lmtime": 1741161241212, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 59901, + "lmtime": 1741161241317, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 130535, + "lmtime": 1741161241432, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 84207, + "lmtime": 1741161241540, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 106797, + "lmtime": 1741161241665, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 104406, + "lmtime": 1741161241787, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 102205, + "lmtime": 1741161241907, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 41364, + "lmtime": 1741161238626, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 17634, + "lmtime": 1741161238735, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 128199, + "lmtime": 1741161238867, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 62740, + "lmtime": 1741161238991, + "modified": false + } + }, + "preview.jpg": { + "type": "-", + "size": 45027, + "lmtime": 1741161221938, + "modified": false + }, + "preview.webp": { + "type": "-", + "size": 27858, + "lmtime": 1741161222023, + "modified": false + } + }, + "graphics": { + "arrow.png": { + "type": "-", + "size": 310, + "lmtime": 1741161241984, + "modified": false + }, + "arrow.webp": { + "type": "-", + "size": 110, + "lmtime": 1741161242052, + "modified": false + }, + "gyro.js": { + "type": "-", + "size": 3786, + "lmtime": 1741161242133, + "modified": false + }, + "introImage.png": { + "type": "-", + "size": 508915, + "lmtime": 1741161242284, + "modified": false + }, + "introImage.webp": { + "type": "-", + "size": 47952, + "lmtime": 1741161242391, + "modified": false + }, + "menu": { + "help.png": { + "type": "-", + "size": 257932, + "lmtime": 1741161243060, + "modified": false + }, + "help.webp": { + "type": "-", + "size": 73678, + "lmtime": 1741161243171, + "modified": false + }, + "menu0.png": { + "type": "-", + "size": 5083, + "lmtime": 1741161243250, + "modified": false + }, + "menu0.webp": { + "type": "-", + "size": 940, + "lmtime": 1741161243316, + "modified": false + }, + "menu10.png": { + "type": "-", + "size": 11039, + "lmtime": 1741161243402, + "modified": false + }, + "menu10.webp": { + "type": "-", + "size": 1758, + "lmtime": 1741161243479, + "modified": false + }, + "menu11.png": { + "type": "-", + "size": 4910, + "lmtime": 1741161243554, + "modified": false + }, + "menu12.png": { + "type": "-", + "size": 4952, + "lmtime": 1741161243630, + "modified": false + }, + "menu13.png": { + "type": "-", + "size": 2937, + "lmtime": 1741161243700, + "modified": false + }, + "menu1.png": { + "type": "-", + "size": 4327, + "lmtime": 1741161243779, + "modified": false + }, + "menu1.webp": { + "type": "-", + "size": 596, + "lmtime": 1741161243956, + "modified": false + }, + "menu2.png": { + "type": "-", + "size": 6124, + "lmtime": 1741161244027, + "modified": false + }, + "menu2.webp": { + "type": "-", + "size": 1144, + "lmtime": 1741161244095, + "modified": false + }, + "menu3.png": { + "type": "-", + "size": 6335, + "lmtime": 1741161244165, + "modified": false + }, + "menu3.webp": { + "type": "-", + "size": 1184, + "lmtime": 1741161244251, + "modified": false + }, + "menu4.png": { + "type": "-", + "size": 6194, + "lmtime": 1741161244335, + "modified": false + }, + "menu4.webp": { + "type": "-", + "size": 1228, + "lmtime": 1741161244406, + "modified": false + }, + "menu5.png": { + "type": "-", + "size": 6296, + "lmtime": 1741161244488, + "modified": false + }, + "menu5.webp": { + "type": "-", + "size": 1264, + "lmtime": 1741161244568, + "modified": false + }, + "menu6.png": { + "type": "-", + "size": 7160, + "lmtime": 1741161244639, + "modified": false + }, + "menu6.webp": { + "type": "-", + "size": 1800, + "lmtime": 1741161244709, + "modified": false + }, + "menu7.png": { + "type": "-", + "size": 6794, + "lmtime": 1741161244783, + "modified": false + }, + "menu7.webp": { + "type": "-", + "size": 1698, + "lmtime": 1741161244864, + "modified": false + }, + "menu8.png": { + "type": "-", + "size": 7058, + "lmtime": 1741161244944, + "modified": false + }, + "menu8.webp": { + "type": "-", + "size": 1376, + "lmtime": 1741161245014, + "modified": false + }, + "menu9.png": { + "type": "-", + "size": 13096, + "lmtime": 1741161245097, + "modified": false + }, + "menu9.webp": { + "type": "-", + "size": 3550, + "lmtime": 1741161245172, + "modified": false + } + }, + "slide.png": { + "type": "-", + "size": 2819, + "lmtime": 1741161242463, + "modified": false + }, + "slide.webp": { + "type": "-", + "size": 34, + "lmtime": 1741161242541, + "modified": false + }, + "spots": { + "object0.jpg": { + "type": "-", + "size": 88770, + "lmtime": 1741161245285, + "modified": false + }, + "object0.webp": { + "type": "-", + "size": 57520, + "lmtime": 1741161245389, + "modified": false + }, + "object1.jpg": { + "type": "-", + "size": 63338, + "lmtime": 1741161245498, + "modified": false + }, + "object1.webp": { + "type": "-", + "size": 34702, + "lmtime": 1741161245600, + "modified": false + }, + "spot0.png": { + "type": "-", + "size": 1439, + "lmtime": 1741161245679, + "modified": false + }, + "spot0.webp": { + "type": "-", + "size": 902, + "lmtime": 1741161245749, + "modified": false + } + }, + "strap.png": { + "type": "-", + "size": 3452, + "lmtime": 1741161242615, + "modified": false + }, + "strap.webp": { + "type": "-", + "size": 326, + "lmtime": 1741161242685, + "modified": false + }, + "thumb_scroll.png": { + "type": "-", + "size": 182, + "lmtime": 1741161242766, + "modified": false + }, + "transparent.png": { + "type": "-", + "size": 234, + "lmtime": 1741161242855, + "modified": false + }, + "transparent.webp": { + "type": "-", + "size": 34, + "lmtime": 1741161242925, + "modified": false + } + } + }, + "build.html": { + "type": "-", + "size": 1244, + "lmtime": 1741161221307, + "modified": false + }, + "build.swf": { + "type": "-", + "size": 118974, + "lmtime": 1741161221418, + "modified": false + }, + "build.xml": { + "type": "-", + "size": 11146, + "lmtime": 1741161221498, + "modified": false + }, + "index.html": { + "type": "-", + "size": 1305, + "lmtime": 1741161221576, + "modified": false + }, + "krpanoiphone.js": { + "type": "-", + "size": 84072, + "lmtime": 1741161221683, + "modified": false + }, + "krpanoiphone.license.js": { + "type": "-", + "size": 2843, + "lmtime": 1741161221753, + "modified": false + }, + "sp.xml": { + "type": "-", + "size": 8187, + "lmtime": 1741161221826, + "modified": false + }, + "swfobject": { + "swfkrpano.js": { + "type": "-", + "size": 7691, + "lmtime": 1741161245835, + "modified": false + } + } + }, + "dr_materac_krosno": { + "index.html": { + "type": "-", + "size": 1294, + "lmtime": 1741161245907, + "modified": false + }, + "panos": { + "panorama_1.tiles": { + "b": { + "l1": { + "1": { + "l1_b_1_1.jpg": { + "type": "-", + "size": 22769, + "lmtime": 1741161246730, + "modified": false + }, + "l1_b_1_1.webp": { + "type": "-", + "size": 12180, + "lmtime": 1741161246807, + "modified": false + }, + "l1_b_1_2.jpg": { + "type": "-", + "size": 32837, + "lmtime": 1741161246900, + "modified": false + }, + "l1_b_1_2.webp": { + "type": "-", + "size": 18960, + "lmtime": 1741161246983, + "modified": false + } + }, + "2": { + "l1_b_2_1.jpg": { + "type": "-", + "size": 52610, + "lmtime": 1741161247082, + "modified": false + }, + "l1_b_2_1.webp": { + "type": "-", + "size": 35322, + "lmtime": 1741161247174, + "modified": false + }, + "l1_b_2_2.jpg": { + "type": "-", + "size": 56756, + "lmtime": 1741161247295, + "modified": false + }, + "l1_b_2_2.webp": { + "type": "-", + "size": 37492, + "lmtime": 1741161247395, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_b_1_1.jpg": { + "type": "-", + "size": 16538, + "lmtime": 1741161247493, + "modified": false + }, + "l2_b_1_1.webp": { + "type": "-", + "size": 6970, + "lmtime": 1741161247571, + "modified": false + }, + "l2_b_1_2.jpg": { + "type": "-", + "size": 10046, + "lmtime": 1741161247643, + "modified": false + }, + "l2_b_1_2.webp": { + "type": "-", + "size": 3192, + "lmtime": 1741161247712, + "modified": false + }, + "l2_b_1_3.jpg": { + "type": "-", + "size": 20771, + "lmtime": 1741161247805, + "modified": false + }, + "l2_b_1_3.webp": { + "type": "-", + "size": 11136, + "lmtime": 1741161247878, + "modified": false + }, + "l2_b_1_4.jpg": { + "type": "-", + "size": 7508, + "lmtime": 1741161247950, + "modified": false + }, + "l2_b_1_4.webp": { + "type": "-", + "size": 2390, + "lmtime": 1741161248019, + "modified": false + } + }, + "2": { + "l2_b_2_1.jpg": { + "type": "-", + "size": 19524, + "lmtime": 1741161248136, + "modified": false + }, + "l2_b_2_1.webp": { + "type": "-", + "size": 8838, + "lmtime": 1741161248216, + "modified": false + }, + "l2_b_2_2.jpg": { + "type": "-", + "size": 51773, + "lmtime": 1741161248312, + "modified": false + }, + "l2_b_2_2.webp": { + "type": "-", + "size": 35418, + "lmtime": 1741161248400, + "modified": false + }, + "l2_b_2_3.jpg": { + "type": "-", + "size": 53168, + "lmtime": 1741161248505, + "modified": false + }, + "l2_b_2_3.webp": { + "type": "-", + "size": 35742, + "lmtime": 1741161248601, + "modified": false + }, + "l2_b_2_4.jpg": { + "type": "-", + "size": 23187, + "lmtime": 1741161248703, + "modified": false + }, + "l2_b_2_4.webp": { + "type": "-", + "size": 14200, + "lmtime": 1741161248780, + "modified": false + } + }, + "3": { + "l2_b_3_1.jpg": { + "type": "-", + "size": 56297, + "lmtime": 1741161248889, + "modified": false + }, + "l2_b_3_1.webp": { + "type": "-", + "size": 40294, + "lmtime": 1741161248993, + "modified": false + }, + "l2_b_3_2.jpg": { + "type": "-", + "size": 46117, + "lmtime": 1741161249096, + "modified": false + }, + "l2_b_3_2.webp": { + "type": "-", + "size": 29286, + "lmtime": 1741161249194, + "modified": false + }, + "l2_b_3_3.jpg": { + "type": "-", + "size": 55833, + "lmtime": 1741161249295, + "modified": false + }, + "l2_b_3_3.webp": { + "type": "-", + "size": 36728, + "lmtime": 1741161249384, + "modified": false + }, + "l2_b_3_4.jpg": { + "type": "-", + "size": 20146, + "lmtime": 1741161249480, + "modified": false + }, + "l2_b_3_4.webp": { + "type": "-", + "size": 11198, + "lmtime": 1741161249554, + "modified": false + } + }, + "4": { + "l2_b_4_1.jpg": { + "type": "-", + "size": 25757, + "lmtime": 1741161249639, + "modified": false + }, + "l2_b_4_1.webp": { + "type": "-", + "size": 16974, + "lmtime": 1741161249748, + "modified": false + }, + "l2_b_4_2.jpg": { + "type": "-", + "size": 15946, + "lmtime": 1741161249831, + "modified": false + }, + "l2_b_4_2.webp": { + "type": "-", + "size": 8780, + "lmtime": 1741161249914, + "modified": false + }, + "l2_b_4_3.jpg": { + "type": "-", + "size": 26659, + "lmtime": 1741161249998, + "modified": false + }, + "l2_b_4_3.webp": { + "type": "-", + "size": 17314, + "lmtime": 1741161250089, + "modified": false + }, + "l2_b_4_4.jpg": { + "type": "-", + "size": 6946, + "lmtime": 1741161250163, + "modified": false + }, + "l2_b_4_4.webp": { + "type": "-", + "size": 2950, + "lmtime": 1741161250237, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_b_1_1.jpg": { + "type": "-", + "size": 17412, + "lmtime": 1741161250333, + "modified": false + }, + "l3_b_1_1.webp": { + "type": "-", + "size": 5358, + "lmtime": 1741161250409, + "modified": false + }, + "l3_b_1_2.jpg": { + "type": "-", + "size": 17249, + "lmtime": 1741161250499, + "modified": false + }, + "l3_b_1_2.webp": { + "type": "-", + "size": 5462, + "lmtime": 1741161250579, + "modified": false + }, + "l3_b_1_3.jpg": { + "type": "-", + "size": 16715, + "lmtime": 1741161250663, + "modified": false + }, + "l3_b_1_3.webp": { + "type": "-", + "size": 5534, + "lmtime": 1741161250736, + "modified": false + }, + "l3_b_1_4.jpg": { + "type": "-", + "size": 16076, + "lmtime": 1741161250828, + "modified": false + }, + "l3_b_1_4.webp": { + "type": "-", + "size": 5330, + "lmtime": 1741161250908, + "modified": false + }, + "l3_b_1_5.jpg": { + "type": "-", + "size": 13945, + "lmtime": 1741161250982, + "modified": false + }, + "l3_b_1_5.webp": { + "type": "-", + "size": 3706, + "lmtime": 1741161251067, + "modified": false + }, + "l3_b_1_6.jpg": { + "type": "-", + "size": 11516, + "lmtime": 1741161251146, + "modified": false + }, + "l3_b_1_6.webp": { + "type": "-", + "size": 2464, + "lmtime": 1741161251216, + "modified": false + }, + "l3_b_1_7.jpg": { + "type": "-", + "size": 14571, + "lmtime": 1741161251288, + "modified": false + }, + "l3_b_1_7.webp": { + "type": "-", + "size": 4300, + "lmtime": 1741161251359, + "modified": false + }, + "l3_b_1_8.jpg": { + "type": "-", + "size": 10386, + "lmtime": 1741161251433, + "modified": false + }, + "l3_b_1_8.webp": { + "type": "-", + "size": 3790, + "lmtime": 1741161251501, + "modified": false + } + }, + "2": { + "l3_b_2_1.jpg": { + "type": "-", + "size": 13105, + "lmtime": 1741161251584, + "modified": false + }, + "l3_b_2_1.webp": { + "type": "-", + "size": 3236, + "lmtime": 1741161251665, + "modified": false + }, + "l3_b_2_2.jpg": { + "type": "-", + "size": 29651, + "lmtime": 1741161251752, + "modified": false + }, + "l3_b_2_2.webp": { + "type": "-", + "size": 13394, + "lmtime": 1741161251833, + "modified": false + }, + "l3_b_2_3.jpg": { + "type": "-", + "size": 17633, + "lmtime": 1741161251925, + "modified": false + }, + "l3_b_2_3.webp": { + "type": "-", + "size": 5990, + "lmtime": 1741161251997, + "modified": false + }, + "l3_b_2_4.jpg": { + "type": "-", + "size": 16277, + "lmtime": 1741161252091, + "modified": false + }, + "l3_b_2_4.webp": { + "type": "-", + "size": 5180, + "lmtime": 1741161252174, + "modified": false + }, + "l3_b_2_5.jpg": { + "type": "-", + "size": 34049, + "lmtime": 1741161252261, + "modified": false + }, + "l3_b_2_5.webp": { + "type": "-", + "size": 18312, + "lmtime": 1741161252348, + "modified": false + }, + "l3_b_2_6.jpg": { + "type": "-", + "size": 25050, + "lmtime": 1741161252434, + "modified": false + }, + "l3_b_2_6.webp": { + "type": "-", + "size": 11472, + "lmtime": 1741161252515, + "modified": false + }, + "l3_b_2_7.jpg": { + "type": "-", + "size": 18583, + "lmtime": 1741161252601, + "modified": false + }, + "l3_b_2_7.webp": { + "type": "-", + "size": 5942, + "lmtime": 1741161252674, + "modified": false + }, + "l3_b_2_8.jpg": { + "type": "-", + "size": 8443, + "lmtime": 1741161252744, + "modified": false + }, + "l3_b_2_8.webp": { + "type": "-", + "size": 2842, + "lmtime": 1741161252817, + "modified": false + } + }, + "3": { + "l3_b_3_1.jpg": { + "type": "-", + "size": 15671, + "lmtime": 1741161252899, + "modified": false + }, + "l3_b_3_1.webp": { + "type": "-", + "size": 4024, + "lmtime": 1741161252970, + "modified": false + }, + "l3_b_3_2.jpg": { + "type": "-", + "size": 14518, + "lmtime": 1741161253049, + "modified": false + }, + "l3_b_3_2.webp": { + "type": "-", + "size": 4282, + "lmtime": 1741161253136, + "modified": false + }, + "l3_b_3_3.jpg": { + "type": "-", + "size": 18159, + "lmtime": 1741161253223, + "modified": false + }, + "l3_b_3_3.webp": { + "type": "-", + "size": 7688, + "lmtime": 1741161253302, + "modified": false + }, + "l3_b_3_4.jpg": { + "type": "-", + "size": 13816, + "lmtime": 1741161253372, + "modified": false + }, + "l3_b_3_4.webp": { + "type": "-", + "size": 4846, + "lmtime": 1741161253451, + "modified": false + }, + "l3_b_3_5.jpg": { + "type": "-", + "size": 17460, + "lmtime": 1741161253538, + "modified": false + }, + "l3_b_3_5.webp": { + "type": "-", + "size": 7124, + "lmtime": 1741161253612, + "modified": false + }, + "l3_b_3_6.jpg": { + "type": "-", + "size": 19381, + "lmtime": 1741161253705, + "modified": false + }, + "l3_b_3_6.webp": { + "type": "-", + "size": 7354, + "lmtime": 1741161253783, + "modified": false + }, + "l3_b_3_7.jpg": { + "type": "-", + "size": 24038, + "lmtime": 1741161253871, + "modified": false + }, + "l3_b_3_7.webp": { + "type": "-", + "size": 11002, + "lmtime": 1741161253946, + "modified": false + }, + "l3_b_3_8.jpg": { + "type": "-", + "size": 10974, + "lmtime": 1741161254034, + "modified": false + }, + "l3_b_3_8.webp": { + "type": "-", + "size": 4470, + "lmtime": 1741161254106, + "modified": false + } + }, + "4": { + "l3_b_4_1.jpg": { + "type": "-", + "size": 16461, + "lmtime": 1741161254205, + "modified": false + }, + "l3_b_4_1.webp": { + "type": "-", + "size": 4366, + "lmtime": 1741161254279, + "modified": false + }, + "l3_b_4_2.jpg": { + "type": "-", + "size": 20505, + "lmtime": 1741161254367, + "modified": false + }, + "l3_b_4_2.webp": { + "type": "-", + "size": 6282, + "lmtime": 1741161254442, + "modified": false + }, + "l3_b_4_3.jpg": { + "type": "-", + "size": 69096, + "lmtime": 1741161254555, + "modified": false + }, + "l3_b_4_3.webp": { + "type": "-", + "size": 51836, + "lmtime": 1741161254657, + "modified": false + }, + "l3_b_4_4.jpg": { + "type": "-", + "size": 58783, + "lmtime": 1741161254772, + "modified": false + }, + "l3_b_4_4.webp": { + "type": "-", + "size": 41420, + "lmtime": 1741161254866, + "modified": false + }, + "l3_b_4_5.jpg": { + "type": "-", + "size": 48082, + "lmtime": 1741161254965, + "modified": false + }, + "l3_b_4_5.webp": { + "type": "-", + "size": 28776, + "lmtime": 1741161255054, + "modified": false + }, + "l3_b_4_6.jpg": { + "type": "-", + "size": 50421, + "lmtime": 1741161255157, + "modified": false + }, + "l3_b_4_6.webp": { + "type": "-", + "size": 30716, + "lmtime": 1741161255254, + "modified": false + }, + "l3_b_4_7.jpg": { + "type": "-", + "size": 52428, + "lmtime": 1741161255376, + "modified": false + }, + "l3_b_4_7.webp": { + "type": "-", + "size": 33312, + "lmtime": 1741161255490, + "modified": false + }, + "l3_b_4_8.jpg": { + "type": "-", + "size": 20812, + "lmtime": 1741161255575, + "modified": false + }, + "l3_b_4_8.webp": { + "type": "-", + "size": 11792, + "lmtime": 1741161255657, + "modified": false + } + }, + "5": { + "l3_b_5_1.jpg": { + "type": "-", + "size": 39023, + "lmtime": 1741161255781, + "modified": false + }, + "l3_b_5_1.webp": { + "type": "-", + "size": 20998, + "lmtime": 1741161255879, + "modified": false + }, + "l3_b_5_2.jpg": { + "type": "-", + "size": 44549, + "lmtime": 1741161255986, + "modified": false + }, + "l3_b_5_2.webp": { + "type": "-", + "size": 26838, + "lmtime": 1741161256085, + "modified": false + }, + "l3_b_5_3.jpg": { + "type": "-", + "size": 54362, + "lmtime": 1741161256209, + "modified": false + }, + "l3_b_5_3.webp": { + "type": "-", + "size": 37114, + "lmtime": 1741161256314, + "modified": false + }, + "l3_b_5_4.jpg": { + "type": "-", + "size": 48267, + "lmtime": 1741161256436, + "modified": false + }, + "l3_b_5_4.webp": { + "type": "-", + "size": 30716, + "lmtime": 1741161256544, + "modified": false + }, + "l3_b_5_5.jpg": { + "type": "-", + "size": 53430, + "lmtime": 1741161256667, + "modified": false + }, + "l3_b_5_5.webp": { + "type": "-", + "size": 36134, + "lmtime": 1741161256779, + "modified": false + }, + "l3_b_5_6.jpg": { + "type": "-", + "size": 51087, + "lmtime": 1741161256886, + "modified": false + }, + "l3_b_5_6.webp": { + "type": "-", + "size": 31978, + "lmtime": 1741161256999, + "modified": false + }, + "l3_b_5_7.jpg": { + "type": "-", + "size": 54114, + "lmtime": 1741161257110, + "modified": false + }, + "l3_b_5_7.webp": { + "type": "-", + "size": 34916, + "lmtime": 1741161257196, + "modified": false + }, + "l3_b_5_8.jpg": { + "type": "-", + "size": 23222, + "lmtime": 1741161257283, + "modified": false + }, + "l3_b_5_8.webp": { + "type": "-", + "size": 13116, + "lmtime": 1741161257351, + "modified": false + } + }, + "6": { + "l3_b_6_1.jpg": { + "type": "-", + "size": 40688, + "lmtime": 1741161257455, + "modified": false + }, + "l3_b_6_1.webp": { + "type": "-", + "size": 22240, + "lmtime": 1741161257550, + "modified": false + }, + "l3_b_6_2.jpg": { + "type": "-", + "size": 47697, + "lmtime": 1741161257648, + "modified": false + }, + "l3_b_6_2.webp": { + "type": "-", + "size": 29626, + "lmtime": 1741161257743, + "modified": false + }, + "l3_b_6_3.jpg": { + "type": "-", + "size": 53523, + "lmtime": 1741161257845, + "modified": false + }, + "l3_b_6_3.webp": { + "type": "-", + "size": 35436, + "lmtime": 1741161257936, + "modified": false + }, + "l3_b_6_4.jpg": { + "type": "-", + "size": 26337, + "lmtime": 1741161258023, + "modified": false + }, + "l3_b_6_4.webp": { + "type": "-", + "size": 12138, + "lmtime": 1741161258096, + "modified": false + }, + "l3_b_6_5.jpg": { + "type": "-", + "size": 39551, + "lmtime": 1741161258194, + "modified": false + }, + "l3_b_6_5.webp": { + "type": "-", + "size": 21866, + "lmtime": 1741161258291, + "modified": false + }, + "l3_b_6_6.jpg": { + "type": "-", + "size": 46398, + "lmtime": 1741161258386, + "modified": false + }, + "l3_b_6_6.webp": { + "type": "-", + "size": 25812, + "lmtime": 1741161258483, + "modified": false + }, + "l3_b_6_7.jpg": { + "type": "-", + "size": 19735, + "lmtime": 1741161258575, + "modified": false + }, + "l3_b_6_7.webp": { + "type": "-", + "size": 6650, + "lmtime": 1741161258653, + "modified": false + }, + "l3_b_6_8.jpg": { + "type": "-", + "size": 11696, + "lmtime": 1741161258729, + "modified": false + }, + "l3_b_6_8.webp": { + "type": "-", + "size": 4606, + "lmtime": 1741161258815, + "modified": false + } + }, + "7": { + "l3_b_7_1.jpg": { + "type": "-", + "size": 39614, + "lmtime": 1741161258915, + "modified": false + }, + "l3_b_7_1.webp": { + "type": "-", + "size": 21996, + "lmtime": 1741161258999, + "modified": false + }, + "l3_b_7_2.jpg": { + "type": "-", + "size": 42505, + "lmtime": 1741161259095, + "modified": false + }, + "l3_b_7_2.webp": { + "type": "-", + "size": 26784, + "lmtime": 1741161259183, + "modified": false + }, + "l3_b_7_3.jpg": { + "type": "-", + "size": 41646, + "lmtime": 1741161259290, + "modified": false + }, + "l3_b_7_3.webp": { + "type": "-", + "size": 23510, + "lmtime": 1741161259375, + "modified": false + }, + "l3_b_7_4.jpg": { + "type": "-", + "size": 34103, + "lmtime": 1741161259461, + "modified": false + }, + "l3_b_7_4.webp": { + "type": "-", + "size": 19574, + "lmtime": 1741161259555, + "modified": false + }, + "l3_b_7_5.jpg": { + "type": "-", + "size": 39222, + "lmtime": 1741161259657, + "modified": false + }, + "l3_b_7_5.webp": { + "type": "-", + "size": 23896, + "lmtime": 1741161259744, + "modified": false + }, + "l3_b_7_6.jpg": { + "type": "-", + "size": 49433, + "lmtime": 1741161259845, + "modified": false + }, + "l3_b_7_6.webp": { + "type": "-", + "size": 27988, + "lmtime": 1741161259940, + "modified": false + }, + "l3_b_7_7.jpg": { + "type": "-", + "size": 26298, + "lmtime": 1741161260038, + "modified": false + }, + "l3_b_7_7.webp": { + "type": "-", + "size": 11128, + "lmtime": 1741161260138, + "modified": false + }, + "l3_b_7_8.jpg": { + "type": "-", + "size": 8799, + "lmtime": 1741161260232, + "modified": false + }, + "l3_b_7_8.webp": { + "type": "-", + "size": 2604, + "lmtime": 1741161260317, + "modified": false + } + }, + "8": { + "l3_b_8_1.jpg": { + "type": "-", + "size": 17772, + "lmtime": 1741161260417, + "modified": false + }, + "l3_b_8_1.webp": { + "type": "-", + "size": 8592, + "lmtime": 1741161260507, + "modified": false + }, + "l3_b_8_2.jpg": { + "type": "-", + "size": 20816, + "lmtime": 1741161260626, + "modified": false + }, + "l3_b_8_2.webp": { + "type": "-", + "size": 13070, + "lmtime": 1741161260719, + "modified": false + }, + "l3_b_8_3.jpg": { + "type": "-", + "size": 14815, + "lmtime": 1741161260831, + "modified": false + }, + "l3_b_8_3.webp": { + "type": "-", + "size": 7162, + "lmtime": 1741161260927, + "modified": false + }, + "l3_b_8_4.jpg": { + "type": "-", + "size": 18032, + "lmtime": 1741161261236, + "modified": false + }, + "l3_b_8_4.webp": { + "type": "-", + "size": 11120, + "lmtime": 1741161261317, + "modified": false + }, + "l3_b_8_5.jpg": { + "type": "-", + "size": 18976, + "lmtime": 1741161261410, + "modified": false + }, + "l3_b_8_5.webp": { + "type": "-", + "size": 11778, + "lmtime": 1741161261492, + "modified": false + }, + "l3_b_8_6.jpg": { + "type": "-", + "size": 22736, + "lmtime": 1741161261576, + "modified": false + }, + "l3_b_8_6.webp": { + "type": "-", + "size": 13026, + "lmtime": 1741161261655, + "modified": false + }, + "l3_b_8_7.jpg": { + "type": "-", + "size": 17266, + "lmtime": 1741161261738, + "modified": false + }, + "l3_b_8_7.webp": { + "type": "-", + "size": 8256, + "lmtime": 1741161261812, + "modified": false + }, + "l3_b_8_8.jpg": { + "type": "-", + "size": 4193, + "lmtime": 1741161261888, + "modified": false + }, + "l3_b_8_8.webp": { + "type": "-", + "size": 1206, + "lmtime": 1741161261969, + "modified": false + } + } + } + }, + "d": { + "l1": { + "1": { + "l1_d_1_1.jpg": { + "type": "-", + "size": 24657, + "lmtime": 1741161262058, + "modified": false + }, + "l1_d_1_1.webp": { + "type": "-", + "size": 11328, + "lmtime": 1741161262138, + "modified": false + }, + "l1_d_1_2.jpg": { + "type": "-", + "size": 29895, + "lmtime": 1741161262227, + "modified": false + }, + "l1_d_1_2.webp": { + "type": "-", + "size": 14658, + "lmtime": 1741161262299, + "modified": false + } + }, + "2": { + "l1_d_2_1.jpg": { + "type": "-", + "size": 26729, + "lmtime": 1741161262393, + "modified": false + }, + "l1_d_2_1.webp": { + "type": "-", + "size": 12418, + "lmtime": 1741161262468, + "modified": false + }, + "l1_d_2_2.jpg": { + "type": "-", + "size": 38947, + "lmtime": 1741161262562, + "modified": false + }, + "l1_d_2_2.webp": { + "type": "-", + "size": 20430, + "lmtime": 1741161262654, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_d_1_1.jpg": { + "type": "-", + "size": 19766, + "lmtime": 1741161262758, + "modified": false + }, + "l2_d_1_1.webp": { + "type": "-", + "size": 7850, + "lmtime": 1741161262828, + "modified": false + }, + "l2_d_1_2.jpg": { + "type": "-", + "size": 17681, + "lmtime": 1741161262914, + "modified": false + }, + "l2_d_1_2.webp": { + "type": "-", + "size": 5816, + "lmtime": 1741161262992, + "modified": false + }, + "l2_d_1_3.jpg": { + "type": "-", + "size": 22588, + "lmtime": 1741161263086, + "modified": false + }, + "l2_d_1_3.webp": { + "type": "-", + "size": 9304, + "lmtime": 1741161263159, + "modified": false + }, + "l2_d_1_4.jpg": { + "type": "-", + "size": 11025, + "lmtime": 1741161263235, + "modified": false + }, + "l2_d_1_4.webp": { + "type": "-", + "size": 4854, + "lmtime": 1741161263318, + "modified": false + } + }, + "2": { + "l2_d_2_1.jpg": { + "type": "-", + "size": 23081, + "lmtime": 1741161263411, + "modified": false + }, + "l2_d_2_1.webp": { + "type": "-", + "size": 10236, + "lmtime": 1741161263490, + "modified": false + }, + "l2_d_2_2.jpg": { + "type": "-", + "size": 24153, + "lmtime": 1741161263581, + "modified": false + }, + "l2_d_2_2.webp": { + "type": "-", + "size": 11736, + "lmtime": 1741161263653, + "modified": false + }, + "l2_d_2_3.jpg": { + "type": "-", + "size": 28998, + "lmtime": 1741161263748, + "modified": false + }, + "l2_d_2_3.webp": { + "type": "-", + "size": 13806, + "lmtime": 1741161263828, + "modified": false + }, + "l2_d_2_4.jpg": { + "type": "-", + "size": 20472, + "lmtime": 1741161263912, + "modified": false + }, + "l2_d_2_4.webp": { + "type": "-", + "size": 11956, + "lmtime": 1741161263987, + "modified": false + } + }, + "3": { + "l2_d_3_1.jpg": { + "type": "-", + "size": 18246, + "lmtime": 1741161264071, + "modified": false + }, + "l2_d_3_1.webp": { + "type": "-", + "size": 7004, + "lmtime": 1741161264142, + "modified": false + }, + "l2_d_3_2.jpg": { + "type": "-", + "size": 23257, + "lmtime": 1741161264250, + "modified": false + }, + "l2_d_3_2.webp": { + "type": "-", + "size": 9608, + "lmtime": 1741161264339, + "modified": false + }, + "l2_d_3_3.jpg": { + "type": "-", + "size": 35004, + "lmtime": 1741161264448, + "modified": false + }, + "l2_d_3_3.webp": { + "type": "-", + "size": 17190, + "lmtime": 1741161264544, + "modified": false + }, + "l2_d_3_4.jpg": { + "type": "-", + "size": 21280, + "lmtime": 1741161264658, + "modified": false + }, + "l2_d_3_4.webp": { + "type": "-", + "size": 12338, + "lmtime": 1741161264747, + "modified": false + } + }, + "4": { + "l2_d_4_1.jpg": { + "type": "-", + "size": 16447, + "lmtime": 1741161264828, + "modified": false + }, + "l2_d_4_1.webp": { + "type": "-", + "size": 8440, + "lmtime": 1741161264906, + "modified": false + }, + "l2_d_4_2.jpg": { + "type": "-", + "size": 17092, + "lmtime": 1741161264998, + "modified": false + }, + "l2_d_4_2.webp": { + "type": "-", + "size": 10314, + "lmtime": 1741161265073, + "modified": false + }, + "l2_d_4_3.jpg": { + "type": "-", + "size": 19903, + "lmtime": 1741161265157, + "modified": false + }, + "l2_d_4_3.webp": { + "type": "-", + "size": 10448, + "lmtime": 1741161265242, + "modified": false + }, + "l2_d_4_4.jpg": { + "type": "-", + "size": 12511, + "lmtime": 1741161265318, + "modified": false + }, + "l2_d_4_4.webp": { + "type": "-", + "size": 7722, + "lmtime": 1741161265401, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_d_1_1.jpg": { + "type": "-", + "size": 17969, + "lmtime": 1741161265500, + "modified": false + }, + "l3_d_1_1.webp": { + "type": "-", + "size": 6028, + "lmtime": 1741161265592, + "modified": false + }, + "l3_d_1_2.jpg": { + "type": "-", + "size": 15523, + "lmtime": 1741161265684, + "modified": false + }, + "l3_d_1_2.webp": { + "type": "-", + "size": 4446, + "lmtime": 1741161265780, + "modified": false + }, + "l3_d_1_3.jpg": { + "type": "-", + "size": 25188, + "lmtime": 1741161265887, + "modified": false + }, + "l3_d_1_3.webp": { + "type": "-", + "size": 9840, + "lmtime": 1741161265977, + "modified": false + }, + "l3_d_1_4.jpg": { + "type": "-", + "size": 19138, + "lmtime": 1741161266069, + "modified": false + }, + "l3_d_1_4.webp": { + "type": "-", + "size": 7074, + "lmtime": 1741161266147, + "modified": false + }, + "l3_d_1_5.jpg": { + "type": "-", + "size": 19853, + "lmtime": 1741161266239, + "modified": false + }, + "l3_d_1_5.webp": { + "type": "-", + "size": 6856, + "lmtime": 1741161266318, + "modified": false + }, + "l3_d_1_6.jpg": { + "type": "-", + "size": 15326, + "lmtime": 1741161266386, + "modified": false + }, + "l3_d_1_6.webp": { + "type": "-", + "size": 5508, + "lmtime": 1741161266459, + "modified": false + }, + "l3_d_1_7.jpg": { + "type": "-", + "size": 25106, + "lmtime": 1741161266552, + "modified": false + }, + "l3_d_1_7.webp": { + "type": "-", + "size": 10980, + "lmtime": 1741161266625, + "modified": false + }, + "l3_d_1_8.jpg": { + "type": "-", + "size": 11494, + "lmtime": 1741161266703, + "modified": false + }, + "l3_d_1_8.webp": { + "type": "-", + "size": 4930, + "lmtime": 1741161266771, + "modified": false + } + }, + "2": { + "l3_d_2_1.jpg": { + "type": "-", + "size": 16836, + "lmtime": 1741161266867, + "modified": false + }, + "l3_d_2_1.webp": { + "type": "-", + "size": 5448, + "lmtime": 1741161266948, + "modified": false + }, + "l3_d_2_2.jpg": { + "type": "-", + "size": 18603, + "lmtime": 1741161267037, + "modified": false + }, + "l3_d_2_2.webp": { + "type": "-", + "size": 6654, + "lmtime": 1741161267110, + "modified": false + }, + "l3_d_2_3.jpg": { + "type": "-", + "size": 22732, + "lmtime": 1741161267194, + "modified": false + }, + "l3_d_2_3.webp": { + "type": "-", + "size": 8918, + "lmtime": 1741161267279, + "modified": false + }, + "l3_d_2_4.jpg": { + "type": "-", + "size": 18560, + "lmtime": 1741161267363, + "modified": false + }, + "l3_d_2_4.webp": { + "type": "-", + "size": 6286, + "lmtime": 1741161267437, + "modified": false + }, + "l3_d_2_5.jpg": { + "type": "-", + "size": 20088, + "lmtime": 1741161267542, + "modified": false + }, + "l3_d_2_5.webp": { + "type": "-", + "size": 6466, + "lmtime": 1741161267611, + "modified": false + }, + "l3_d_2_6.jpg": { + "type": "-", + "size": 19699, + "lmtime": 1741161267703, + "modified": false + }, + "l3_d_2_6.webp": { + "type": "-", + "size": 7942, + "lmtime": 1741161267786, + "modified": false + }, + "l3_d_2_7.jpg": { + "type": "-", + "size": 13575, + "lmtime": 1741161267856, + "modified": false + }, + "l3_d_2_7.webp": { + "type": "-", + "size": 4872, + "lmtime": 1741161267927, + "modified": false + }, + "l3_d_2_8.jpg": { + "type": "-", + "size": 7259, + "lmtime": 1741161268003, + "modified": false + }, + "l3_d_2_8.webp": { + "type": "-", + "size": 2670, + "lmtime": 1741161268083, + "modified": false + } + }, + "3": { + "l3_d_3_1.jpg": { + "type": "-", + "size": 17246, + "lmtime": 1741161268198, + "modified": false + }, + "l3_d_3_1.webp": { + "type": "-", + "size": 5792, + "lmtime": 1741161268302, + "modified": false + }, + "l3_d_3_2.jpg": { + "type": "-", + "size": 17994, + "lmtime": 1741161268406, + "modified": false + }, + "l3_d_3_2.webp": { + "type": "-", + "size": 6556, + "lmtime": 1741161268494, + "modified": false + }, + "l3_d_3_3.jpg": { + "type": "-", + "size": 23163, + "lmtime": 1741161268587, + "modified": false + }, + "l3_d_3_3.webp": { + "type": "-", + "size": 9190, + "lmtime": 1741161268660, + "modified": false + }, + "l3_d_3_4.jpg": { + "type": "-", + "size": 16984, + "lmtime": 1741161268746, + "modified": false + }, + "l3_d_3_4.webp": { + "type": "-", + "size": 5256, + "lmtime": 1741161268818, + "modified": false + }, + "l3_d_3_5.jpg": { + "type": "-", + "size": 19904, + "lmtime": 1741161268910, + "modified": false + }, + "l3_d_3_5.webp": { + "type": "-", + "size": 6840, + "lmtime": 1741161268981, + "modified": false + }, + "l3_d_3_6.jpg": { + "type": "-", + "size": 31322, + "lmtime": 1741161269072, + "modified": false + }, + "l3_d_3_6.webp": { + "type": "-", + "size": 15444, + "lmtime": 1741161269144, + "modified": false + }, + "l3_d_3_7.jpg": { + "type": "-", + "size": 29329, + "lmtime": 1741161269238, + "modified": false + }, + "l3_d_3_7.webp": { + "type": "-", + "size": 14030, + "lmtime": 1741161269312, + "modified": false + }, + "l3_d_3_8.jpg": { + "type": "-", + "size": 18741, + "lmtime": 1741161269407, + "modified": false + }, + "l3_d_3_8.webp": { + "type": "-", + "size": 9864, + "lmtime": 1741161269479, + "modified": false + } + }, + "4": { + "l3_d_4_1.jpg": { + "type": "-", + "size": 21449, + "lmtime": 1741161269566, + "modified": false + }, + "l3_d_4_1.webp": { + "type": "-", + "size": 8674, + "lmtime": 1741161269648, + "modified": false + }, + "l3_d_4_2.jpg": { + "type": "-", + "size": 18175, + "lmtime": 1741161269734, + "modified": false + }, + "l3_d_4_2.webp": { + "type": "-", + "size": 6728, + "lmtime": 1741161269807, + "modified": false + }, + "l3_d_4_3.jpg": { + "type": "-", + "size": 25223, + "lmtime": 1741161269891, + "modified": false + }, + "l3_d_4_3.webp": { + "type": "-", + "size": 10438, + "lmtime": 1741161269969, + "modified": false + }, + "l3_d_4_4.jpg": { + "type": "-", + "size": 10808, + "lmtime": 1741161270041, + "modified": false + }, + "l3_d_4_4.webp": { + "type": "-", + "size": 5056, + "lmtime": 1741161270119, + "modified": false + }, + "l3_d_4_5.jpg": { + "type": "-", + "size": 15819, + "lmtime": 1741161270214, + "modified": false + }, + "l3_d_4_5.webp": { + "type": "-", + "size": 5454, + "lmtime": 1741161270290, + "modified": false + }, + "l3_d_4_6.jpg": { + "type": "-", + "size": 29300, + "lmtime": 1741161270382, + "modified": false + }, + "l3_d_4_6.webp": { + "type": "-", + "size": 14038, + "lmtime": 1741161270460, + "modified": false + }, + "l3_d_4_7.jpg": { + "type": "-", + "size": 31870, + "lmtime": 1741161270554, + "modified": false + }, + "l3_d_4_7.webp": { + "type": "-", + "size": 15960, + "lmtime": 1741161270644, + "modified": false + }, + "l3_d_4_8.jpg": { + "type": "-", + "size": 16436, + "lmtime": 1741161270738, + "modified": false + }, + "l3_d_4_8.webp": { + "type": "-", + "size": 7862, + "lmtime": 1741161270809, + "modified": false + } + }, + "5": { + "l3_d_5_1.jpg": { + "type": "-", + "size": 13866, + "lmtime": 1741161270884, + "modified": false + }, + "l3_d_5_1.webp": { + "type": "-", + "size": 3778, + "lmtime": 1741161270955, + "modified": false + }, + "l3_d_5_2.jpg": { + "type": "-", + "size": 15590, + "lmtime": 1741161271032, + "modified": false + }, + "l3_d_5_2.webp": { + "type": "-", + "size": 5004, + "lmtime": 1741161271112, + "modified": false + }, + "l3_d_5_3.jpg": { + "type": "-", + "size": 27568, + "lmtime": 1741161271217, + "modified": false + }, + "l3_d_5_3.webp": { + "type": "-", + "size": 12002, + "lmtime": 1741161271294, + "modified": false + }, + "l3_d_5_4.jpg": { + "type": "-", + "size": 18537, + "lmtime": 1741161271386, + "modified": false + }, + "l3_d_5_4.webp": { + "type": "-", + "size": 6746, + "lmtime": 1741161271466, + "modified": false + }, + "l3_d_5_5.jpg": { + "type": "-", + "size": 22255, + "lmtime": 1741161271553, + "modified": false + }, + "l3_d_5_5.webp": { + "type": "-", + "size": 8888, + "lmtime": 1741161271628, + "modified": false + }, + "l3_d_5_6.jpg": { + "type": "-", + "size": 32557, + "lmtime": 1741161271725, + "modified": false + }, + "l3_d_5_6.webp": { + "type": "-", + "size": 16610, + "lmtime": 1741161271820, + "modified": false + }, + "l3_d_5_7.jpg": { + "type": "-", + "size": 30389, + "lmtime": 1741161271906, + "modified": false + }, + "l3_d_5_7.webp": { + "type": "-", + "size": 15618, + "lmtime": 1741161271985, + "modified": false + }, + "l3_d_5_8.jpg": { + "type": "-", + "size": 16779, + "lmtime": 1741161272077, + "modified": false + }, + "l3_d_5_8.webp": { + "type": "-", + "size": 8306, + "lmtime": 1741161272149, + "modified": false + } + }, + "6": { + "l3_d_6_1.jpg": { + "type": "-", + "size": 13530, + "lmtime": 1741161272235, + "modified": false + }, + "l3_d_6_1.webp": { + "type": "-", + "size": 3516, + "lmtime": 1741161272311, + "modified": false + }, + "l3_d_6_2.jpg": { + "type": "-", + "size": 16928, + "lmtime": 1741161272399, + "modified": false + }, + "l3_d_6_2.webp": { + "type": "-", + "size": 5420, + "lmtime": 1741161272477, + "modified": false + }, + "l3_d_6_3.jpg": { + "type": "-", + "size": 30039, + "lmtime": 1741161272574, + "modified": false + }, + "l3_d_6_3.webp": { + "type": "-", + "size": 14544, + "lmtime": 1741161272646, + "modified": false + }, + "l3_d_6_4.jpg": { + "type": "-", + "size": 27411, + "lmtime": 1741161272742, + "modified": false + }, + "l3_d_6_4.webp": { + "type": "-", + "size": 14296, + "lmtime": 1741161272828, + "modified": false + }, + "l3_d_6_5.jpg": { + "type": "-", + "size": 28005, + "lmtime": 1741161272916, + "modified": false + }, + "l3_d_6_5.webp": { + "type": "-", + "size": 13250, + "lmtime": 1741161272986, + "modified": false + }, + "l3_d_6_6.jpg": { + "type": "-", + "size": 37821, + "lmtime": 1741161273076, + "modified": false + }, + "l3_d_6_6.webp": { + "type": "-", + "size": 18542, + "lmtime": 1741161273160, + "modified": false + }, + "l3_d_6_7.jpg": { + "type": "-", + "size": 36946, + "lmtime": 1741161273252, + "modified": false + }, + "l3_d_6_7.webp": { + "type": "-", + "size": 20490, + "lmtime": 1741161273348, + "modified": false + }, + "l3_d_6_8.jpg": { + "type": "-", + "size": 17284, + "lmtime": 1741161273434, + "modified": false + }, + "l3_d_6_8.webp": { + "type": "-", + "size": 8872, + "lmtime": 1741161273506, + "modified": false + } + }, + "7": { + "l3_d_7_1.jpg": { + "type": "-", + "size": 15948, + "lmtime": 1741161273598, + "modified": false + }, + "l3_d_7_1.webp": { + "type": "-", + "size": 5166, + "lmtime": 1741161273669, + "modified": false + }, + "l3_d_7_2.jpg": { + "type": "-", + "size": 27139, + "lmtime": 1741161273758, + "modified": false + }, + "l3_d_7_2.webp": { + "type": "-", + "size": 12084, + "lmtime": 1741161273830, + "modified": false + }, + "l3_d_7_3.jpg": { + "type": "-", + "size": 37404, + "lmtime": 1741161273926, + "modified": false + }, + "l3_d_7_3.webp": { + "type": "-", + "size": 22394, + "lmtime": 1741161274013, + "modified": false + }, + "l3_d_7_4.jpg": { + "type": "-", + "size": 32645, + "lmtime": 1741161274100, + "modified": false + }, + "l3_d_7_4.webp": { + "type": "-", + "size": 19486, + "lmtime": 1741161274189, + "modified": false + }, + "l3_d_7_5.jpg": { + "type": "-", + "size": 30504, + "lmtime": 1741161274293, + "modified": false + }, + "l3_d_7_5.webp": { + "type": "-", + "size": 15888, + "lmtime": 1741161274377, + "modified": false + }, + "l3_d_7_6.jpg": { + "type": "-", + "size": 35695, + "lmtime": 1741161274481, + "modified": false + }, + "l3_d_7_6.webp": { + "type": "-", + "size": 17638, + "lmtime": 1741161274571, + "modified": false + }, + "l3_d_7_7.jpg": { + "type": "-", + "size": 32699, + "lmtime": 1741161274665, + "modified": false + }, + "l3_d_7_7.webp": { + "type": "-", + "size": 14842, + "lmtime": 1741161274737, + "modified": false + }, + "l3_d_7_8.jpg": { + "type": "-", + "size": 18804, + "lmtime": 1741161274830, + "modified": false + }, + "l3_d_7_8.webp": { + "type": "-", + "size": 9448, + "lmtime": 1741161274907, + "modified": false + } + }, + "8": { + "l3_d_8_1.jpg": { + "type": "-", + "size": 9932, + "lmtime": 1741161274984, + "modified": false + }, + "l3_d_8_1.webp": { + "type": "-", + "size": 3768, + "lmtime": 1741161275061, + "modified": false + }, + "l3_d_8_2.jpg": { + "type": "-", + "size": 19424, + "lmtime": 1741161275149, + "modified": false + }, + "l3_d_8_2.webp": { + "type": "-", + "size": 10584, + "lmtime": 1741161275221, + "modified": false + }, + "l3_d_8_3.jpg": { + "type": "-", + "size": 18481, + "lmtime": 1741161275306, + "modified": false + }, + "l3_d_8_3.webp": { + "type": "-", + "size": 10866, + "lmtime": 1741161275379, + "modified": false + }, + "l3_d_8_4.jpg": { + "type": "-", + "size": 17605, + "lmtime": 1741161275463, + "modified": false + }, + "l3_d_8_4.webp": { + "type": "-", + "size": 10650, + "lmtime": 1741161275551, + "modified": false + }, + "l3_d_8_5.jpg": { + "type": "-", + "size": 17457, + "lmtime": 1741161275666, + "modified": false + }, + "l3_d_8_5.webp": { + "type": "-", + "size": 9838, + "lmtime": 1741161275749, + "modified": false + }, + "l3_d_8_6.jpg": { + "type": "-", + "size": 16273, + "lmtime": 1741161275831, + "modified": false + }, + "l3_d_8_6.webp": { + "type": "-", + "size": 7922, + "lmtime": 1741161275905, + "modified": false + }, + "l3_d_8_7.jpg": { + "type": "-", + "size": 17444, + "lmtime": 1741161275988, + "modified": false + }, + "l3_d_8_7.webp": { + "type": "-", + "size": 8282, + "lmtime": 1741161276060, + "modified": false + }, + "l3_d_8_8.jpg": { + "type": "-", + "size": 10409, + "lmtime": 1741161276135, + "modified": false + }, + "l3_d_8_8.webp": { + "type": "-", + "size": 5646, + "lmtime": 1741161276203, + "modified": false + } + } + } + }, + "f": { + "l1": { + "1": { + "l1_f_1_1.jpg": { + "type": "-", + "size": 31992, + "lmtime": 1741161276303, + "modified": false + }, + "l1_f_1_1.webp": { + "type": "-", + "size": 17234, + "lmtime": 1741161276385, + "modified": false + }, + "l1_f_1_2.jpg": { + "type": "-", + "size": 19350, + "lmtime": 1741161276469, + "modified": false + }, + "l1_f_1_2.webp": { + "type": "-", + "size": 10228, + "lmtime": 1741161276551, + "modified": false + } + }, + "2": { + "l1_f_2_1.jpg": { + "type": "-", + "size": 45299, + "lmtime": 1741161276644, + "modified": false + }, + "l1_f_2_1.webp": { + "type": "-", + "size": 28982, + "lmtime": 1741161276734, + "modified": false + }, + "l1_f_2_2.jpg": { + "type": "-", + "size": 43495, + "lmtime": 1741161276831, + "modified": false + }, + "l1_f_2_2.webp": { + "type": "-", + "size": 24404, + "lmtime": 1741161276924, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_f_1_1.jpg": { + "type": "-", + "size": 13736, + "lmtime": 1741161277013, + "modified": false + }, + "l2_f_1_1.webp": { + "type": "-", + "size": 5010, + "lmtime": 1741161277083, + "modified": false + }, + "l2_f_1_2.jpg": { + "type": "-", + "size": 14130, + "lmtime": 1741161277159, + "modified": false + }, + "l2_f_1_2.webp": { + "type": "-", + "size": 6032, + "lmtime": 1741161277302, + "modified": false + }, + "l2_f_1_3.jpg": { + "type": "-", + "size": 10857, + "lmtime": 1741161277374, + "modified": false + }, + "l2_f_1_3.webp": { + "type": "-", + "size": 4496, + "lmtime": 1741161277447, + "modified": false + }, + "l2_f_1_4.jpg": { + "type": "-", + "size": 5448, + "lmtime": 1741161277526, + "modified": false + }, + "l2_f_1_4.webp": { + "type": "-", + "size": 1986, + "lmtime": 1741161277606, + "modified": false + } + }, + "2": { + "l2_f_2_1.jpg": { + "type": "-", + "size": 51295, + "lmtime": 1741161277708, + "modified": false + }, + "l2_f_2_1.webp": { + "type": "-", + "size": 33510, + "lmtime": 1741161277800, + "modified": false + }, + "l2_f_2_2.jpg": { + "type": "-", + "size": 40800, + "lmtime": 1741161277898, + "modified": false + }, + "l2_f_2_2.webp": { + "type": "-", + "size": 23468, + "lmtime": 1741161277995, + "modified": false + }, + "l2_f_2_3.jpg": { + "type": "-", + "size": 35285, + "lmtime": 1741161278085, + "modified": false + }, + "l2_f_2_3.webp": { + "type": "-", + "size": 19636, + "lmtime": 1741161278174, + "modified": false + }, + "l2_f_2_4.jpg": { + "type": "-", + "size": 3201, + "lmtime": 1741161278265, + "modified": false + }, + "l2_f_2_4.webp": { + "type": "-", + "size": 1364, + "lmtime": 1741161278344, + "modified": false + } + }, + "3": { + "l2_f_3_1.jpg": { + "type": "-", + "size": 36317, + "lmtime": 1741161278434, + "modified": false + }, + "l2_f_3_1.webp": { + "type": "-", + "size": 22118, + "lmtime": 1741161278518, + "modified": false + }, + "l2_f_3_2.jpg": { + "type": "-", + "size": 41146, + "lmtime": 1741161278615, + "modified": false + }, + "l2_f_3_2.webp": { + "type": "-", + "size": 23476, + "lmtime": 1741161278701, + "modified": false + }, + "l2_f_3_3.jpg": { + "type": "-", + "size": 45088, + "lmtime": 1741161278800, + "modified": false + }, + "l2_f_3_3.webp": { + "type": "-", + "size": 25446, + "lmtime": 1741161278889, + "modified": false + }, + "l2_f_3_4.jpg": { + "type": "-", + "size": 16558, + "lmtime": 1741161278975, + "modified": false + }, + "l2_f_3_4.webp": { + "type": "-", + "size": 8472, + "lmtime": 1741161279045, + "modified": false + } + }, + "4": { + "l2_f_4_1.jpg": { + "type": "-", + "size": 5779, + "lmtime": 1741161279126, + "modified": false + }, + "l2_f_4_1.webp": { + "type": "-", + "size": 1934, + "lmtime": 1741161279209, + "modified": false + }, + "l2_f_4_2.jpg": { + "type": "-", + "size": 13143, + "lmtime": 1741161279294, + "modified": false + }, + "l2_f_4_2.webp": { + "type": "-", + "size": 6076, + "lmtime": 1741161279387, + "modified": false + }, + "l2_f_4_3.jpg": { + "type": "-", + "size": 19268, + "lmtime": 1741161279492, + "modified": false + }, + "l2_f_4_3.webp": { + "type": "-", + "size": 8102, + "lmtime": 1741161279574, + "modified": false + }, + "l2_f_4_4.jpg": { + "type": "-", + "size": 8869, + "lmtime": 1741161279654, + "modified": false + }, + "l2_f_4_4.webp": { + "type": "-", + "size": 4218, + "lmtime": 1741161279742, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_f_1_1.jpg": { + "type": "-", + "size": 17025, + "lmtime": 1741161279856, + "modified": false + }, + "l3_f_1_1.webp": { + "type": "-", + "size": 5004, + "lmtime": 1741161279947, + "modified": false + }, + "l3_f_1_2.jpg": { + "type": "-", + "size": 13760, + "lmtime": 1741161280026, + "modified": false + }, + "l3_f_1_2.webp": { + "type": "-", + "size": 3716, + "lmtime": 1741161280116, + "modified": false + }, + "l3_f_1_3.jpg": { + "type": "-", + "size": 12136, + "lmtime": 1741161280203, + "modified": false + }, + "l3_f_1_3.webp": { + "type": "-", + "size": 3196, + "lmtime": 1741161280268, + "modified": false + }, + "l3_f_1_4.jpg": { + "type": "-", + "size": 14714, + "lmtime": 1741161280350, + "modified": false + }, + "l3_f_1_4.webp": { + "type": "-", + "size": 4180, + "lmtime": 1741161280415, + "modified": false + }, + "l3_f_1_5.jpg": { + "type": "-", + "size": 14887, + "lmtime": 1741161280487, + "modified": false + }, + "l3_f_1_5.webp": { + "type": "-", + "size": 3538, + "lmtime": 1741161280559, + "modified": false + }, + "l3_f_1_6.jpg": { + "type": "-", + "size": 15117, + "lmtime": 1741161280635, + "modified": false + }, + "l3_f_1_6.webp": { + "type": "-", + "size": 3810, + "lmtime": 1741161280707, + "modified": false + }, + "l3_f_1_7.jpg": { + "type": "-", + "size": 16309, + "lmtime": 1741161280789, + "modified": false + }, + "l3_f_1_7.webp": { + "type": "-", + "size": 4690, + "lmtime": 1741161280860, + "modified": false + }, + "l3_f_1_8.jpg": { + "type": "-", + "size": 8451, + "lmtime": 1741161280939, + "modified": false + }, + "l3_f_1_8.webp": { + "type": "-", + "size": 2616, + "lmtime": 1741161281017, + "modified": false + } + }, + "2": { + "l3_f_2_1.jpg": { + "type": "-", + "size": 16008, + "lmtime": 1741161281099, + "modified": false + }, + "l3_f_2_1.webp": { + "type": "-", + "size": 4964, + "lmtime": 1741161281174, + "modified": false + }, + "l3_f_2_2.jpg": { + "type": "-", + "size": 16232, + "lmtime": 1741161281258, + "modified": false + }, + "l3_f_2_2.webp": { + "type": "-", + "size": 5054, + "lmtime": 1741161281329, + "modified": false + }, + "l3_f_2_3.jpg": { + "type": "-", + "size": 16243, + "lmtime": 1741161281413, + "modified": false + }, + "l3_f_2_3.webp": { + "type": "-", + "size": 5826, + "lmtime": 1741161281494, + "modified": false + }, + "l3_f_2_4.jpg": { + "type": "-", + "size": 19565, + "lmtime": 1741161281580, + "modified": false + }, + "l3_f_2_4.webp": { + "type": "-", + "size": 7090, + "lmtime": 1741161281657, + "modified": false + }, + "l3_f_2_5.jpg": { + "type": "-", + "size": 13177, + "lmtime": 1741161281726, + "modified": false + }, + "l3_f_2_5.webp": { + "type": "-", + "size": 2716, + "lmtime": 1741161281802, + "modified": false + }, + "l3_f_2_6.jpg": { + "type": "-", + "size": 18448, + "lmtime": 1741161281888, + "modified": false + }, + "l3_f_2_6.webp": { + "type": "-", + "size": 6120, + "lmtime": 1741161281961, + "modified": false + }, + "l3_f_2_7.jpg": { + "type": "-", + "size": 20001, + "lmtime": 1741161282045, + "modified": false + }, + "l3_f_2_7.webp": { + "type": "-", + "size": 7602, + "lmtime": 1741161282128, + "modified": false + }, + "l3_f_2_8.jpg": { + "type": "-", + "size": 5555, + "lmtime": 1741161282194, + "modified": false + }, + "l3_f_2_8.webp": { + "type": "-", + "size": 1392, + "lmtime": 1741161282271, + "modified": false + } + }, + "3": { + "l3_f_3_1.jpg": { + "type": "-", + "size": 19833, + "lmtime": 1741161282355, + "modified": false + }, + "l3_f_3_1.webp": { + "type": "-", + "size": 6876, + "lmtime": 1741161282431, + "modified": false + }, + "l3_f_3_2.jpg": { + "type": "-", + "size": 17426, + "lmtime": 1741161282518, + "modified": false + }, + "l3_f_3_2.webp": { + "type": "-", + "size": 5530, + "lmtime": 1741161282597, + "modified": false + }, + "l3_f_3_3.jpg": { + "type": "-", + "size": 19791, + "lmtime": 1741161282683, + "modified": false + }, + "l3_f_3_3.webp": { + "type": "-", + "size": 7042, + "lmtime": 1741161282763, + "modified": false + }, + "l3_f_3_4.jpg": { + "type": "-", + "size": 24142, + "lmtime": 1741161282870, + "modified": false + }, + "l3_f_3_4.webp": { + "type": "-", + "size": 11032, + "lmtime": 1741161282942, + "modified": false + }, + "l3_f_3_5.jpg": { + "type": "-", + "size": 17236, + "lmtime": 1741161283034, + "modified": false + }, + "l3_f_3_5.webp": { + "type": "-", + "size": 6128, + "lmtime": 1741161283115, + "modified": false + }, + "l3_f_3_6.jpg": { + "type": "-", + "size": 20877, + "lmtime": 1741161283217, + "modified": false + }, + "l3_f_3_6.webp": { + "type": "-", + "size": 8876, + "lmtime": 1741161283287, + "modified": false + }, + "l3_f_3_7.jpg": { + "type": "-", + "size": 11163, + "lmtime": 1741161283364, + "modified": false + }, + "l3_f_3_7.webp": { + "type": "-", + "size": 2930, + "lmtime": 1741161283434, + "modified": false + }, + "l3_f_3_8.jpg": { + "type": "-", + "size": 5475, + "lmtime": 1741161283514, + "modified": false + }, + "l3_f_3_8.webp": { + "type": "-", + "size": 1180, + "lmtime": 1741161283593, + "modified": false + } + }, + "4": { + "l3_f_4_1.jpg": { + "type": "-", + "size": 48586, + "lmtime": 1741161283687, + "modified": false + }, + "l3_f_4_1.webp": { + "type": "-", + "size": 29104, + "lmtime": 1741161283778, + "modified": false + }, + "l3_f_4_2.jpg": { + "type": "-", + "size": 52359, + "lmtime": 1741161283885, + "modified": false + }, + "l3_f_4_2.webp": { + "type": "-", + "size": 33040, + "lmtime": 1741161283982, + "modified": false + }, + "l3_f_4_3.jpg": { + "type": "-", + "size": 40061, + "lmtime": 1741161284072, + "modified": false + }, + "l3_f_4_3.webp": { + "type": "-", + "size": 20840, + "lmtime": 1741161284160, + "modified": false + }, + "l3_f_4_4.jpg": { + "type": "-", + "size": 35767, + "lmtime": 1741161284261, + "modified": false + }, + "l3_f_4_4.webp": { + "type": "-", + "size": 18620, + "lmtime": 1741161284350, + "modified": false + }, + "l3_f_4_5.jpg": { + "type": "-", + "size": 36142, + "lmtime": 1741161284440, + "modified": false + }, + "l3_f_4_5.webp": { + "type": "-", + "size": 18658, + "lmtime": 1741161284529, + "modified": false + }, + "l3_f_4_6.jpg": { + "type": "-", + "size": 42238, + "lmtime": 1741161284626, + "modified": false + }, + "l3_f_4_6.webp": { + "type": "-", + "size": 22764, + "lmtime": 1741161284715, + "modified": false + }, + "l3_f_4_7.jpg": { + "type": "-", + "size": 12457, + "lmtime": 1741161284790, + "modified": false + }, + "l3_f_4_7.webp": { + "type": "-", + "size": 2994, + "lmtime": 1741161284872, + "modified": false + }, + "l3_f_4_8.jpg": { + "type": "-", + "size": 5254, + "lmtime": 1741161284951, + "modified": false + }, + "l3_f_4_8.webp": { + "type": "-", + "size": 1070, + "lmtime": 1741161285025, + "modified": false + } + }, + "5": { + "l3_f_5_1.jpg": { + "type": "-", + "size": 47413, + "lmtime": 1741161285125, + "modified": false + }, + "l3_f_5_1.webp": { + "type": "-", + "size": 27956, + "lmtime": 1741161285215, + "modified": false + }, + "l3_f_5_2.jpg": { + "type": "-", + "size": 53143, + "lmtime": 1741161285319, + "modified": false + }, + "l3_f_5_2.webp": { + "type": "-", + "size": 34068, + "lmtime": 1741161285409, + "modified": false + }, + "l3_f_5_3.jpg": { + "type": "-", + "size": 43631, + "lmtime": 1741161285511, + "modified": false + }, + "l3_f_5_3.webp": { + "type": "-", + "size": 26184, + "lmtime": 1741161285605, + "modified": false + }, + "l3_f_5_4.jpg": { + "type": "-", + "size": 46493, + "lmtime": 1741161285710, + "modified": false + }, + "l3_f_5_4.webp": { + "type": "-", + "size": 28320, + "lmtime": 1741161285797, + "modified": false + }, + "l3_f_5_5.jpg": { + "type": "-", + "size": 39252, + "lmtime": 1741161285891, + "modified": false + }, + "l3_f_5_5.webp": { + "type": "-", + "size": 20876, + "lmtime": 1741161285977, + "modified": false + }, + "l3_f_5_6.jpg": { + "type": "-", + "size": 28258, + "lmtime": 1741161286063, + "modified": false + }, + "l3_f_5_6.webp": { + "type": "-", + "size": 12460, + "lmtime": 1741161286135, + "modified": false + }, + "l3_f_5_7.jpg": { + "type": "-", + "size": 30764, + "lmtime": 1741161286222, + "modified": false + }, + "l3_f_5_7.webp": { + "type": "-", + "size": 13978, + "lmtime": 1741161286302, + "modified": false + }, + "l3_f_5_8.jpg": { + "type": "-", + "size": 15787, + "lmtime": 1741161286373, + "modified": false + }, + "l3_f_5_8.webp": { + "type": "-", + "size": 7648, + "lmtime": 1741161286451, + "modified": false + } + }, + "6": { + "l3_f_6_1.jpg": { + "type": "-", + "size": 21473, + "lmtime": 1741161286552, + "modified": false + }, + "l3_f_6_1.webp": { + "type": "-", + "size": 8918, + "lmtime": 1741161286631, + "modified": false + }, + "l3_f_6_2.jpg": { + "type": "-", + "size": 16477, + "lmtime": 1741161286748, + "modified": false + }, + "l3_f_6_2.webp": { + "type": "-", + "size": 5596, + "lmtime": 1741161286829, + "modified": false + }, + "l3_f_6_3.jpg": { + "type": "-", + "size": 41678, + "lmtime": 1741161286925, + "modified": false + }, + "l3_f_6_3.webp": { + "type": "-", + "size": 21948, + "lmtime": 1741161287015, + "modified": false + }, + "l3_f_6_4.jpg": { + "type": "-", + "size": 27032, + "lmtime": 1741161287129, + "modified": false + }, + "l3_f_6_4.webp": { + "type": "-", + "size": 11914, + "lmtime": 1741161287239, + "modified": false + }, + "l3_f_6_5.jpg": { + "type": "-", + "size": 34219, + "lmtime": 1741161287337, + "modified": false + }, + "l3_f_6_5.webp": { + "type": "-", + "size": 15708, + "lmtime": 1741161287428, + "modified": false + }, + "l3_f_6_6.jpg": { + "type": "-", + "size": 37551, + "lmtime": 1741161287567, + "modified": false + }, + "l3_f_6_6.webp": { + "type": "-", + "size": 17394, + "lmtime": 1741161287683, + "modified": false + }, + "l3_f_6_7.jpg": { + "type": "-", + "size": 23791, + "lmtime": 1741161287792, + "modified": false + }, + "l3_f_6_7.webp": { + "type": "-", + "size": 10110, + "lmtime": 1741161287891, + "modified": false + }, + "l3_f_6_8.jpg": { + "type": "-", + "size": 14575, + "lmtime": 1741161287991, + "modified": false + }, + "l3_f_6_8.webp": { + "type": "-", + "size": 6986, + "lmtime": 1741161288103, + "modified": false + } + }, + "7": { + "l3_f_7_1.jpg": { + "type": "-", + "size": 13498, + "lmtime": 1741161288178, + "modified": false + }, + "l3_f_7_1.webp": { + "type": "-", + "size": 4050, + "lmtime": 1741161288258, + "modified": false + }, + "l3_f_7_2.jpg": { + "type": "-", + "size": 13439, + "lmtime": 1741161288329, + "modified": false + }, + "l3_f_7_2.webp": { + "type": "-", + "size": 3130, + "lmtime": 1741161288400, + "modified": false + }, + "l3_f_7_3.jpg": { + "type": "-", + "size": 37684, + "lmtime": 1741161288496, + "modified": false + }, + "l3_f_7_3.webp": { + "type": "-", + "size": 18788, + "lmtime": 1741161288595, + "modified": false + }, + "l3_f_7_4.jpg": { + "type": "-", + "size": 18167, + "lmtime": 1741161288678, + "modified": false + }, + "l3_f_7_4.webp": { + "type": "-", + "size": 5662, + "lmtime": 1741161288757, + "modified": false + }, + "l3_f_7_5.jpg": { + "type": "-", + "size": 22315, + "lmtime": 1741161288854, + "modified": false + }, + "l3_f_7_5.webp": { + "type": "-", + "size": 7794, + "lmtime": 1741161288999, + "modified": false + }, + "l3_f_7_6.jpg": { + "type": "-", + "size": 37997, + "lmtime": 1741161289117, + "modified": false + }, + "l3_f_7_6.webp": { + "type": "-", + "size": 14878, + "lmtime": 1741161289187, + "modified": false + }, + "l3_f_7_7.jpg": { + "type": "-", + "size": 27197, + "lmtime": 1741161289278, + "modified": false + }, + "l3_f_7_7.webp": { + "type": "-", + "size": 10458, + "lmtime": 1741161289352, + "modified": false + }, + "l3_f_7_8.jpg": { + "type": "-", + "size": 11158, + "lmtime": 1741161289454, + "modified": false + }, + "l3_f_7_8.webp": { + "type": "-", + "size": 4264, + "lmtime": 1741161289541, + "modified": false + } + }, + "8": { + "l3_f_8_1.jpg": { + "type": "-", + "size": 5138, + "lmtime": 1741161289632, + "modified": false + }, + "l3_f_8_1.webp": { + "type": "-", + "size": 1300, + "lmtime": 1741161289718, + "modified": false + }, + "l3_f_8_2.jpg": { + "type": "-", + "size": 7188, + "lmtime": 1741161289822, + "modified": false + }, + "l3_f_8_2.webp": { + "type": "-", + "size": 1774, + "lmtime": 1741161289915, + "modified": false + }, + "l3_f_8_3.jpg": { + "type": "-", + "size": 16388, + "lmtime": 1741161290019, + "modified": false + }, + "l3_f_8_3.webp": { + "type": "-", + "size": 7890, + "lmtime": 1741161290128, + "modified": false + }, + "l3_f_8_4.jpg": { + "type": "-", + "size": 9408, + "lmtime": 1741161290235, + "modified": false + }, + "l3_f_8_4.webp": { + "type": "-", + "size": 3262, + "lmtime": 1741161290305, + "modified": false + }, + "l3_f_8_5.jpg": { + "type": "-", + "size": 10338, + "lmtime": 1741161290389, + "modified": false + }, + "l3_f_8_5.webp": { + "type": "-", + "size": 3386, + "lmtime": 1741161290493, + "modified": false + }, + "l3_f_8_6.jpg": { + "type": "-", + "size": 12973, + "lmtime": 1741161290572, + "modified": false + }, + "l3_f_8_6.webp": { + "type": "-", + "size": 4480, + "lmtime": 1741161290672, + "modified": false + }, + "l3_f_8_7.jpg": { + "type": "-", + "size": 17061, + "lmtime": 1741161290786, + "modified": false + }, + "l3_f_8_7.webp": { + "type": "-", + "size": 6592, + "lmtime": 1741161290892, + "modified": false + }, + "l3_f_8_8.jpg": { + "type": "-", + "size": 6237, + "lmtime": 1741161290983, + "modified": false + }, + "l3_f_8_8.webp": { + "type": "-", + "size": 2294, + "lmtime": 1741161291084, + "modified": false + } + } + } + }, + "l": { + "l1": { + "1": { + "l1_l_1_1.jpg": { + "type": "-", + "size": 26923, + "lmtime": 1741161291210, + "modified": false + }, + "l1_l_1_1.webp": { + "type": "-", + "size": 14132, + "lmtime": 1741161291307, + "modified": false + }, + "l1_l_1_2.jpg": { + "type": "-", + "size": 21846, + "lmtime": 1741161291416, + "modified": false + }, + "l1_l_1_2.webp": { + "type": "-", + "size": 11326, + "lmtime": 1741161291499, + "modified": false + } + }, + "2": { + "l1_l_2_1.jpg": { + "type": "-", + "size": 45334, + "lmtime": 1741161291608, + "modified": false + }, + "l1_l_2_1.webp": { + "type": "-", + "size": 28824, + "lmtime": 1741161291708, + "modified": false + }, + "l1_l_2_2.jpg": { + "type": "-", + "size": 33825, + "lmtime": 1741161291830, + "modified": false + }, + "l1_l_2_2.webp": { + "type": "-", + "size": 18600, + "lmtime": 1741161291941, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_l_1_1.jpg": { + "type": "-", + "size": 14187, + "lmtime": 1741161292039, + "modified": false + }, + "l2_l_1_1.webp": { + "type": "-", + "size": 4282, + "lmtime": 1741161292180, + "modified": false + }, + "l2_l_1_2.jpg": { + "type": "-", + "size": 13618, + "lmtime": 1741161292264, + "modified": false + }, + "l2_l_1_2.webp": { + "type": "-", + "size": 4998, + "lmtime": 1741161292350, + "modified": false + }, + "l2_l_1_3.jpg": { + "type": "-", + "size": 7929, + "lmtime": 1741161292438, + "modified": false + }, + "l2_l_1_3.webp": { + "type": "-", + "size": 2194, + "lmtime": 1741161292534, + "modified": false + }, + "l2_l_1_4.jpg": { + "type": "-", + "size": 7173, + "lmtime": 1741161292621, + "modified": false + }, + "l2_l_1_4.webp": { + "type": "-", + "size": 2430, + "lmtime": 1741161292725, + "modified": false + } + }, + "2": { + "l2_l_2_1.jpg": { + "type": "-", + "size": 42570, + "lmtime": 1741161292827, + "modified": false + }, + "l2_l_2_1.webp": { + "type": "-", + "size": 24676, + "lmtime": 1741161292935, + "modified": false + }, + "l2_l_2_2.jpg": { + "type": "-", + "size": 40519, + "lmtime": 1741161293041, + "modified": false + }, + "l2_l_2_2.webp": { + "type": "-", + "size": 25088, + "lmtime": 1741161293134, + "modified": false + }, + "l2_l_2_3.jpg": { + "type": "-", + "size": 26627, + "lmtime": 1741161293221, + "modified": false + }, + "l2_l_2_3.webp": { + "type": "-", + "size": 13470, + "lmtime": 1741161293300, + "modified": false + }, + "l2_l_2_4.jpg": { + "type": "-", + "size": 18418, + "lmtime": 1741161293387, + "modified": false + }, + "l2_l_2_4.webp": { + "type": "-", + "size": 10328, + "lmtime": 1741161293462, + "modified": false + } + }, + "3": { + "l2_l_3_1.jpg": { + "type": "-", + "size": 49063, + "lmtime": 1741161293567, + "modified": false + }, + "l2_l_3_1.webp": { + "type": "-", + "size": 33024, + "lmtime": 1741161293668, + "modified": false + }, + "l2_l_3_2.jpg": { + "type": "-", + "size": 34233, + "lmtime": 1741161293778, + "modified": false + }, + "l2_l_3_2.webp": { + "type": "-", + "size": 19386, + "lmtime": 1741161293883, + "modified": false + }, + "l2_l_3_3.jpg": { + "type": "-", + "size": 32391, + "lmtime": 1741161293989, + "modified": false + }, + "l2_l_3_3.webp": { + "type": "-", + "size": 17958, + "lmtime": 1741161294079, + "modified": false + }, + "l2_l_3_4.jpg": { + "type": "-", + "size": 14839, + "lmtime": 1741161294157, + "modified": false + }, + "l2_l_3_4.webp": { + "type": "-", + "size": 7736, + "lmtime": 1741161294231, + "modified": false + } + }, + "4": { + "l2_l_4_1.jpg": { + "type": "-", + "size": 7265, + "lmtime": 1741161294315, + "modified": false + }, + "l2_l_4_1.webp": { + "type": "-", + "size": 3314, + "lmtime": 1741161294393, + "modified": false + }, + "l2_l_4_2.jpg": { + "type": "-", + "size": 9611, + "lmtime": 1741161294468, + "modified": false + }, + "l2_l_4_2.webp": { + "type": "-", + "size": 4858, + "lmtime": 1741161294542, + "modified": false + }, + "l2_l_4_3.jpg": { + "type": "-", + "size": 9634, + "lmtime": 1741161294619, + "modified": false + }, + "l2_l_4_3.webp": { + "type": "-", + "size": 4468, + "lmtime": 1741161294698, + "modified": false + }, + "l2_l_4_4.jpg": { + "type": "-", + "size": 4248, + "lmtime": 1741161294771, + "modified": false + }, + "l2_l_4_4.webp": { + "type": "-", + "size": 1446, + "lmtime": 1741161294847, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_l_1_1.jpg": { + "type": "-", + "size": 17324, + "lmtime": 1741161294937, + "modified": false + }, + "l3_l_1_1.webp": { + "type": "-", + "size": 6072, + "lmtime": 1741161295015, + "modified": false + }, + "l3_l_1_2.jpg": { + "type": "-", + "size": 16431, + "lmtime": 1741161295099, + "modified": false + }, + "l3_l_1_2.webp": { + "type": "-", + "size": 4706, + "lmtime": 1741161295170, + "modified": false + }, + "l3_l_1_3.jpg": { + "type": "-", + "size": 16184, + "lmtime": 1741161295262, + "modified": false + }, + "l3_l_1_3.webp": { + "type": "-", + "size": 4682, + "lmtime": 1741161295333, + "modified": false + }, + "l3_l_1_4.jpg": { + "type": "-", + "size": 15512, + "lmtime": 1741161295407, + "modified": false + }, + "l3_l_1_4.webp": { + "type": "-", + "size": 4742, + "lmtime": 1741161295477, + "modified": false + }, + "l3_l_1_5.jpg": { + "type": "-", + "size": 15197, + "lmtime": 1741161295552, + "modified": false + }, + "l3_l_1_5.webp": { + "type": "-", + "size": 4226, + "lmtime": 1741161295630, + "modified": false + }, + "l3_l_1_6.jpg": { + "type": "-", + "size": 14784, + "lmtime": 1741161295705, + "modified": false + }, + "l3_l_1_6.webp": { + "type": "-", + "size": 4068, + "lmtime": 1741161295781, + "modified": false + }, + "l3_l_1_7.jpg": { + "type": "-", + "size": 15518, + "lmtime": 1741161295851, + "modified": false + }, + "l3_l_1_7.webp": { + "type": "-", + "size": 4516, + "lmtime": 1741161295928, + "modified": false + }, + "l3_l_1_8.jpg": { + "type": "-", + "size": 8061, + "lmtime": 1741161296010, + "modified": false + }, + "l3_l_1_8.webp": { + "type": "-", + "size": 2514, + "lmtime": 1741161296079, + "modified": false + } + }, + "2": { + "l3_l_2_1.jpg": { + "type": "-", + "size": 16770, + "lmtime": 1741161296168, + "modified": false + }, + "l3_l_2_1.webp": { + "type": "-", + "size": 5294, + "lmtime": 1741161296240, + "modified": false + }, + "l3_l_2_2.jpg": { + "type": "-", + "size": 16284, + "lmtime": 1741161296322, + "modified": false + }, + "l3_l_2_2.webp": { + "type": "-", + "size": 4714, + "lmtime": 1741161296405, + "modified": false + }, + "l3_l_2_3.jpg": { + "type": "-", + "size": 23697, + "lmtime": 1741161296503, + "modified": false + }, + "l3_l_2_3.webp": { + "type": "-", + "size": 9306, + "lmtime": 1741161296574, + "modified": false + }, + "l3_l_2_4.jpg": { + "type": "-", + "size": 14200, + "lmtime": 1741161296648, + "modified": false + }, + "l3_l_2_4.webp": { + "type": "-", + "size": 3484, + "lmtime": 1741161296719, + "modified": false + }, + "l3_l_2_5.jpg": { + "type": "-", + "size": 13427, + "lmtime": 1741161296790, + "modified": false + }, + "l3_l_2_5.webp": { + "type": "-", + "size": 3262, + "lmtime": 1741161296859, + "modified": false + }, + "l3_l_2_6.jpg": { + "type": "-", + "size": 14630, + "lmtime": 1741161296928, + "modified": false + }, + "l3_l_2_6.webp": { + "type": "-", + "size": 3788, + "lmtime": 1741161297007, + "modified": false + }, + "l3_l_2_7.jpg": { + "type": "-", + "size": 16774, + "lmtime": 1741161297093, + "modified": false + }, + "l3_l_2_7.webp": { + "type": "-", + "size": 5254, + "lmtime": 1741161297169, + "modified": false + }, + "l3_l_2_8.jpg": { + "type": "-", + "size": 10634, + "lmtime": 1741161297237, + "modified": false + }, + "l3_l_2_8.webp": { + "type": "-", + "size": 4160, + "lmtime": 1741161297314, + "modified": false + } + }, + "3": { + "l3_l_3_1.jpg": { + "type": "-", + "size": 22729, + "lmtime": 1741161297435, + "modified": false + }, + "l3_l_3_1.webp": { + "type": "-", + "size": 9138, + "lmtime": 1741161297528, + "modified": false + }, + "l3_l_3_2.jpg": { + "type": "-", + "size": 16650, + "lmtime": 1741161297636, + "modified": false + }, + "l3_l_3_2.webp": { + "type": "-", + "size": 5434, + "lmtime": 1741161297726, + "modified": false + }, + "l3_l_3_3.jpg": { + "type": "-", + "size": 16891, + "lmtime": 1741161297811, + "modified": false + }, + "l3_l_3_3.webp": { + "type": "-", + "size": 5710, + "lmtime": 1741161297885, + "modified": false + }, + "l3_l_3_4.jpg": { + "type": "-", + "size": 15467, + "lmtime": 1741161297957, + "modified": false + }, + "l3_l_3_4.webp": { + "type": "-", + "size": 5524, + "lmtime": 1741161298030, + "modified": false + }, + "l3_l_3_5.jpg": { + "type": "-", + "size": 17140, + "lmtime": 1741161298113, + "modified": false + }, + "l3_l_3_5.webp": { + "type": "-", + "size": 6228, + "lmtime": 1741161298184, + "modified": false + }, + "l3_l_3_6.jpg": { + "type": "-", + "size": 16396, + "lmtime": 1741161298277, + "modified": false + }, + "l3_l_3_6.webp": { + "type": "-", + "size": 5628, + "lmtime": 1741161298349, + "modified": false + }, + "l3_l_3_7.jpg": { + "type": "-", + "size": 20323, + "lmtime": 1741161298438, + "modified": false + }, + "l3_l_3_7.webp": { + "type": "-", + "size": 8076, + "lmtime": 1741161298511, + "modified": false + }, + "l3_l_3_8.jpg": { + "type": "-", + "size": 9969, + "lmtime": 1741161298586, + "modified": false + }, + "l3_l_3_8.webp": { + "type": "-", + "size": 3968, + "lmtime": 1741161298668, + "modified": false + } + }, + "4": { + "l3_l_4_1.jpg": { + "type": "-", + "size": 46220, + "lmtime": 1741161298781, + "modified": false + }, + "l3_l_4_1.webp": { + "type": "-", + "size": 26398, + "lmtime": 1741161298882, + "modified": false + }, + "l3_l_4_2.jpg": { + "type": "-", + "size": 44079, + "lmtime": 1741161298985, + "modified": false + }, + "l3_l_4_2.webp": { + "type": "-", + "size": 25310, + "lmtime": 1741161299086, + "modified": false + }, + "l3_l_4_3.jpg": { + "type": "-", + "size": 23876, + "lmtime": 1741161299180, + "modified": false + }, + "l3_l_4_3.webp": { + "type": "-", + "size": 11286, + "lmtime": 1741161299257, + "modified": false + }, + "l3_l_4_4.jpg": { + "type": "-", + "size": 63288, + "lmtime": 1741161299371, + "modified": false + }, + "l3_l_4_4.webp": { + "type": "-", + "size": 45390, + "lmtime": 1741161299493, + "modified": false + }, + "l3_l_4_5.jpg": { + "type": "-", + "size": 24215, + "lmtime": 1741161299598, + "modified": false + }, + "l3_l_4_5.webp": { + "type": "-", + "size": 11162, + "lmtime": 1741161299698, + "modified": false + }, + "l3_l_4_6.jpg": { + "type": "-", + "size": 30032, + "lmtime": 1741161299805, + "modified": false + }, + "l3_l_4_6.webp": { + "type": "-", + "size": 14786, + "lmtime": 1741161299890, + "modified": false + }, + "l3_l_4_7.jpg": { + "type": "-", + "size": 31672, + "lmtime": 1741161299992, + "modified": false + }, + "l3_l_4_7.webp": { + "type": "-", + "size": 15736, + "lmtime": 1741161300087, + "modified": false + }, + "l3_l_4_8.jpg": { + "type": "-", + "size": 19358, + "lmtime": 1741161300181, + "modified": false + }, + "l3_l_4_8.webp": { + "type": "-", + "size": 10410, + "lmtime": 1741161300267, + "modified": false + } + }, + "5": { + "l3_l_5_1.jpg": { + "type": "-", + "size": 41704, + "lmtime": 1741161300397, + "modified": false + }, + "l3_l_5_1.webp": { + "type": "-", + "size": 23804, + "lmtime": 1741161300522, + "modified": false + }, + "l3_l_5_2.jpg": { + "type": "-", + "size": 51033, + "lmtime": 1741161300634, + "modified": false + }, + "l3_l_5_2.webp": { + "type": "-", + "size": 31438, + "lmtime": 1741161300728, + "modified": false + }, + "l3_l_5_3.jpg": { + "type": "-", + "size": 44696, + "lmtime": 1741161300837, + "modified": false + }, + "l3_l_5_3.webp": { + "type": "-", + "size": 25902, + "lmtime": 1741161300953, + "modified": false + }, + "l3_l_5_4.jpg": { + "type": "-", + "size": 38591, + "lmtime": 1741161301087, + "modified": false + }, + "l3_l_5_4.webp": { + "type": "-", + "size": 21154, + "lmtime": 1741161301451, + "modified": false + }, + "l3_l_5_5.jpg": { + "type": "-", + "size": 38384, + "lmtime": 1741161301567, + "modified": false + }, + "l3_l_5_5.webp": { + "type": "-", + "size": 21110, + "lmtime": 1741161301669, + "modified": false + }, + "l3_l_5_6.jpg": { + "type": "-", + "size": 35583, + "lmtime": 1741161301794, + "modified": false + }, + "l3_l_5_6.webp": { + "type": "-", + "size": 20186, + "lmtime": 1741161301898, + "modified": false + }, + "l3_l_5_7.jpg": { + "type": "-", + "size": 36708, + "lmtime": 1741161302016, + "modified": false + }, + "l3_l_5_7.webp": { + "type": "-", + "size": 19382, + "lmtime": 1741161302111, + "modified": false + }, + "l3_l_5_8.jpg": { + "type": "-", + "size": 15702, + "lmtime": 1741161302199, + "modified": false + }, + "l3_l_5_8.webp": { + "type": "-", + "size": 7590, + "lmtime": 1741161302292, + "modified": false + } + }, + "6": { + "l3_l_6_1.jpg": { + "type": "-", + "size": 32486, + "lmtime": 1741161302384, + "modified": false + }, + "l3_l_6_1.webp": { + "type": "-", + "size": 15840, + "lmtime": 1741161302467, + "modified": false + }, + "l3_l_6_2.jpg": { + "type": "-", + "size": 37757, + "lmtime": 1741161302568, + "modified": false + }, + "l3_l_6_2.webp": { + "type": "-", + "size": 18894, + "lmtime": 1741161302654, + "modified": false + }, + "l3_l_6_3.jpg": { + "type": "-", + "size": 34514, + "lmtime": 1741161302745, + "modified": false + }, + "l3_l_6_3.webp": { + "type": "-", + "size": 18730, + "lmtime": 1741161302836, + "modified": false + }, + "l3_l_6_4.jpg": { + "type": "-", + "size": 25877, + "lmtime": 1741161302931, + "modified": false + }, + "l3_l_6_4.webp": { + "type": "-", + "size": 12446, + "lmtime": 1741161303027, + "modified": false + }, + "l3_l_6_5.jpg": { + "type": "-", + "size": 30810, + "lmtime": 1741161303116, + "modified": false + }, + "l3_l_6_5.webp": { + "type": "-", + "size": 16270, + "lmtime": 1741161303208, + "modified": false + }, + "l3_l_6_6.jpg": { + "type": "-", + "size": 27395, + "lmtime": 1741161303309, + "modified": false + }, + "l3_l_6_6.webp": { + "type": "-", + "size": 12046, + "lmtime": 1741161303394, + "modified": false + }, + "l3_l_6_7.jpg": { + "type": "-", + "size": 24503, + "lmtime": 1741161303483, + "modified": false + }, + "l3_l_6_7.webp": { + "type": "-", + "size": 10640, + "lmtime": 1741161303556, + "modified": false + }, + "l3_l_6_8.jpg": { + "type": "-", + "size": 11202, + "lmtime": 1741161303627, + "modified": false + }, + "l3_l_6_8.webp": { + "type": "-", + "size": 4746, + "lmtime": 1741161303709, + "modified": false + } + }, + "7": { + "l3_l_7_1.jpg": { + "type": "-", + "size": 15555, + "lmtime": 1741161303797, + "modified": false + }, + "l3_l_7_1.webp": { + "type": "-", + "size": 5336, + "lmtime": 1741161303885, + "modified": false + }, + "l3_l_7_2.jpg": { + "type": "-", + "size": 14054, + "lmtime": 1741161303974, + "modified": false + }, + "l3_l_7_2.webp": { + "type": "-", + "size": 4748, + "lmtime": 1741161304081, + "modified": false + }, + "l3_l_7_3.jpg": { + "type": "-", + "size": 11863, + "lmtime": 1741161304160, + "modified": false + }, + "l3_l_7_3.webp": { + "type": "-", + "size": 4030, + "lmtime": 1741161304240, + "modified": false + }, + "l3_l_7_4.jpg": { + "type": "-", + "size": 9500, + "lmtime": 1741161304338, + "modified": false + }, + "l3_l_7_4.webp": { + "type": "-", + "size": 2956, + "lmtime": 1741161304410, + "modified": false + }, + "l3_l_7_5.jpg": { + "type": "-", + "size": 8684, + "lmtime": 1741161304485, + "modified": false + }, + "l3_l_7_5.webp": { + "type": "-", + "size": 2516, + "lmtime": 1741161304575, + "modified": false + }, + "l3_l_7_6.jpg": { + "type": "-", + "size": 9430, + "lmtime": 1741161304660, + "modified": false + }, + "l3_l_7_6.webp": { + "type": "-", + "size": 2342, + "lmtime": 1741161304748, + "modified": false + }, + "l3_l_7_7.jpg": { + "type": "-", + "size": 10512, + "lmtime": 1741161304840, + "modified": false + }, + "l3_l_7_7.webp": { + "type": "-", + "size": 2594, + "lmtime": 1741161304920, + "modified": false + }, + "l3_l_7_8.jpg": { + "type": "-", + "size": 6050, + "lmtime": 1741161305005, + "modified": false + }, + "l3_l_7_8.webp": { + "type": "-", + "size": 1526, + "lmtime": 1741161305085, + "modified": false + } + }, + "8": { + "l3_l_8_1.jpg": { + "type": "-", + "size": 7233, + "lmtime": 1741161305153, + "modified": false + }, + "l3_l_8_1.webp": { + "type": "-", + "size": 2634, + "lmtime": 1741161305239, + "modified": false + }, + "l3_l_8_2.jpg": { + "type": "-", + "size": 8186, + "lmtime": 1741161305325, + "modified": false + }, + "l3_l_8_2.webp": { + "type": "-", + "size": 3142, + "lmtime": 1741161305420, + "modified": false + }, + "l3_l_8_3.jpg": { + "type": "-", + "size": 8611, + "lmtime": 1741161305511, + "modified": false + }, + "l3_l_8_3.webp": { + "type": "-", + "size": 3684, + "lmtime": 1741161305596, + "modified": false + }, + "l3_l_8_4.jpg": { + "type": "-", + "size": 9650, + "lmtime": 1741161305670, + "modified": false + }, + "l3_l_8_4.webp": { + "type": "-", + "size": 4150, + "lmtime": 1741161305743, + "modified": false + }, + "l3_l_8_5.jpg": { + "type": "-", + "size": 7973, + "lmtime": 1741161305835, + "modified": false + }, + "l3_l_8_5.webp": { + "type": "-", + "size": 3122, + "lmtime": 1741161305921, + "modified": false + }, + "l3_l_8_6.jpg": { + "type": "-", + "size": 11029, + "lmtime": 1741161306001, + "modified": false + }, + "l3_l_8_6.webp": { + "type": "-", + "size": 5012, + "lmtime": 1741161306073, + "modified": false + }, + "l3_l_8_7.jpg": { + "type": "-", + "size": 10561, + "lmtime": 1741161306160, + "modified": false + }, + "l3_l_8_7.webp": { + "type": "-", + "size": 4344, + "lmtime": 1741161306272, + "modified": false + }, + "l3_l_8_8.jpg": { + "type": "-", + "size": 3899, + "lmtime": 1741161306363, + "modified": false + }, + "l3_l_8_8.webp": { + "type": "-", + "size": 1218, + "lmtime": 1741161306433, + "modified": false + } + } + } + }, + "preview.jpg": { + "type": "-", + "size": 53557, + "lmtime": 1741161246374, + "modified": false + }, + "preview.webp": { + "type": "-", + "size": 27290, + "lmtime": 1741161246458, + "modified": false + }, + "r": { + "l1": { + "1": { + "l1_r_1_1.jpg": { + "type": "-", + "size": 17519, + "lmtime": 1741161306540, + "modified": false + }, + "l1_r_1_1.webp": { + "type": "-", + "size": 7684, + "lmtime": 1741161306626, + "modified": false + }, + "l1_r_1_2.jpg": { + "type": "-", + "size": 16547, + "lmtime": 1741161306736, + "modified": false + }, + "l1_r_1_2.webp": { + "type": "-", + "size": 7880, + "lmtime": 1741161306827, + "modified": false + } + }, + "2": { + "l1_r_2_1.jpg": { + "type": "-", + "size": 32733, + "lmtime": 1741161306929, + "modified": false + }, + "l1_r_2_1.webp": { + "type": "-", + "size": 16552, + "lmtime": 1741161307018, + "modified": false + }, + "l1_r_2_2.jpg": { + "type": "-", + "size": 43613, + "lmtime": 1741161307119, + "modified": false + }, + "l1_r_2_2.webp": { + "type": "-", + "size": 25482, + "lmtime": 1741161307237, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_r_1_1.jpg": { + "type": "-", + "size": 21528, + "lmtime": 1741161307354, + "modified": false + }, + "l2_r_1_1.webp": { + "type": "-", + "size": 9368, + "lmtime": 1741161307433, + "modified": false + }, + "l2_r_1_2.jpg": { + "type": "-", + "size": 5354, + "lmtime": 1741161307515, + "modified": false + }, + "l2_r_1_2.webp": { + "type": "-", + "size": 2046, + "lmtime": 1741161307597, + "modified": false + }, + "l2_r_1_3.jpg": { + "type": "-", + "size": 20838, + "lmtime": 1741161307694, + "modified": false + }, + "l2_r_1_3.webp": { + "type": "-", + "size": 10374, + "lmtime": 1741161307773, + "modified": false + }, + "l2_r_1_4.jpg": { + "type": "-", + "size": 3647, + "lmtime": 1741161307863, + "modified": false + }, + "l2_r_1_4.webp": { + "type": "-", + "size": 1324, + "lmtime": 1741161307951, + "modified": false + } + }, + "2": { + "l2_r_2_1.jpg": { + "type": "-", + "size": 15660, + "lmtime": 1741161308043, + "modified": false + }, + "l2_r_2_1.webp": { + "type": "-", + "size": 6166, + "lmtime": 1741161308142, + "modified": false + }, + "l2_r_2_2.jpg": { + "type": "-", + "size": 4271, + "lmtime": 1741161308236, + "modified": false + }, + "l2_r_2_2.webp": { + "type": "-", + "size": 1686, + "lmtime": 1741161308344, + "modified": false + }, + "l2_r_2_3.jpg": { + "type": "-", + "size": 16707, + "lmtime": 1741161308434, + "modified": false + }, + "l2_r_2_3.webp": { + "type": "-", + "size": 7170, + "lmtime": 1741161308518, + "modified": false + }, + "l2_r_2_4.jpg": { + "type": "-", + "size": 6974, + "lmtime": 1741161308611, + "modified": false + }, + "l2_r_2_4.webp": { + "type": "-", + "size": 3628, + "lmtime": 1741161308681, + "modified": false + } + }, + "3": { + "l2_r_3_1.jpg": { + "type": "-", + "size": 32945, + "lmtime": 1741161308781, + "modified": false + }, + "l2_r_3_1.webp": { + "type": "-", + "size": 18238, + "lmtime": 1741161308869, + "modified": false + }, + "l2_r_3_2.jpg": { + "type": "-", + "size": 27004, + "lmtime": 1741161308960, + "modified": false + }, + "l2_r_3_2.webp": { + "type": "-", + "size": 12788, + "lmtime": 1741161309030, + "modified": false + }, + "l2_r_3_3.jpg": { + "type": "-", + "size": 39723, + "lmtime": 1741161309156, + "modified": false + }, + "l2_r_3_3.webp": { + "type": "-", + "size": 21800, + "lmtime": 1741161309261, + "modified": false + }, + "l2_r_3_4.jpg": { + "type": "-", + "size": 27947, + "lmtime": 1741161309358, + "modified": false + }, + "l2_r_3_4.webp": { + "type": "-", + "size": 19088, + "lmtime": 1741161309486, + "modified": false + } + }, + "4": { + "l2_r_4_1.jpg": { + "type": "-", + "size": 15259, + "lmtime": 1741161309587, + "modified": false + }, + "l2_r_4_1.webp": { + "type": "-", + "size": 7166, + "lmtime": 1741161309678, + "modified": false + }, + "l2_r_4_2.jpg": { + "type": "-", + "size": 17234, + "lmtime": 1741161309792, + "modified": false + }, + "l2_r_4_2.webp": { + "type": "-", + "size": 8478, + "lmtime": 1741161309881, + "modified": false + }, + "l2_r_4_3.jpg": { + "type": "-", + "size": 19015, + "lmtime": 1741161309974, + "modified": false + }, + "l2_r_4_3.webp": { + "type": "-", + "size": 10474, + "lmtime": 1741161310050, + "modified": false + }, + "l2_r_4_4.jpg": { + "type": "-", + "size": 11627, + "lmtime": 1741161310131, + "modified": false + }, + "l2_r_4_4.webp": { + "type": "-", + "size": 6688, + "lmtime": 1741161310213, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_r_1_1.jpg": { + "type": "-", + "size": 23963, + "lmtime": 1741161310339, + "modified": false + }, + "l3_r_1_1.webp": { + "type": "-", + "size": 9354, + "lmtime": 1741161310424, + "modified": false + }, + "l3_r_1_2.jpg": { + "type": "-", + "size": 35469, + "lmtime": 1741161310514, + "modified": false + }, + "l3_r_1_2.webp": { + "type": "-", + "size": 16500, + "lmtime": 1741161310608, + "modified": false + }, + "l3_r_1_3.jpg": { + "type": "-", + "size": 11912, + "lmtime": 1741161310708, + "modified": false + }, + "l3_r_1_3.webp": { + "type": "-", + "size": 3232, + "lmtime": 1741161310810, + "modified": false + }, + "l3_r_1_4.jpg": { + "type": "-", + "size": 10480, + "lmtime": 1741161310904, + "modified": false + }, + "l3_r_1_4.webp": { + "type": "-", + "size": 2286, + "lmtime": 1741161310996, + "modified": false + }, + "l3_r_1_5.jpg": { + "type": "-", + "size": 19068, + "lmtime": 1741161311097, + "modified": false + }, + "l3_r_1_5.webp": { + "type": "-", + "size": 7440, + "lmtime": 1741161311189, + "modified": false + }, + "l3_r_1_6.jpg": { + "type": "-", + "size": 39485, + "lmtime": 1741161311303, + "modified": false + }, + "l3_r_1_6.webp": { + "type": "-", + "size": 21306, + "lmtime": 1741161311419, + "modified": false + }, + "l3_r_1_7.jpg": { + "type": "-", + "size": 16594, + "lmtime": 1741161311523, + "modified": false + }, + "l3_r_1_7.webp": { + "type": "-", + "size": 5374, + "lmtime": 1741161311602, + "modified": false + }, + "l3_r_1_8.jpg": { + "type": "-", + "size": 6949, + "lmtime": 1741161311685, + "modified": false + } + }, + "2": { + "l3_r_2_1.jpg": { + "type": "-", + "size": 11476, + "lmtime": 1741161311790, + "modified": false + }, + "l3_r_2_1.webp": { + "type": "-", + "size": 2762, + "lmtime": 1741161311883, + "modified": false + }, + "l3_r_2_2.jpg": { + "type": "-", + "size": 10913, + "lmtime": 1741161311977, + "modified": false + }, + "l3_r_2_2.webp": { + "type": "-", + "size": 2492, + "lmtime": 1741161312061, + "modified": false + }, + "l3_r_2_3.jpg": { + "type": "-", + "size": 8717, + "lmtime": 1741161312150, + "modified": false + }, + "l3_r_2_3.webp": { + "type": "-", + "size": 1618, + "lmtime": 1741161312235, + "modified": false + }, + "l3_r_2_4.jpg": { + "type": "-", + "size": 8500, + "lmtime": 1741161312328, + "modified": false + }, + "l3_r_2_4.webp": { + "type": "-", + "size": 1812, + "lmtime": 1741161312406, + "modified": false + }, + "l3_r_2_5.jpg": { + "type": "-", + "size": 8869, + "lmtime": 1741161312506, + "modified": false + }, + "l3_r_2_5.webp": { + "type": "-", + "size": 2192, + "lmtime": 1741161312611, + "modified": false + }, + "l3_r_2_6.jpg": { + "type": "-", + "size": 11432, + "lmtime": 1741161312697, + "modified": false + }, + "l3_r_2_6.webp": { + "type": "-", + "size": 3134, + "lmtime": 1741161312796, + "modified": false + }, + "l3_r_2_7.jpg": { + "type": "-", + "size": 11135, + "lmtime": 1741161312896, + "modified": false + }, + "l3_r_2_7.webp": { + "type": "-", + "size": 2146, + "lmtime": 1741161312991, + "modified": false + }, + "l3_r_2_8.jpg": { + "type": "-", + "size": 6063, + "lmtime": 1741161313097, + "modified": false + }, + "l3_r_2_8.webp": { + "type": "-", + "size": 1390, + "lmtime": 1741161313211, + "modified": false + } + }, + "3": { + "l3_r_3_1.jpg": { + "type": "-", + "size": 14397, + "lmtime": 1741161313309, + "modified": false + }, + "l3_r_3_1.webp": { + "type": "-", + "size": 4114, + "lmtime": 1741161313397, + "modified": false + }, + "l3_r_3_2.jpg": { + "type": "-", + "size": 15289, + "lmtime": 1741161313496, + "modified": false + }, + "l3_r_3_2.webp": { + "type": "-", + "size": 4796, + "lmtime": 1741161313603, + "modified": false + }, + "l3_r_3_3.jpg": { + "type": "-", + "size": 7350, + "lmtime": 1741161313684, + "modified": false + }, + "l3_r_3_3.webp": { + "type": "-", + "size": 1646, + "lmtime": 1741161313767, + "modified": false + }, + "l3_r_3_4.jpg": { + "type": "-", + "size": 6885, + "lmtime": 1741161313846, + "modified": false + }, + "l3_r_3_4.webp": { + "type": "-", + "size": 1448, + "lmtime": 1741161313917, + "modified": false + }, + "l3_r_3_5.jpg": { + "type": "-", + "size": 7020, + "lmtime": 1741161313994, + "modified": false + }, + "l3_r_3_5.webp": { + "type": "-", + "size": 1694, + "lmtime": 1741161314068, + "modified": false + }, + "l3_r_3_6.jpg": { + "type": "-", + "size": 18045, + "lmtime": 1741161314158, + "modified": false + }, + "l3_r_3_6.webp": { + "type": "-", + "size": 6302, + "lmtime": 1741161314238, + "modified": false + }, + "l3_r_3_7.jpg": { + "type": "-", + "size": 10698, + "lmtime": 1741161314313, + "modified": false + }, + "l3_r_3_7.webp": { + "type": "-", + "size": 2008, + "lmtime": 1741161314383, + "modified": false + }, + "l3_r_3_8.jpg": { + "type": "-", + "size": 6342, + "lmtime": 1741161314454, + "modified": false + }, + "l3_r_3_8.webp": { + "type": "-", + "size": 1426, + "lmtime": 1741161314530, + "modified": false + } + }, + "4": { + "l3_r_4_1.jpg": { + "type": "-", + "size": 14212, + "lmtime": 1741161314612, + "modified": false + }, + "l3_r_4_1.webp": { + "type": "-", + "size": 3678, + "lmtime": 1741161314680, + "modified": false + }, + "l3_r_4_2.jpg": { + "type": "-", + "size": 15893, + "lmtime": 1741161314774, + "modified": false + }, + "l3_r_4_2.webp": { + "type": "-", + "size": 4652, + "lmtime": 1741161314855, + "modified": false + }, + "l3_r_4_3.jpg": { + "type": "-", + "size": 6405, + "lmtime": 1741161314931, + "modified": false + }, + "l3_r_4_3.webp": { + "type": "-", + "size": 1370, + "lmtime": 1741161315004, + "modified": false + }, + "l3_r_4_4.jpg": { + "type": "-", + "size": 7071, + "lmtime": 1741161315075, + "modified": false + }, + "l3_r_4_4.webp": { + "type": "-", + "size": 1752, + "lmtime": 1741161315145, + "modified": false + }, + "l3_r_4_5.jpg": { + "type": "-", + "size": 6555, + "lmtime": 1741161315223, + "modified": false + }, + "l3_r_4_5.webp": { + "type": "-", + "size": 1280, + "lmtime": 1741161315296, + "modified": false + }, + "l3_r_4_6.jpg": { + "type": "-", + "size": 18768, + "lmtime": 1741161315389, + "modified": false + }, + "l3_r_4_6.webp": { + "type": "-", + "size": 5932, + "lmtime": 1741161315469, + "modified": false + }, + "l3_r_4_7.jpg": { + "type": "-", + "size": 14865, + "lmtime": 1741161315545, + "modified": false + }, + "l3_r_4_7.webp": { + "type": "-", + "size": 4170, + "lmtime": 1741161315689, + "modified": false + }, + "l3_r_4_8.jpg": { + "type": "-", + "size": 6422, + "lmtime": 1741161315763, + "modified": false + }, + "l3_r_4_8.webp": { + "type": "-", + "size": 1448, + "lmtime": 1741161315845, + "modified": false + } + }, + "5": { + "l3_r_5_1.jpg": { + "type": "-", + "size": 31593, + "lmtime": 1741161315931, + "modified": false + }, + "l3_r_5_1.webp": { + "type": "-", + "size": 14904, + "lmtime": 1741161316004, + "modified": false + }, + "l3_r_5_2.jpg": { + "type": "-", + "size": 21702, + "lmtime": 1741161316097, + "modified": false + }, + "l3_r_5_2.webp": { + "type": "-", + "size": 8624, + "lmtime": 1741161316173, + "modified": false + }, + "l3_r_5_3.jpg": { + "type": "-", + "size": 15181, + "lmtime": 1741161316258, + "modified": false + }, + "l3_r_5_3.webp": { + "type": "-", + "size": 5082, + "lmtime": 1741161316334, + "modified": false + }, + "l3_r_5_4.jpg": { + "type": "-", + "size": 11413, + "lmtime": 1741161316405, + "modified": false + }, + "l3_r_5_4.webp": { + "type": "-", + "size": 3414, + "lmtime": 1741161316485, + "modified": false + }, + "l3_r_5_5.jpg": { + "type": "-", + "size": 9059, + "lmtime": 1741161316551, + "modified": false + }, + "l3_r_5_5.webp": { + "type": "-", + "size": 2334, + "lmtime": 1741161316631, + "modified": false + }, + "l3_r_5_6.jpg": { + "type": "-", + "size": 27029, + "lmtime": 1741161316720, + "modified": false + }, + "l3_r_5_6.webp": { + "type": "-", + "size": 11800, + "lmtime": 1741161316797, + "modified": false + }, + "l3_r_5_7.jpg": { + "type": "-", + "size": 45453, + "lmtime": 1741161316899, + "modified": false + }, + "l3_r_5_7.webp": { + "type": "-", + "size": 27514, + "lmtime": 1741161317000, + "modified": false + }, + "l3_r_5_8.jpg": { + "type": "-", + "size": 19697, + "lmtime": 1741161317091, + "modified": false + }, + "l3_r_5_8.webp": { + "type": "-", + "size": 10616, + "lmtime": 1741161317167, + "modified": false + } + }, + "6": { + "l3_r_6_1.jpg": { + "type": "-", + "size": 27606, + "lmtime": 1741161317251, + "modified": false + }, + "l3_r_6_1.webp": { + "type": "-", + "size": 14578, + "lmtime": 1741161317324, + "modified": false + }, + "l3_r_6_2.jpg": { + "type": "-", + "size": 24291, + "lmtime": 1741161317410, + "modified": false + }, + "l3_r_6_2.webp": { + "type": "-", + "size": 11270, + "lmtime": 1741161317489, + "modified": false + }, + "l3_r_6_3.jpg": { + "type": "-", + "size": 28546, + "lmtime": 1741161317580, + "modified": false + }, + "l3_r_6_3.webp": { + "type": "-", + "size": 12244, + "lmtime": 1741161317651, + "modified": false + }, + "l3_r_6_4.jpg": { + "type": "-", + "size": 28524, + "lmtime": 1741161317739, + "modified": false + }, + "l3_r_6_4.webp": { + "type": "-", + "size": 13716, + "lmtime": 1741161317815, + "modified": false + }, + "l3_r_6_5.jpg": { + "type": "-", + "size": 29023, + "lmtime": 1741161317901, + "modified": false + }, + "l3_r_6_5.webp": { + "type": "-", + "size": 13464, + "lmtime": 1741161317983, + "modified": false + }, + "l3_r_6_6.jpg": { + "type": "-", + "size": 34903, + "lmtime": 1741161318080, + "modified": false + }, + "l3_r_6_6.webp": { + "type": "-", + "size": 16946, + "lmtime": 1741161318165, + "modified": false + }, + "l3_r_6_7.jpg": { + "type": "-", + "size": 49320, + "lmtime": 1741161318273, + "modified": false + }, + "l3_r_6_7.webp": { + "type": "-", + "size": 28998, + "lmtime": 1741161318374, + "modified": false + }, + "l3_r_6_8.jpg": { + "type": "-", + "size": 16940, + "lmtime": 1741161318469, + "modified": false + }, + "l3_r_6_8.webp": { + "type": "-", + "size": 8368, + "lmtime": 1741161318550, + "modified": false + } + }, + "7": { + "l3_r_7_1.jpg": { + "type": "-", + "size": 21362, + "lmtime": 1741161318637, + "modified": false + }, + "l3_r_7_1.webp": { + "type": "-", + "size": 9816, + "lmtime": 1741161318715, + "modified": false + }, + "l3_r_7_2.jpg": { + "type": "-", + "size": 25462, + "lmtime": 1741161318800, + "modified": false + }, + "l3_r_7_2.webp": { + "type": "-", + "size": 10834, + "lmtime": 1741161318875, + "modified": false + }, + "l3_r_7_3.jpg": { + "type": "-", + "size": 23710, + "lmtime": 1741161318961, + "modified": false + }, + "l3_r_7_3.webp": { + "type": "-", + "size": 9744, + "lmtime": 1741161319032, + "modified": false + }, + "l3_r_7_4.jpg": { + "type": "-", + "size": 24349, + "lmtime": 1741161319121, + "modified": false + }, + "l3_r_7_4.webp": { + "type": "-", + "size": 10844, + "lmtime": 1741161319191, + "modified": false + }, + "l3_r_7_5.jpg": { + "type": "-", + "size": 33136, + "lmtime": 1741161319280, + "modified": false + }, + "l3_r_7_5.webp": { + "type": "-", + "size": 16694, + "lmtime": 1741161319372, + "modified": false + }, + "l3_r_7_6.jpg": { + "type": "-", + "size": 31875, + "lmtime": 1741161319469, + "modified": false + }, + "l3_r_7_6.webp": { + "type": "-", + "size": 15348, + "lmtime": 1741161319558, + "modified": false + }, + "l3_r_7_7.jpg": { + "type": "-", + "size": 43243, + "lmtime": 1741161319649, + "modified": false + }, + "l3_r_7_7.webp": { + "type": "-", + "size": 22956, + "lmtime": 1741161319750, + "modified": false + }, + "l3_r_7_8.jpg": { + "type": "-", + "size": 23915, + "lmtime": 1741161319846, + "modified": false + }, + "l3_r_7_8.webp": { + "type": "-", + "size": 12926, + "lmtime": 1741161319936, + "modified": false + } + }, + "8": { + "l3_r_8_1.jpg": { + "type": "-", + "size": 13444, + "lmtime": 1741161320026, + "modified": false + }, + "l3_r_8_1.webp": { + "type": "-", + "size": 5884, + "lmtime": 1741161320094, + "modified": false + }, + "l3_r_8_2.jpg": { + "type": "-", + "size": 9278, + "lmtime": 1741161320181, + "modified": false + }, + "l3_r_8_2.webp": { + "type": "-", + "size": 3554, + "lmtime": 1741161320250, + "modified": false + }, + "l3_r_8_3.jpg": { + "type": "-", + "size": 15929, + "lmtime": 1741161320344, + "modified": false + }, + "l3_r_8_3.webp": { + "type": "-", + "size": 7602, + "lmtime": 1741161320422, + "modified": false + }, + "l3_r_8_4.jpg": { + "type": "-", + "size": 11423, + "lmtime": 1741161320492, + "modified": false + }, + "l3_r_8_4.webp": { + "type": "-", + "size": 5096, + "lmtime": 1741161320564, + "modified": false + }, + "l3_r_8_5.jpg": { + "type": "-", + "size": 15797, + "lmtime": 1741161320643, + "modified": false + }, + "l3_r_8_5.webp": { + "type": "-", + "size": 7802, + "lmtime": 1741161320718, + "modified": false + }, + "l3_r_8_6.jpg": { + "type": "-", + "size": 14591, + "lmtime": 1741161320795, + "modified": false + }, + "l3_r_8_6.webp": { + "type": "-", + "size": 6862, + "lmtime": 1741161320871, + "modified": false + }, + "l3_r_8_7.jpg": { + "type": "-", + "size": 15108, + "lmtime": 1741161320944, + "modified": false + }, + "l3_r_8_7.webp": { + "type": "-", + "size": 6514, + "lmtime": 1741161321014, + "modified": false + }, + "l3_r_8_8.jpg": { + "type": "-", + "size": 6978, + "lmtime": 1741161321087, + "modified": false + }, + "l3_r_8_8.webp": { + "type": "-", + "size": 3238, + "lmtime": 1741161321168, + "modified": false + } + } + } + }, + "thumb.jpg": { + "type": "-", + "size": 14238, + "lmtime": 1741161246531, + "modified": false + }, + "thumb.webp": { + "type": "-", + "size": 9904, + "lmtime": 1741161246612, + "modified": false + }, + "u": { + "l1": { + "1": { + "l1_u_1_1.jpg": { + "type": "-", + "size": 16977, + "lmtime": 1741161321254, + "modified": false + }, + "l1_u_1_1.webp": { + "type": "-", + "size": 7592, + "lmtime": 1741161321331, + "modified": false + }, + "l1_u_1_2.jpg": { + "type": "-", + "size": 4634, + "lmtime": 1741161321408, + "modified": false + }, + "l1_u_1_2.webp": { + "type": "-", + "size": 1638, + "lmtime": 1741161321475, + "modified": false + } + }, + "2": { + "l1_u_2_1.jpg": { + "type": "-", + "size": 20415, + "lmtime": 1741161321567, + "modified": false + }, + "l1_u_2_1.webp": { + "type": "-", + "size": 11194, + "lmtime": 1741161321649, + "modified": false + }, + "l1_u_2_2.jpg": { + "type": "-", + "size": 7851, + "lmtime": 1741161321723, + "modified": false + }, + "l1_u_2_2.webp": { + "type": "-", + "size": 3186, + "lmtime": 1741161321790, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_u_1_1.jpg": { + "type": "-", + "size": 15345, + "lmtime": 1741161321866, + "modified": false + }, + "l2_u_1_1.webp": { + "type": "-", + "size": 7386, + "lmtime": 1741161321947, + "modified": false + }, + "l2_u_1_2.jpg": { + "type": "-", + "size": 14224, + "lmtime": 1741161322023, + "modified": false + }, + "l2_u_1_2.webp": { + "type": "-", + "size": 6402, + "lmtime": 1741161322091, + "modified": false + }, + "l2_u_1_3.jpg": { + "type": "-", + "size": 7879, + "lmtime": 1741161322174, + "modified": false + }, + "l2_u_1_3.webp": { + "type": "-", + "size": 1704, + "lmtime": 1741161322238, + "modified": false + }, + "l2_u_1_4.jpg": { + "type": "-", + "size": 4750, + "lmtime": 1741161322313, + "modified": false + }, + "l2_u_1_4.webp": { + "type": "-", + "size": 1154, + "lmtime": 1741161322386, + "modified": false + } + }, + "2": { + "l2_u_2_1.jpg": { + "type": "-", + "size": 12102, + "lmtime": 1741161322464, + "modified": false + }, + "l2_u_2_1.webp": { + "type": "-", + "size": 5362, + "lmtime": 1741161322540, + "modified": false + }, + "l2_u_2_2.jpg": { + "type": "-", + "size": 9744, + "lmtime": 1741161322630, + "modified": false + }, + "l2_u_2_2.webp": { + "type": "-", + "size": 4156, + "lmtime": 1741161322703, + "modified": false + }, + "l2_u_2_3.jpg": { + "type": "-", + "size": 5797, + "lmtime": 1741161322782, + "modified": false + }, + "l2_u_2_3.webp": { + "type": "-", + "size": 1534, + "lmtime": 1741161322851, + "modified": false + }, + "l2_u_2_4.jpg": { + "type": "-", + "size": 4416, + "lmtime": 1741161322928, + "modified": false + }, + "l2_u_2_4.webp": { + "type": "-", + "size": 1420, + "lmtime": 1741161322999, + "modified": false + } + }, + "3": { + "l2_u_3_1.jpg": { + "type": "-", + "size": 11273, + "lmtime": 1741161323073, + "modified": false + }, + "l2_u_3_1.webp": { + "type": "-", + "size": 4626, + "lmtime": 1741161323150, + "modified": false + }, + "l2_u_3_2.jpg": { + "type": "-", + "size": 11864, + "lmtime": 1741161323220, + "modified": false + }, + "l2_u_3_2.webp": { + "type": "-", + "size": 5168, + "lmtime": 1741161323299, + "modified": false + }, + "l2_u_3_3.jpg": { + "type": "-", + "size": 7201, + "lmtime": 1741161323374, + "modified": false + }, + "l2_u_3_3.webp": { + "type": "-", + "size": 2458, + "lmtime": 1741161323439, + "modified": false + }, + "l2_u_3_4.jpg": { + "type": "-", + "size": 5062, + "lmtime": 1741161323511, + "modified": false + }, + "l2_u_3_4.webp": { + "type": "-", + "size": 1802, + "lmtime": 1741161323585, + "modified": false + } + }, + "4": { + "l2_u_4_1.jpg": { + "type": "-", + "size": 14142, + "lmtime": 1741161323657, + "modified": false + }, + "l2_u_4_1.webp": { + "type": "-", + "size": 7638, + "lmtime": 1741161323736, + "modified": false + }, + "l2_u_4_2.jpg": { + "type": "-", + "size": 16458, + "lmtime": 1741161323823, + "modified": false + }, + "l2_u_4_2.webp": { + "type": "-", + "size": 9052, + "lmtime": 1741161323910, + "modified": false + }, + "l2_u_4_3.jpg": { + "type": "-", + "size": 3597, + "lmtime": 1741161323979, + "modified": false + }, + "l2_u_4_3.webp": { + "type": "-", + "size": 694, + "lmtime": 1741161324047, + "modified": false + }, + "l2_u_4_4.jpg": { + "type": "-", + "size": 2873, + "lmtime": 1741161324118, + "modified": false + }, + "l2_u_4_4.webp": { + "type": "-", + "size": 788, + "lmtime": 1741161324198, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_u_1_1.jpg": { + "type": "-", + "size": 12077, + "lmtime": 1741161324286, + "modified": false + }, + "l3_u_1_1.webp": { + "type": "-", + "size": 2718, + "lmtime": 1741161324366, + "modified": false + }, + "l3_u_1_2.jpg": { + "type": "-", + "size": 10564, + "lmtime": 1741161324438, + "modified": false + }, + "l3_u_1_2.webp": { + "type": "-", + "size": 2226, + "lmtime": 1741161324514, + "modified": false + }, + "l3_u_1_3.jpg": { + "type": "-", + "size": 9640, + "lmtime": 1741161324592, + "modified": false + }, + "l3_u_1_3.webp": { + "type": "-", + "size": 2144, + "lmtime": 1741161324667, + "modified": false + }, + "l3_u_1_4.jpg": { + "type": "-", + "size": 14661, + "lmtime": 1741161324740, + "modified": false + }, + "l3_u_1_4.webp": { + "type": "-", + "size": 4062, + "lmtime": 1741161324813, + "modified": false + }, + "l3_u_1_5.jpg": { + "type": "-", + "size": 15583, + "lmtime": 1741161324896, + "modified": false + }, + "l3_u_1_5.webp": { + "type": "-", + "size": 4442, + "lmtime": 1741161324972, + "modified": false + }, + "l3_u_1_6.jpg": { + "type": "-", + "size": 16269, + "lmtime": 1741161325052, + "modified": false + }, + "l3_u_1_6.webp": { + "type": "-", + "size": 4874, + "lmtime": 1741161325128, + "modified": false + }, + "l3_u_1_7.jpg": { + "type": "-", + "size": 16208, + "lmtime": 1741161325220, + "modified": false + }, + "l3_u_1_8.jpg": { + "type": "-", + "size": 8231, + "lmtime": 1741161325296, + "modified": false + } + }, + "2": { + "l3_u_2_1.jpg": { + "type": "-", + "size": 12801, + "lmtime": 1741161325380, + "modified": false + }, + "l3_u_2_1.webp": { + "type": "-", + "size": 3284, + "lmtime": 1741161325452, + "modified": false + }, + "l3_u_2_2.jpg": { + "type": "-", + "size": 25849, + "lmtime": 1741161325535, + "modified": false + }, + "l3_u_2_2.webp": { + "type": "-", + "size": 12492, + "lmtime": 1741161325607, + "modified": false + }, + "l3_u_2_3.jpg": { + "type": "-", + "size": 27531, + "lmtime": 1741161325708, + "modified": false + }, + "l3_u_2_3.webp": { + "type": "-", + "size": 14550, + "lmtime": 1741161325779, + "modified": false + }, + "l3_u_2_4.jpg": { + "type": "-", + "size": 12938, + "lmtime": 1741161325851, + "modified": false + }, + "l3_u_2_4.webp": { + "type": "-", + "size": 3148, + "lmtime": 1741161325927, + "modified": false + }, + "l3_u_2_5.jpg": { + "type": "-", + "size": 13225, + "lmtime": 1741161325999, + "modified": false + }, + "l3_u_2_5.webp": { + "type": "-", + "size": 2826, + "lmtime": 1741161326073, + "modified": false + }, + "l3_u_2_6.jpg": { + "type": "-", + "size": 14338, + "lmtime": 1741161326141, + "modified": false + }, + "l3_u_2_6.webp": { + "type": "-", + "size": 3424, + "lmtime": 1741161326220, + "modified": false + }, + "l3_u_2_7.jpg": { + "type": "-", + "size": 15459, + "lmtime": 1741161326305, + "modified": false + }, + "l3_u_2_7.webp": { + "type": "-", + "size": 4714, + "lmtime": 1741161326381, + "modified": false + }, + "l3_u_2_8.jpg": { + "type": "-", + "size": 7934, + "lmtime": 1741161326447, + "modified": false + }, + "l3_u_2_8.webp": { + "type": "-", + "size": 2126, + "lmtime": 1741161326519, + "modified": false + } + }, + "3": { + "l3_u_3_1.jpg": { + "type": "-", + "size": 14261, + "lmtime": 1741161326598, + "modified": false + }, + "l3_u_3_1.webp": { + "type": "-", + "size": 3788, + "lmtime": 1741161326813, + "modified": false + }, + "l3_u_3_2.jpg": { + "type": "-", + "size": 24033, + "lmtime": 1741161326994, + "modified": false + }, + "l3_u_3_2.webp": { + "type": "-", + "size": 12302, + "lmtime": 1741161327129, + "modified": false + }, + "l3_u_3_3.jpg": { + "type": "-", + "size": 24647, + "lmtime": 1741161327227, + "modified": false + }, + "l3_u_3_3.webp": { + "type": "-", + "size": 14772, + "lmtime": 1741161327306, + "modified": false + }, + "l3_u_3_4.jpg": { + "type": "-", + "size": 11647, + "lmtime": 1741161327378, + "modified": false + }, + "l3_u_3_4.webp": { + "type": "-", + "size": 2994, + "lmtime": 1741161327451, + "modified": false + }, + "l3_u_3_5.jpg": { + "type": "-", + "size": 10955, + "lmtime": 1741161327528, + "modified": false + }, + "l3_u_3_5.webp": { + "type": "-", + "size": 2036, + "lmtime": 1741161327598, + "modified": false + }, + "l3_u_3_6.jpg": { + "type": "-", + "size": 12666, + "lmtime": 1741161327682, + "modified": false + }, + "l3_u_3_6.webp": { + "type": "-", + "size": 2548, + "lmtime": 1741161327773, + "modified": false + }, + "l3_u_3_7.jpg": { + "type": "-", + "size": 14242, + "lmtime": 1741161327847, + "modified": false + }, + "l3_u_3_7.webp": { + "type": "-", + "size": 3768, + "lmtime": 1741161327927, + "modified": false + }, + "l3_u_3_8.jpg": { + "type": "-", + "size": 7998, + "lmtime": 1741161328003, + "modified": false + }, + "l3_u_3_8.webp": { + "type": "-", + "size": 2266, + "lmtime": 1741161328078, + "modified": false + } + }, + "4": { + "l3_u_4_1.jpg": { + "type": "-", + "size": 13142, + "lmtime": 1741161328151, + "modified": false + }, + "l3_u_4_1.webp": { + "type": "-", + "size": 3166, + "lmtime": 1741161328226, + "modified": false + }, + "l3_u_4_2.jpg": { + "type": "-", + "size": 7151, + "lmtime": 1741161328307, + "modified": false + }, + "l3_u_4_2.webp": { + "type": "-", + "size": 1474, + "lmtime": 1741161328381, + "modified": false + }, + "l3_u_4_3.jpg": { + "type": "-", + "size": 4981, + "lmtime": 1741161328452, + "modified": false + }, + "l3_u_4_3.webp": { + "type": "-", + "size": 1450, + "lmtime": 1741161328527, + "modified": false + }, + "l3_u_4_4.jpg": { + "type": "-", + "size": 10511, + "lmtime": 1741161328601, + "modified": false + }, + "l3_u_4_4.webp": { + "type": "-", + "size": 2290, + "lmtime": 1741161328672, + "modified": false + }, + "l3_u_4_5.jpg": { + "type": "-", + "size": 9671, + "lmtime": 1741161328749, + "modified": false + }, + "l3_u_4_5.webp": { + "type": "-", + "size": 1588, + "lmtime": 1741161328822, + "modified": false + }, + "l3_u_4_6.jpg": { + "type": "-", + "size": 11476, + "lmtime": 1741161328904, + "modified": false + }, + "l3_u_4_6.webp": { + "type": "-", + "size": 2466, + "lmtime": 1741161328970, + "modified": false + }, + "l3_u_4_7.jpg": { + "type": "-", + "size": 13836, + "lmtime": 1741161329048, + "modified": false + }, + "l3_u_4_7.webp": { + "type": "-", + "size": 3494, + "lmtime": 1741161329119, + "modified": false + }, + "l3_u_4_8.jpg": { + "type": "-", + "size": 7525, + "lmtime": 1741161329195, + "modified": false + }, + "l3_u_4_8.webp": { + "type": "-", + "size": 1974, + "lmtime": 1741161329269, + "modified": false + } + }, + "5": { + "l3_u_5_1.jpg": { + "type": "-", + "size": 12483, + "lmtime": 1741161329360, + "modified": false + }, + "l3_u_5_1.webp": { + "type": "-", + "size": 2490, + "lmtime": 1741161329430, + "modified": false + }, + "l3_u_5_2.jpg": { + "type": "-", + "size": 7522, + "lmtime": 1741161329502, + "modified": false + }, + "l3_u_5_2.webp": { + "type": "-", + "size": 1486, + "lmtime": 1741161329574, + "modified": false + }, + "l3_u_5_3.jpg": { + "type": "-", + "size": 5230, + "lmtime": 1741161329654, + "modified": false + }, + "l3_u_5_3.webp": { + "type": "-", + "size": 1252, + "lmtime": 1741161329728, + "modified": false + }, + "l3_u_5_4.jpg": { + "type": "-", + "size": 8334, + "lmtime": 1741161329796, + "modified": false + }, + "l3_u_5_4.webp": { + "type": "-", + "size": 2432, + "lmtime": 1741161329868, + "modified": false + }, + "l3_u_5_5.jpg": { + "type": "-", + "size": 7224, + "lmtime": 1741161329938, + "modified": false + }, + "l3_u_5_5.webp": { + "type": "-", + "size": 2180, + "lmtime": 1741161330026, + "modified": false + }, + "l3_u_5_6.jpg": { + "type": "-", + "size": 8334, + "lmtime": 1741161330097, + "modified": false + }, + "l3_u_5_6.webp": { + "type": "-", + "size": 2228, + "lmtime": 1741161330170, + "modified": false + }, + "l3_u_5_7.jpg": { + "type": "-", + "size": 11669, + "lmtime": 1741161330256, + "modified": false + }, + "l3_u_5_7.webp": { + "type": "-", + "size": 2772, + "lmtime": 1741161330333, + "modified": false + }, + "l3_u_5_8.jpg": { + "type": "-", + "size": 6154, + "lmtime": 1741161330407, + "modified": false + }, + "l3_u_5_8.webp": { + "type": "-", + "size": 1602, + "lmtime": 1741161330481, + "modified": false + } + }, + "6": { + "l3_u_6_1.jpg": { + "type": "-", + "size": 13606, + "lmtime": 1741161330558, + "modified": false + }, + "l3_u_6_1.webp": { + "type": "-", + "size": 3734, + "lmtime": 1741161330630, + "modified": false + }, + "l3_u_6_2.jpg": { + "type": "-", + "size": 7938, + "lmtime": 1741161330709, + "modified": false + }, + "l3_u_6_2.webp": { + "type": "-", + "size": 1378, + "lmtime": 1741161330778, + "modified": false + }, + "l3_u_6_3.jpg": { + "type": "-", + "size": 6950, + "lmtime": 1741161330864, + "modified": false + }, + "l3_u_6_3.webp": { + "type": "-", + "size": 1554, + "lmtime": 1741161330929, + "modified": false + }, + "l3_u_6_4.jpg": { + "type": "-", + "size": 13901, + "lmtime": 1741161331005, + "modified": false + }, + "l3_u_6_4.webp": { + "type": "-", + "size": 4150, + "lmtime": 1741161331082, + "modified": false + }, + "l3_u_6_5.jpg": { + "type": "-", + "size": 13313, + "lmtime": 1741161331151, + "modified": false + }, + "l3_u_6_5.webp": { + "type": "-", + "size": 3644, + "lmtime": 1741161331231, + "modified": false + }, + "l3_u_6_6.jpg": { + "type": "-", + "size": 14033, + "lmtime": 1741161331318, + "modified": false + }, + "l3_u_6_6.webp": { + "type": "-", + "size": 3850, + "lmtime": 1741161331389, + "modified": false + }, + "l3_u_6_7.jpg": { + "type": "-", + "size": 14206, + "lmtime": 1741161331471, + "modified": false + }, + "l3_u_6_7.webp": { + "type": "-", + "size": 3542, + "lmtime": 1741161331561, + "modified": false + }, + "l3_u_6_8.jpg": { + "type": "-", + "size": 8352, + "lmtime": 1741161331637, + "modified": false + }, + "l3_u_6_8.webp": { + "type": "-", + "size": 2680, + "lmtime": 1741161331727, + "modified": false + } + }, + "7": { + "l3_u_7_1.jpg": { + "type": "-", + "size": 12492, + "lmtime": 1741161331842, + "modified": false + }, + "l3_u_7_1.webp": { + "type": "-", + "size": 3090, + "lmtime": 1741161331910, + "modified": false + }, + "l3_u_7_2.jpg": { + "type": "-", + "size": 31337, + "lmtime": 1741161332000, + "modified": false + }, + "l3_u_7_2.webp": { + "type": "-", + "size": 15226, + "lmtime": 1741161332080, + "modified": false + }, + "l3_u_7_3.jpg": { + "type": "-", + "size": 45752, + "lmtime": 1741161332176, + "modified": false + }, + "l3_u_7_3.webp": { + "type": "-", + "size": 28746, + "lmtime": 1741161332267, + "modified": false + }, + "l3_u_7_4.jpg": { + "type": "-", + "size": 14485, + "lmtime": 1741161332339, + "modified": false + }, + "l3_u_7_4.webp": { + "type": "-", + "size": 3896, + "lmtime": 1741161332410, + "modified": false + }, + "l3_u_7_5.jpg": { + "type": "-", + "size": 13957, + "lmtime": 1741161332480, + "modified": false + }, + "l3_u_7_5.webp": { + "type": "-", + "size": 3400, + "lmtime": 1741161332556, + "modified": false + }, + "l3_u_7_6.jpg": { + "type": "-", + "size": 14321, + "lmtime": 1741161332632, + "modified": false + }, + "l3_u_7_6.webp": { + "type": "-", + "size": 3038, + "lmtime": 1741161332738, + "modified": false + }, + "l3_u_7_7.jpg": { + "type": "-", + "size": 14627, + "lmtime": 1741161332825, + "modified": false + }, + "l3_u_7_7.webp": { + "type": "-", + "size": 3302, + "lmtime": 1741161332924, + "modified": false + }, + "l3_u_7_8.jpg": { + "type": "-", + "size": 8456, + "lmtime": 1741161333035, + "modified": false + }, + "l3_u_7_8.webp": { + "type": "-", + "size": 2630, + "lmtime": 1741161333117, + "modified": false + } + }, + "8": { + "l3_u_8_1.jpg": { + "type": "-", + "size": 6436, + "lmtime": 1741161333206, + "modified": false + }, + "l3_u_8_1.webp": { + "type": "-", + "size": 1796, + "lmtime": 1741161333302, + "modified": false + }, + "l3_u_8_2.jpg": { + "type": "-", + "size": 16896, + "lmtime": 1741161333404, + "modified": false + }, + "l3_u_8_2.webp": { + "type": "-", + "size": 8476, + "lmtime": 1741161333498, + "modified": false + }, + "l3_u_8_3.jpg": { + "type": "-", + "size": 24992, + "lmtime": 1741161333613, + "modified": false + }, + "l3_u_8_3.webp": { + "type": "-", + "size": 16782, + "lmtime": 1741161333731, + "modified": false + }, + "l3_u_8_4.jpg": { + "type": "-", + "size": 8256, + "lmtime": 1741161333842, + "modified": false + }, + "l3_u_8_4.webp": { + "type": "-", + "size": 2448, + "lmtime": 1741161333939, + "modified": false + }, + "l3_u_8_5.jpg": { + "type": "-", + "size": 7632, + "lmtime": 1741161334028, + "modified": false + }, + "l3_u_8_5.webp": { + "type": "-", + "size": 1948, + "lmtime": 1741161334099, + "modified": false + }, + "l3_u_8_6.jpg": { + "type": "-", + "size": 7579, + "lmtime": 1741161334180, + "modified": false + }, + "l3_u_8_6.webp": { + "type": "-", + "size": 1752, + "lmtime": 1741161334253, + "modified": false + }, + "l3_u_8_7.jpg": { + "type": "-", + "size": 7694, + "lmtime": 1741161334337, + "modified": false + }, + "l3_u_8_7.webp": { + "type": "-", + "size": 2136, + "lmtime": 1741161334415, + "modified": false + }, + "l3_u_8_8.jpg": { + "type": "-", + "size": 4135, + "lmtime": 1741161334486, + "modified": false + }, + "l3_u_8_8.webp": { + "type": "-", + "size": 1300, + "lmtime": 1741161334569, + "modified": false + } + } + } + } + }, + "panorama_2.tiles": { + "b": { + "l1": { + "1": { + "l1_b_1_1.jpg": { + "type": "-", + "size": 29358, + "lmtime": 1741161335014, + "modified": false + }, + "l1_b_1_1.webp": { + "type": "-", + "size": 14848, + "lmtime": 1741161335087, + "modified": false + }, + "l1_b_1_2.jpg": { + "type": "-", + "size": 31002, + "lmtime": 1741161335176, + "modified": false + }, + "l1_b_1_2.webp": { + "type": "-", + "size": 19376, + "lmtime": 1741161335271, + "modified": false + } + }, + "2": { + "l1_b_2_1.jpg": { + "type": "-", + "size": 51263, + "lmtime": 1741161335373, + "modified": false + }, + "l1_b_2_1.webp": { + "type": "-", + "size": 32916, + "lmtime": 1741161335464, + "modified": false + }, + "l1_b_2_2.jpg": { + "type": "-", + "size": 45363, + "lmtime": 1741161335574, + "modified": false + }, + "l1_b_2_2.webp": { + "type": "-", + "size": 27234, + "lmtime": 1741161335661, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_b_1_1.jpg": { + "type": "-", + "size": 18893, + "lmtime": 1741161335769, + "modified": false + }, + "l2_b_1_1.webp": { + "type": "-", + "size": 9326, + "lmtime": 1741161335845, + "modified": false + }, + "l2_b_1_2.jpg": { + "type": "-", + "size": 5247, + "lmtime": 1741161335924, + "modified": false + }, + "l2_b_1_2.webp": { + "type": "-", + "size": 2134, + "lmtime": 1741161336006, + "modified": false + }, + "l2_b_1_3.jpg": { + "type": "-", + "size": 20632, + "lmtime": 1741161336088, + "modified": false + }, + "l2_b_1_3.webp": { + "type": "-", + "size": 11424, + "lmtime": 1741161336164, + "modified": false + }, + "l2_b_1_4.jpg": { + "type": "-", + "size": 4811, + "lmtime": 1741161336237, + "modified": false + }, + "l2_b_1_4.webp": { + "type": "-", + "size": 1986, + "lmtime": 1741161336309, + "modified": false + } + }, + "2": { + "l2_b_2_1.jpg": { + "type": "-", + "size": 48470, + "lmtime": 1741161336412, + "modified": false + }, + "l2_b_2_1.webp": { + "type": "-", + "size": 28908, + "lmtime": 1741161336504, + "modified": false + }, + "l2_b_2_2.jpg": { + "type": "-", + "size": 38856, + "lmtime": 1741161336636, + "modified": false + }, + "l2_b_2_2.webp": { + "type": "-", + "size": 22318, + "lmtime": 1741161336740, + "modified": false + }, + "l2_b_2_3.jpg": { + "type": "-", + "size": 29265, + "lmtime": 1741161336848, + "modified": false + }, + "l2_b_2_3.webp": { + "type": "-", + "size": 16178, + "lmtime": 1741161336945, + "modified": false + }, + "l2_b_2_4.jpg": { + "type": "-", + "size": 38835, + "lmtime": 1741161337052, + "modified": false + }, + "l2_b_2_4.webp": { + "type": "-", + "size": 30870, + "lmtime": 1741161337140, + "modified": false + } + }, + "3": { + "l2_b_3_1.jpg": { + "type": "-", + "size": 54311, + "lmtime": 1741161337239, + "modified": false + }, + "l2_b_3_1.webp": { + "type": "-", + "size": 37174, + "lmtime": 1741161337332, + "modified": false + }, + "l2_b_3_2.jpg": { + "type": "-", + "size": 43831, + "lmtime": 1741161337430, + "modified": false + }, + "l2_b_3_2.webp": { + "type": "-", + "size": 28044, + "lmtime": 1741161337521, + "modified": false + }, + "l2_b_3_3.jpg": { + "type": "-", + "size": 42260, + "lmtime": 1741161337626, + "modified": false + }, + "l2_b_3_3.webp": { + "type": "-", + "size": 24788, + "lmtime": 1741161337719, + "modified": false + }, + "l2_b_3_4.jpg": { + "type": "-", + "size": 17876, + "lmtime": 1741161337807, + "modified": false + }, + "l2_b_3_4.webp": { + "type": "-", + "size": 8968, + "lmtime": 1741161337894, + "modified": false + } + }, + "4": { + "l2_b_4_1.jpg": { + "type": "-", + "size": 21844, + "lmtime": 1741161337989, + "modified": false + }, + "l2_b_4_1.webp": { + "type": "-", + "size": 13478, + "lmtime": 1741161338065, + "modified": false + }, + "l2_b_4_2.jpg": { + "type": "-", + "size": 22750, + "lmtime": 1741161338152, + "modified": false + }, + "l2_b_4_2.webp": { + "type": "-", + "size": 18092, + "lmtime": 1741161338239, + "modified": false + }, + "l2_b_4_3.jpg": { + "type": "-", + "size": 22628, + "lmtime": 1741161338325, + "modified": false + }, + "l2_b_4_3.webp": { + "type": "-", + "size": 13600, + "lmtime": 1741161338398, + "modified": false + }, + "l2_b_4_4.jpg": { + "type": "-", + "size": 10265, + "lmtime": 1741161338470, + "modified": false + }, + "l2_b_4_4.webp": { + "type": "-", + "size": 5420, + "lmtime": 1741161338540, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_b_1_1.jpg": { + "type": "-", + "size": 23613, + "lmtime": 1741161338655, + "modified": false + }, + "l3_b_1_2.jpg": { + "type": "-", + "size": 30468, + "lmtime": 1741161338754, + "modified": false + }, + "l3_b_1_3.jpg": { + "type": "-", + "size": 11275, + "lmtime": 1741161338831, + "modified": false + }, + "l3_b_1_4.jpg": { + "type": "-", + "size": 10460, + "lmtime": 1741161338904, + "modified": false + }, + "l3_b_1_5.jpg": { + "type": "-", + "size": 11383, + "lmtime": 1741161338985, + "modified": false + }, + "l3_b_1_6.jpg": { + "type": "-", + "size": 38945, + "lmtime": 1741161339086, + "modified": false + }, + "l3_b_1_7.jpg": { + "type": "-", + "size": 25120, + "lmtime": 1741161339172, + "modified": false + }, + "l3_b_1_7.webp": { + "type": "-", + "size": 10070, + "lmtime": 1741161339255, + "modified": false + }, + "l3_b_1_8.jpg": { + "type": "-", + "size": 6104, + "lmtime": 1741161339333, + "modified": false + }, + "l3_b_1_8.webp": { + "type": "-", + "size": 1378, + "lmtime": 1741161339411, + "modified": false + } + }, + "2": { + "l3_b_2_1.jpg": { + "type": "-", + "size": 13513, + "lmtime": 1741161339497, + "modified": false + }, + "l3_b_2_1.webp": { + "type": "-", + "size": 4098, + "lmtime": 1741161339565, + "modified": false + }, + "l3_b_2_2.jpg": { + "type": "-", + "size": 11588, + "lmtime": 1741161339637, + "modified": false + }, + "l3_b_2_2.webp": { + "type": "-", + "size": 2896, + "lmtime": 1741161339717, + "modified": false + }, + "l3_b_2_3.jpg": { + "type": "-", + "size": 10614, + "lmtime": 1741161339802, + "modified": false + }, + "l3_b_2_3.webp": { + "type": "-", + "size": 2768, + "lmtime": 1741161339870, + "modified": false + }, + "l3_b_2_4.jpg": { + "type": "-", + "size": 10024, + "lmtime": 1741161339947, + "modified": false + }, + "l3_b_2_4.webp": { + "type": "-", + "size": 2890, + "lmtime": 1741161340024, + "modified": false + }, + "l3_b_2_5.jpg": { + "type": "-", + "size": 10775, + "lmtime": 1741161340095, + "modified": false + }, + "l3_b_2_5.webp": { + "type": "-", + "size": 3380, + "lmtime": 1741161340176, + "modified": false + }, + "l3_b_2_6.jpg": { + "type": "-", + "size": 11179, + "lmtime": 1741161340248, + "modified": false + }, + "l3_b_2_6.webp": { + "type": "-", + "size": 2718, + "lmtime": 1741161340322, + "modified": false + }, + "l3_b_2_7.jpg": { + "type": "-", + "size": 12607, + "lmtime": 1741161340390, + "modified": false + }, + "l3_b_2_7.webp": { + "type": "-", + "size": 3272, + "lmtime": 1741161340469, + "modified": false + }, + "l3_b_2_8.jpg": { + "type": "-", + "size": 6254, + "lmtime": 1741161340547, + "modified": false + }, + "l3_b_2_8.webp": { + "type": "-", + "size": 1734, + "lmtime": 1741161340612, + "modified": false + } + }, + "3": { + "l3_b_3_1.jpg": { + "type": "-", + "size": 31112, + "lmtime": 1741161340708, + "modified": false + }, + "l3_b_3_1.webp": { + "type": "-", + "size": 13360, + "lmtime": 1741161340785, + "modified": false + }, + "l3_b_3_2.jpg": { + "type": "-", + "size": 20620, + "lmtime": 1741161340875, + "modified": false + }, + "l3_b_3_2.webp": { + "type": "-", + "size": 7958, + "lmtime": 1741161340952, + "modified": false + }, + "l3_b_3_3.jpg": { + "type": "-", + "size": 21819, + "lmtime": 1741161341044, + "modified": false + }, + "l3_b_3_3.webp": { + "type": "-", + "size": 9098, + "lmtime": 1741161341122, + "modified": false + }, + "l3_b_3_4.jpg": { + "type": "-", + "size": 14213, + "lmtime": 1741161341199, + "modified": false + }, + "l3_b_3_4.webp": { + "type": "-", + "size": 4832, + "lmtime": 1741161341276, + "modified": false + }, + "l3_b_3_5.jpg": { + "type": "-", + "size": 10526, + "lmtime": 1741161341348, + "modified": false + }, + "l3_b_3_5.webp": { + "type": "-", + "size": 2940, + "lmtime": 1741161341428, + "modified": false + }, + "l3_b_3_6.jpg": { + "type": "-", + "size": 21465, + "lmtime": 1741161341518, + "modified": false + }, + "l3_b_3_6.webp": { + "type": "-", + "size": 8668, + "lmtime": 1741161341590, + "modified": false + }, + "l3_b_3_7.jpg": { + "type": "-", + "size": 29647, + "lmtime": 1741161341680, + "modified": false + }, + "l3_b_3_7.webp": { + "type": "-", + "size": 14542, + "lmtime": 1741161341755, + "modified": false + }, + "l3_b_3_8.jpg": { + "type": "-", + "size": 22991, + "lmtime": 1741161341847, + "modified": false + }, + "l3_b_3_8.webp": { + "type": "-", + "size": 14400, + "lmtime": 1741161341923, + "modified": false + } + }, + "4": { + "l3_b_4_1.jpg": { + "type": "-", + "size": 46442, + "lmtime": 1741161342016, + "modified": false + }, + "l3_b_4_1.webp": { + "type": "-", + "size": 25950, + "lmtime": 1741161342105, + "modified": false + }, + "l3_b_4_2.jpg": { + "type": "-", + "size": 48329, + "lmtime": 1741161342207, + "modified": false + }, + "l3_b_4_2.webp": { + "type": "-", + "size": 28182, + "lmtime": 1741161342299, + "modified": false + }, + "l3_b_4_3.jpg": { + "type": "-", + "size": 35432, + "lmtime": 1741161342389, + "modified": false + }, + "l3_b_4_3.webp": { + "type": "-", + "size": 16748, + "lmtime": 1741161342474, + "modified": false + }, + "l3_b_4_4.jpg": { + "type": "-", + "size": 32793, + "lmtime": 1741161342564, + "modified": false + }, + "l3_b_4_4.webp": { + "type": "-", + "size": 15838, + "lmtime": 1741161342645, + "modified": false + }, + "l3_b_4_5.jpg": { + "type": "-", + "size": 23330, + "lmtime": 1741161342732, + "modified": false + }, + "l3_b_4_5.webp": { + "type": "-", + "size": 9934, + "lmtime": 1741161342813, + "modified": false + }, + "l3_b_4_6.jpg": { + "type": "-", + "size": 27601, + "lmtime": 1741161342907, + "modified": false + }, + "l3_b_4_6.webp": { + "type": "-", + "size": 12290, + "lmtime": 1741161342977, + "modified": false + }, + "l3_b_4_7.jpg": { + "type": "-", + "size": 61459, + "lmtime": 1741161343085, + "modified": false + }, + "l3_b_4_7.webp": { + "type": "-", + "size": 41692, + "lmtime": 1741161343183, + "modified": false + }, + "l3_b_4_8.jpg": { + "type": "-", + "size": 34265, + "lmtime": 1741161343276, + "modified": false + }, + "l3_b_4_8.webp": { + "type": "-", + "size": 23782, + "lmtime": 1741161343366, + "modified": false + } + }, + "5": { + "l3_b_5_1.jpg": { + "type": "-", + "size": 41796, + "lmtime": 1741161343471, + "modified": false + }, + "l3_b_5_1.webp": { + "type": "-", + "size": 22282, + "lmtime": 1741161343565, + "modified": false + }, + "l3_b_5_2.jpg": { + "type": "-", + "size": 42589, + "lmtime": 1741161343663, + "modified": false + }, + "l3_b_5_2.webp": { + "type": "-", + "size": 23494, + "lmtime": 1741161343754, + "modified": false + }, + "l3_b_5_3.jpg": { + "type": "-", + "size": 45844, + "lmtime": 1741161343850, + "modified": false + }, + "l3_b_5_3.webp": { + "type": "-", + "size": 27052, + "lmtime": 1741161343938, + "modified": false + }, + "l3_b_5_4.jpg": { + "type": "-", + "size": 31530, + "lmtime": 1741161344028, + "modified": false + }, + "l3_b_5_4.webp": { + "type": "-", + "size": 14822, + "lmtime": 1741161344104, + "modified": false + }, + "l3_b_5_5.jpg": { + "type": "-", + "size": 39690, + "lmtime": 1741161344195, + "modified": false + }, + "l3_b_5_5.webp": { + "type": "-", + "size": 20412, + "lmtime": 1741161344292, + "modified": false + }, + "l3_b_5_6.jpg": { + "type": "-", + "size": 44434, + "lmtime": 1741161344401, + "modified": false + }, + "l3_b_5_6.webp": { + "type": "-", + "size": 22596, + "lmtime": 1741161344488, + "modified": false + }, + "l3_b_5_7.jpg": { + "type": "-", + "size": 38396, + "lmtime": 1741161344577, + "modified": false + }, + "l3_b_5_7.webp": { + "type": "-", + "size": 20580, + "lmtime": 1741161344666, + "modified": false + }, + "l3_b_5_8.jpg": { + "type": "-", + "size": 15612, + "lmtime": 1741161344745, + "modified": false + }, + "l3_b_5_8.webp": { + "type": "-", + "size": 7726, + "lmtime": 1741161344836, + "modified": false + } + }, + "6": { + "l3_b_6_1.jpg": { + "type": "-", + "size": 46356, + "lmtime": 1741161344933, + "modified": false + }, + "l3_b_6_1.webp": { + "type": "-", + "size": 28368, + "lmtime": 1741161345029, + "modified": false + }, + "l3_b_6_2.jpg": { + "type": "-", + "size": 44022, + "lmtime": 1741161345124, + "modified": false + }, + "l3_b_6_2.webp": { + "type": "-", + "size": 26960, + "lmtime": 1741161345211, + "modified": false + }, + "l3_b_6_3.jpg": { + "type": "-", + "size": 40477, + "lmtime": 1741161345307, + "modified": false + }, + "l3_b_6_3.webp": { + "type": "-", + "size": 23414, + "lmtime": 1741161345417, + "modified": false + }, + "l3_b_6_4.jpg": { + "type": "-", + "size": 32127, + "lmtime": 1741161345515, + "modified": false + }, + "l3_b_6_4.webp": { + "type": "-", + "size": 16550, + "lmtime": 1741161345627, + "modified": false + }, + "l3_b_6_5.jpg": { + "type": "-", + "size": 47285, + "lmtime": 1741161345734, + "modified": false + }, + "l3_b_6_5.webp": { + "type": "-", + "size": 27700, + "lmtime": 1741161345842, + "modified": false + }, + "l3_b_6_6.jpg": { + "type": "-", + "size": 38024, + "lmtime": 1741161345956, + "modified": false + }, + "l3_b_6_6.webp": { + "type": "-", + "size": 20368, + "lmtime": 1741161346057, + "modified": false + }, + "l3_b_6_7.jpg": { + "type": "-", + "size": 32919, + "lmtime": 1741161346161, + "modified": false + }, + "l3_b_6_7.webp": { + "type": "-", + "size": 16008, + "lmtime": 1741161346271, + "modified": false + }, + "l3_b_6_8.jpg": { + "type": "-", + "size": 16225, + "lmtime": 1741161346361, + "modified": false + }, + "l3_b_6_8.webp": { + "type": "-", + "size": 7538, + "lmtime": 1741161346433, + "modified": false + } + }, + "7": { + "l3_b_7_1.jpg": { + "type": "-", + "size": 43435, + "lmtime": 1741161346526, + "modified": false + }, + "l3_b_7_1.webp": { + "type": "-", + "size": 26776, + "lmtime": 1741161346616, + "modified": false + }, + "l3_b_7_2.jpg": { + "type": "-", + "size": 36107, + "lmtime": 1741161346710, + "modified": false + }, + "l3_b_7_2.webp": { + "type": "-", + "size": 17352, + "lmtime": 1741161346820, + "modified": false + }, + "l3_b_7_3.jpg": { + "type": "-", + "size": 54496, + "lmtime": 1741161346967, + "modified": false + }, + "l3_b_7_3.webp": { + "type": "-", + "size": 43614, + "lmtime": 1741161347066, + "modified": false + }, + "l3_b_7_4.jpg": { + "type": "-", + "size": 44233, + "lmtime": 1741161347165, + "modified": false + }, + "l3_b_7_4.webp": { + "type": "-", + "size": 31212, + "lmtime": 1741161347256, + "modified": false + }, + "l3_b_7_5.jpg": { + "type": "-", + "size": 40704, + "lmtime": 1741161347379, + "modified": false + }, + "l3_b_7_5.webp": { + "type": "-", + "size": 22260, + "lmtime": 1741161347479, + "modified": false + }, + "l3_b_7_6.jpg": { + "type": "-", + "size": 37619, + "lmtime": 1741161347579, + "modified": false + }, + "l3_b_7_6.webp": { + "type": "-", + "size": 20026, + "lmtime": 1741161347672, + "modified": false + }, + "l3_b_7_7.jpg": { + "type": "-", + "size": 35284, + "lmtime": 1741161347760, + "modified": false + }, + "l3_b_7_7.webp": { + "type": "-", + "size": 17304, + "lmtime": 1741161347846, + "modified": false + }, + "l3_b_7_8.jpg": { + "type": "-", + "size": 17291, + "lmtime": 1741161347966, + "modified": false + }, + "l3_b_7_8.webp": { + "type": "-", + "size": 8424, + "lmtime": 1741161348074, + "modified": false + } + }, + "8": { + "l3_b_8_1.jpg": { + "type": "-", + "size": 17308, + "lmtime": 1741161348185, + "modified": false + }, + "l3_b_8_1.webp": { + "type": "-", + "size": 8350, + "lmtime": 1741161348279, + "modified": false + }, + "l3_b_8_2.jpg": { + "type": "-", + "size": 13451, + "lmtime": 1741161348366, + "modified": false + }, + "l3_b_8_2.webp": { + "type": "-", + "size": 6040, + "lmtime": 1741161348468, + "modified": false + }, + "l3_b_8_3.jpg": { + "type": "-", + "size": 26957, + "lmtime": 1741161348571, + "modified": false + }, + "l3_b_8_3.webp": { + "type": "-", + "size": 20632, + "lmtime": 1741161348661, + "modified": false + }, + "l3_b_8_4.jpg": { + "type": "-", + "size": 21176, + "lmtime": 1741161348759, + "modified": false + }, + "l3_b_8_4.webp": { + "type": "-", + "size": 14518, + "lmtime": 1741161348836, + "modified": false + }, + "l3_b_8_5.jpg": { + "type": "-", + "size": 18288, + "lmtime": 1741161348943, + "modified": false + }, + "l3_b_8_5.webp": { + "type": "-", + "size": 9858, + "lmtime": 1741161349032, + "modified": false + }, + "l3_b_8_6.jpg": { + "type": "-", + "size": 17033, + "lmtime": 1741161349140, + "modified": false + }, + "l3_b_8_6.webp": { + "type": "-", + "size": 8004, + "lmtime": 1741161349226, + "modified": false + }, + "l3_b_8_7.jpg": { + "type": "-", + "size": 14559, + "lmtime": 1741161349320, + "modified": false + }, + "l3_b_8_7.webp": { + "type": "-", + "size": 6460, + "lmtime": 1741161349397, + "modified": false + }, + "l3_b_8_8.jpg": { + "type": "-", + "size": 7128, + "lmtime": 1741161349468, + "modified": false + }, + "l3_b_8_8.webp": { + "type": "-", + "size": 3112, + "lmtime": 1741161349537, + "modified": false + } + } + } + }, + "d": { + "l1": { + "1": { + "l1_d_1_1.jpg": { + "type": "-", + "size": 29355, + "lmtime": 1741161349637, + "modified": false + }, + "l1_d_1_1.webp": { + "type": "-", + "size": 15512, + "lmtime": 1741161349717, + "modified": false + }, + "l1_d_1_2.jpg": { + "type": "-", + "size": 30717, + "lmtime": 1741161349833, + "modified": false + }, + "l1_d_1_2.webp": { + "type": "-", + "size": 14898, + "lmtime": 1741161349935, + "modified": false + } + }, + "2": { + "l1_d_2_1.jpg": { + "type": "-", + "size": 29147, + "lmtime": 1741161350050, + "modified": false + }, + "l1_d_2_1.webp": { + "type": "-", + "size": 15010, + "lmtime": 1741161350134, + "modified": false + }, + "l1_d_2_2.jpg": { + "type": "-", + "size": 29282, + "lmtime": 1741161350238, + "modified": false + }, + "l1_d_2_2.webp": { + "type": "-", + "size": 14482, + "lmtime": 1741161350318, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_d_1_1.jpg": { + "type": "-", + "size": 37825, + "lmtime": 1741161350437, + "modified": false + }, + "l2_d_1_1.webp": { + "type": "-", + "size": 21018, + "lmtime": 1741161350547, + "modified": false + }, + "l2_d_1_2.jpg": { + "type": "-", + "size": 21363, + "lmtime": 1741161350661, + "modified": false + }, + "l2_d_1_2.webp": { + "type": "-", + "size": 8824, + "lmtime": 1741161350753, + "modified": false + }, + "l2_d_1_3.jpg": { + "type": "-", + "size": 31179, + "lmtime": 1741161350847, + "modified": false + }, + "l2_d_1_3.webp": { + "type": "-", + "size": 17398, + "lmtime": 1741161350949, + "modified": false + }, + "l2_d_1_4.jpg": { + "type": "-", + "size": 23000, + "lmtime": 1741161351045, + "modified": false + }, + "l2_d_1_4.webp": { + "type": "-", + "size": 14250, + "lmtime": 1741161351142, + "modified": false + } + }, + "2": { + "l2_d_2_1.jpg": { + "type": "-", + "size": 42364, + "lmtime": 1741161351239, + "modified": false + }, + "l2_d_2_1.webp": { + "type": "-", + "size": 28864, + "lmtime": 1741161351345, + "modified": false + }, + "l2_d_2_2.jpg": { + "type": "-", + "size": 22370, + "lmtime": 1741161351450, + "modified": false + }, + "l2_d_2_2.webp": { + "type": "-", + "size": 9424, + "lmtime": 1741161351552, + "modified": false + }, + "l2_d_2_3.jpg": { + "type": "-", + "size": 30077, + "lmtime": 1741161351656, + "modified": false + }, + "l2_d_2_3.webp": { + "type": "-", + "size": 16578, + "lmtime": 1741161351770, + "modified": false + }, + "l2_d_2_4.jpg": { + "type": "-", + "size": 24713, + "lmtime": 1741161351891, + "modified": false + }, + "l2_d_2_4.webp": { + "type": "-", + "size": 17996, + "lmtime": 1741161352014, + "modified": false + } + }, + "3": { + "l2_d_3_1.jpg": { + "type": "-", + "size": 33245, + "lmtime": 1741161352121, + "modified": false + }, + "l2_d_3_1.webp": { + "type": "-", + "size": 19688, + "lmtime": 1741161352219, + "modified": false + }, + "l2_d_3_2.jpg": { + "type": "-", + "size": 27290, + "lmtime": 1741161352328, + "modified": false + }, + "l2_d_3_2.webp": { + "type": "-", + "size": 12594, + "lmtime": 1741161352429, + "modified": false + }, + "l2_d_3_3.jpg": { + "type": "-", + "size": 26222, + "lmtime": 1741161352546, + "modified": false + }, + "l2_d_3_3.webp": { + "type": "-", + "size": 12336, + "lmtime": 1741161352640, + "modified": false + }, + "l2_d_3_4.jpg": { + "type": "-", + "size": 24322, + "lmtime": 1741161352745, + "modified": false + }, + "l2_d_3_4.webp": { + "type": "-", + "size": 16916, + "lmtime": 1741161352869, + "modified": false + } + }, + "4": { + "l2_d_4_1.jpg": { + "type": "-", + "size": 11931, + "lmtime": 1741161352954, + "modified": false + }, + "l2_d_4_1.webp": { + "type": "-", + "size": 5716, + "lmtime": 1741161353056, + "modified": false + }, + "l2_d_4_2.jpg": { + "type": "-", + "size": 15556, + "lmtime": 1741161353156, + "modified": false + }, + "l2_d_4_2.webp": { + "type": "-", + "size": 7206, + "lmtime": 1741161353241, + "modified": false + }, + "l2_d_4_3.jpg": { + "type": "-", + "size": 12087, + "lmtime": 1741161353338, + "modified": false + }, + "l2_d_4_3.webp": { + "type": "-", + "size": 5042, + "lmtime": 1741161353441, + "modified": false + }, + "l2_d_4_4.jpg": { + "type": "-", + "size": 8742, + "lmtime": 1741161353538, + "modified": false + }, + "l2_d_4_4.webp": { + "type": "-", + "size": 4666, + "lmtime": 1741161353642, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_d_1_1.jpg": { + "type": "-", + "size": 47004, + "lmtime": 1741161353775, + "modified": false + }, + "l3_d_1_1.webp": { + "type": "-", + "size": 26946, + "lmtime": 1741161353878, + "modified": false + }, + "l3_d_1_2.jpg": { + "type": "-", + "size": 31389, + "lmtime": 1741161353973, + "modified": false + }, + "l3_d_1_2.webp": { + "type": "-", + "size": 15926, + "lmtime": 1741161354082, + "modified": false + }, + "l3_d_1_3.jpg": { + "type": "-", + "size": 29998, + "lmtime": 1741161354196, + "modified": false + }, + "l3_d_1_3.webp": { + "type": "-", + "size": 17368, + "lmtime": 1741161354305, + "modified": false + }, + "l3_d_1_4.jpg": { + "type": "-", + "size": 28919, + "lmtime": 1741161354395, + "modified": false + }, + "l3_d_1_4.webp": { + "type": "-", + "size": 15948, + "lmtime": 1741161354483, + "modified": false + }, + "l3_d_1_5.jpg": { + "type": "-", + "size": 28136, + "lmtime": 1741161354589, + "modified": false + }, + "l3_d_1_5.webp": { + "type": "-", + "size": 15228, + "lmtime": 1741161354682, + "modified": false + }, + "l3_d_1_6.jpg": { + "type": "-", + "size": 30101, + "lmtime": 1741161354784, + "modified": false + }, + "l3_d_1_6.webp": { + "type": "-", + "size": 16296, + "lmtime": 1741161354909, + "modified": false + }, + "l3_d_1_7.jpg": { + "type": "-", + "size": 34765, + "lmtime": 1741161355015, + "modified": false + }, + "l3_d_1_7.webp": { + "type": "-", + "size": 18776, + "lmtime": 1741161355116, + "modified": false + }, + "l3_d_1_8.jpg": { + "type": "-", + "size": 18902, + "lmtime": 1741161355201, + "modified": false + }, + "l3_d_1_8.webp": { + "type": "-", + "size": 10612, + "lmtime": 1741161355282, + "modified": false + } + }, + "2": { + "l3_d_2_1.jpg": { + "type": "-", + "size": 27503, + "lmtime": 1741161355403, + "modified": false + }, + "l3_d_2_1.webp": { + "type": "-", + "size": 12572, + "lmtime": 1741161355485, + "modified": false + }, + "l3_d_2_2.jpg": { + "type": "-", + "size": 37250, + "lmtime": 1741161355594, + "modified": false + }, + "l3_d_2_2.webp": { + "type": "-", + "size": 20886, + "lmtime": 1741161355719, + "modified": false + }, + "l3_d_2_3.jpg": { + "type": "-", + "size": 30321, + "lmtime": 1741161355825, + "modified": false + }, + "l3_d_2_3.webp": { + "type": "-", + "size": 17672, + "lmtime": 1741161355927, + "modified": false + }, + "l3_d_2_4.jpg": { + "type": "-", + "size": 30734, + "lmtime": 1741161356022, + "modified": false + }, + "l3_d_2_4.webp": { + "type": "-", + "size": 18404, + "lmtime": 1741161356110, + "modified": false + }, + "l3_d_2_5.jpg": { + "type": "-", + "size": 27605, + "lmtime": 1741161356214, + "modified": false + }, + "l3_d_2_5.webp": { + "type": "-", + "size": 14852, + "lmtime": 1741161356296, + "modified": false + }, + "l3_d_2_6.jpg": { + "type": "-", + "size": 33080, + "lmtime": 1741161356384, + "modified": false + }, + "l3_d_2_6.webp": { + "type": "-", + "size": 19420, + "lmtime": 1741161356475, + "modified": false + }, + "l3_d_2_7.jpg": { + "type": "-", + "size": 39571, + "lmtime": 1741161356583, + "modified": false + }, + "l3_d_2_7.webp": { + "type": "-", + "size": 23696, + "lmtime": 1741161356669, + "modified": false + }, + "l3_d_2_8.jpg": { + "type": "-", + "size": 23671, + "lmtime": 1741161356758, + "modified": false + }, + "l3_d_2_8.webp": { + "type": "-", + "size": 15778, + "lmtime": 1741161356837, + "modified": false + } + }, + "3": { + "l3_d_3_1.jpg": { + "type": "-", + "size": 54824, + "lmtime": 1741161356937, + "modified": false + }, + "l3_d_3_1.webp": { + "type": "-", + "size": 34686, + "lmtime": 1741161357030, + "modified": false + }, + "l3_d_3_2.jpg": { + "type": "-", + "size": 39689, + "lmtime": 1741161357128, + "modified": false + }, + "l3_d_3_2.webp": { + "type": "-", + "size": 23114, + "lmtime": 1741161357222, + "modified": false + }, + "l3_d_3_3.jpg": { + "type": "-", + "size": 30499, + "lmtime": 1741161357318, + "modified": false + }, + "l3_d_3_3.webp": { + "type": "-", + "size": 18032, + "lmtime": 1741161357408, + "modified": false + }, + "l3_d_3_4.jpg": { + "type": "-", + "size": 26555, + "lmtime": 1741161357493, + "modified": false + }, + "l3_d_3_4.webp": { + "type": "-", + "size": 12910, + "lmtime": 1741161357563, + "modified": false + }, + "l3_d_3_5.jpg": { + "type": "-", + "size": 28537, + "lmtime": 1741161357648, + "modified": false + }, + "l3_d_3_5.webp": { + "type": "-", + "size": 14846, + "lmtime": 1741161357734, + "modified": false + }, + "l3_d_3_6.jpg": { + "type": "-", + "size": 34339, + "lmtime": 1741161357828, + "modified": false + }, + "l3_d_3_6.webp": { + "type": "-", + "size": 20318, + "lmtime": 1741161357926, + "modified": false + }, + "l3_d_3_7.jpg": { + "type": "-", + "size": 40158, + "lmtime": 1741161358019, + "modified": false + }, + "l3_d_3_7.webp": { + "type": "-", + "size": 25174, + "lmtime": 1741161358103, + "modified": false + }, + "l3_d_3_8.jpg": { + "type": "-", + "size": 23365, + "lmtime": 1741161358191, + "modified": false + }, + "l3_d_3_8.webp": { + "type": "-", + "size": 16676, + "lmtime": 1741161358292, + "modified": false + } + }, + "4": { + "l3_d_4_1.jpg": { + "type": "-", + "size": 42181, + "lmtime": 1741161358384, + "modified": false + }, + "l3_d_4_1.webp": { + "type": "-", + "size": 28662, + "lmtime": 1741161358472, + "modified": false + }, + "l3_d_4_2.jpg": { + "type": "-", + "size": 41888, + "lmtime": 1741161358565, + "modified": false + }, + "l3_d_4_2.webp": { + "type": "-", + "size": 27480, + "lmtime": 1741161358665, + "modified": false + }, + "l3_d_4_3.jpg": { + "type": "-", + "size": 27381, + "lmtime": 1741161358752, + "modified": false + }, + "l3_d_4_3.webp": { + "type": "-", + "size": 13552, + "lmtime": 1741161358825, + "modified": false + }, + "l3_d_4_4.jpg": { + "type": "-", + "size": 10413, + "lmtime": 1741161358900, + "modified": false + }, + "l3_d_4_4.webp": { + "type": "-", + "size": 4854, + "lmtime": 1741161358971, + "modified": false + }, + "l3_d_4_5.jpg": { + "type": "-", + "size": 16464, + "lmtime": 1741161359057, + "modified": false + }, + "l3_d_4_5.webp": { + "type": "-", + "size": 5876, + "lmtime": 1741161359136, + "modified": false + }, + "l3_d_4_6.jpg": { + "type": "-", + "size": 34375, + "lmtime": 1741161359232, + "modified": false + }, + "l3_d_4_6.webp": { + "type": "-", + "size": 20724, + "lmtime": 1741161359321, + "modified": false + }, + "l3_d_4_7.jpg": { + "type": "-", + "size": 42121, + "lmtime": 1741161359419, + "modified": false + }, + "l3_d_4_7.webp": { + "type": "-", + "size": 26650, + "lmtime": 1741161359509, + "modified": false + }, + "l3_d_4_8.jpg": { + "type": "-", + "size": 24990, + "lmtime": 1741161359608, + "modified": false + }, + "l3_d_4_8.webp": { + "type": "-", + "size": 16648, + "lmtime": 1741161359717, + "modified": false + } + }, + "5": { + "l3_d_5_1.jpg": { + "type": "-", + "size": 34074, + "lmtime": 1741161359843, + "modified": false + }, + "l3_d_5_1.webp": { + "type": "-", + "size": 20390, + "lmtime": 1741161359929, + "modified": false + }, + "l3_d_5_2.jpg": { + "type": "-", + "size": 36184, + "lmtime": 1741161360030, + "modified": false + }, + "l3_d_5_2.webp": { + "type": "-", + "size": 22642, + "lmtime": 1741161360126, + "modified": false + }, + "l3_d_5_3.jpg": { + "type": "-", + "size": 31537, + "lmtime": 1741161360230, + "modified": false + }, + "l3_d_5_3.webp": { + "type": "-", + "size": 17750, + "lmtime": 1741161360326, + "modified": false + }, + "l3_d_5_4.jpg": { + "type": "-", + "size": 19134, + "lmtime": 1741161360417, + "modified": false + }, + "l3_d_5_4.webp": { + "type": "-", + "size": 7358, + "lmtime": 1741161360505, + "modified": false + }, + "l3_d_5_5.jpg": { + "type": "-", + "size": 23931, + "lmtime": 1741161360613, + "modified": false + }, + "l3_d_5_5.webp": { + "type": "-", + "size": 10562, + "lmtime": 1741161360697, + "modified": false + }, + "l3_d_5_6.jpg": { + "type": "-", + "size": 33208, + "lmtime": 1741161360820, + "modified": false + }, + "l3_d_5_6.webp": { + "type": "-", + "size": 19358, + "lmtime": 1741161360924, + "modified": false + }, + "l3_d_5_7.jpg": { + "type": "-", + "size": 38814, + "lmtime": 1741161361037, + "modified": false + }, + "l3_d_5_7.webp": { + "type": "-", + "size": 22186, + "lmtime": 1741161361130, + "modified": false + }, + "l3_d_5_8.jpg": { + "type": "-", + "size": 27239, + "lmtime": 1741161361237, + "modified": false + }, + "l3_d_5_8.webp": { + "type": "-", + "size": 19524, + "lmtime": 1741161361456, + "modified": false + } + }, + "6": { + "l3_d_6_1.jpg": { + "type": "-", + "size": 39661, + "lmtime": 1741161361581, + "modified": false + }, + "l3_d_6_1.webp": { + "type": "-", + "size": 23562, + "lmtime": 1741161361696, + "modified": false + }, + "l3_d_6_2.jpg": { + "type": "-", + "size": 41202, + "lmtime": 1741161361821, + "modified": false + }, + "l3_d_6_2.webp": { + "type": "-", + "size": 24870, + "lmtime": 1741161361932, + "modified": false + }, + "l3_d_6_3.jpg": { + "type": "-", + "size": 41129, + "lmtime": 1741161362051, + "modified": false + }, + "l3_d_6_3.webp": { + "type": "-", + "size": 24290, + "lmtime": 1741161362165, + "modified": false + }, + "l3_d_6_4.jpg": { + "type": "-", + "size": 30632, + "lmtime": 1741161362284, + "modified": false + }, + "l3_d_6_4.webp": { + "type": "-", + "size": 17500, + "lmtime": 1741161362397, + "modified": false + }, + "l3_d_6_5.jpg": { + "type": "-", + "size": 28603, + "lmtime": 1741161362492, + "modified": false + }, + "l3_d_6_5.webp": { + "type": "-", + "size": 15764, + "lmtime": 1741161362577, + "modified": false + }, + "l3_d_6_6.jpg": { + "type": "-", + "size": 31878, + "lmtime": 1741161362684, + "modified": false + }, + "l3_d_6_6.webp": { + "type": "-", + "size": 18632, + "lmtime": 1741161362773, + "modified": false + }, + "l3_d_6_7.jpg": { + "type": "-", + "size": 38475, + "lmtime": 1741161362871, + "modified": false + }, + "l3_d_6_7.webp": { + "type": "-", + "size": 22394, + "lmtime": 1741161362979, + "modified": false + }, + "l3_d_6_8.jpg": { + "type": "-", + "size": 22403, + "lmtime": 1741161363098, + "modified": false + }, + "l3_d_6_8.webp": { + "type": "-", + "size": 14158, + "lmtime": 1741161363188, + "modified": false + } + }, + "7": { + "l3_d_7_1.jpg": { + "type": "-", + "size": 23808, + "lmtime": 1741161363303, + "modified": false + }, + "l3_d_7_1.webp": { + "type": "-", + "size": 9178, + "lmtime": 1741161363517, + "modified": false + }, + "l3_d_7_2.jpg": { + "type": "-", + "size": 17125, + "lmtime": 1741161363627, + "modified": false + }, + "l3_d_7_2.webp": { + "type": "-", + "size": 6136, + "lmtime": 1741161363725, + "modified": false + }, + "l3_d_7_3.jpg": { + "type": "-", + "size": 31070, + "lmtime": 1741161363827, + "modified": false + }, + "l3_d_7_3.webp": { + "type": "-", + "size": 14222, + "lmtime": 1741161363953, + "modified": false + }, + "l3_d_7_4.jpg": { + "type": "-", + "size": 30329, + "lmtime": 1741161364122, + "modified": false + }, + "l3_d_7_4.webp": { + "type": "-", + "size": 16992, + "lmtime": 1741161364219, + "modified": false + }, + "l3_d_7_5.jpg": { + "type": "-", + "size": 26276, + "lmtime": 1741161364324, + "modified": false + }, + "l3_d_7_5.webp": { + "type": "-", + "size": 13592, + "lmtime": 1741161364404, + "modified": false + }, + "l3_d_7_6.jpg": { + "type": "-", + "size": 25100, + "lmtime": 1741161364507, + "modified": false + }, + "l3_d_7_6.webp": { + "type": "-", + "size": 12736, + "lmtime": 1741161364596, + "modified": false + }, + "l3_d_7_7.jpg": { + "type": "-", + "size": 29524, + "lmtime": 1741161364689, + "modified": false + }, + "l3_d_7_7.webp": { + "type": "-", + "size": 12994, + "lmtime": 1741161364766, + "modified": false + }, + "l3_d_7_8.jpg": { + "type": "-", + "size": 15438, + "lmtime": 1741161364846, + "modified": false + }, + "l3_d_7_8.webp": { + "type": "-", + "size": 7524, + "lmtime": 1741161364934, + "modified": false + } + }, + "8": { + "l3_d_8_1.jpg": { + "type": "-", + "size": 6408, + "lmtime": 1741161365013, + "modified": false + }, + "l3_d_8_1.webp": { + "type": "-", + "size": 1776, + "lmtime": 1741161365101, + "modified": false + }, + "l3_d_8_2.jpg": { + "type": "-", + "size": 4485, + "lmtime": 1741161365186, + "modified": false + }, + "l3_d_8_2.webp": { + "type": "-", + "size": 1146, + "lmtime": 1741161365257, + "modified": false + }, + "l3_d_8_3.jpg": { + "type": "-", + "size": 15740, + "lmtime": 1741161365346, + "modified": false + }, + "l3_d_8_3.webp": { + "type": "-", + "size": 7372, + "lmtime": 1741161365436, + "modified": false + }, + "l3_d_8_4.jpg": { + "type": "-", + "size": 14110, + "lmtime": 1741161365525, + "modified": false + }, + "l3_d_8_4.webp": { + "type": "-", + "size": 7372, + "lmtime": 1741161365619, + "modified": false + }, + "l3_d_8_5.jpg": { + "type": "-", + "size": 13922, + "lmtime": 1741161365844, + "modified": false + }, + "l3_d_8_5.webp": { + "type": "-", + "size": 6920, + "lmtime": 1741161365922, + "modified": false + }, + "l3_d_8_6.jpg": { + "type": "-", + "size": 13657, + "lmtime": 1741161365994, + "modified": false + }, + "l3_d_8_6.webp": { + "type": "-", + "size": 6750, + "lmtime": 1741161366077, + "modified": false + }, + "l3_d_8_7.jpg": { + "type": "-", + "size": 13296, + "lmtime": 1741161366154, + "modified": false + }, + "l3_d_8_7.webp": { + "type": "-", + "size": 5646, + "lmtime": 1741161366232, + "modified": false + }, + "l3_d_8_8.jpg": { + "type": "-", + "size": 7075, + "lmtime": 1741161366323, + "modified": false + }, + "l3_d_8_8.webp": { + "type": "-", + "size": 3180, + "lmtime": 1741161366408, + "modified": false + } + } + } + }, + "f": { + "l1": { + "1": { + "l1_f_1_1.jpg": { + "type": "-", + "size": 16073, + "lmtime": 1741161366520, + "modified": false + }, + "l1_f_1_1.webp": { + "type": "-", + "size": 7330, + "lmtime": 1741161366624, + "modified": false + }, + "l1_f_1_2.jpg": { + "type": "-", + "size": 30335, + "lmtime": 1741161366745, + "modified": false + }, + "l1_f_1_2.webp": { + "type": "-", + "size": 16788, + "lmtime": 1741161366852, + "modified": false + } + }, + "2": { + "l1_f_2_1.jpg": { + "type": "-", + "size": 49556, + "lmtime": 1741161366979, + "modified": false + }, + "l1_f_2_1.webp": { + "type": "-", + "size": 33090, + "lmtime": 1741161367100, + "modified": false + }, + "l1_f_2_2.jpg": { + "type": "-", + "size": 48156, + "lmtime": 1741161367211, + "modified": false + }, + "l1_f_2_2.webp": { + "type": "-", + "size": 28884, + "lmtime": 1741161367318, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_f_1_1.jpg": { + "type": "-", + "size": 11434, + "lmtime": 1741161367419, + "modified": false + }, + "l2_f_1_1.webp": { + "type": "-", + "size": 3570, + "lmtime": 1741161367495, + "modified": false + }, + "l2_f_1_2.jpg": { + "type": "-", + "size": 8713, + "lmtime": 1741161367579, + "modified": false + }, + "l2_f_1_2.webp": { + "type": "-", + "size": 3248, + "lmtime": 1741161367672, + "modified": false + }, + "l2_f_1_3.jpg": { + "type": "-", + "size": 15758, + "lmtime": 1741161367760, + "modified": false + }, + "l2_f_1_3.webp": { + "type": "-", + "size": 7372, + "lmtime": 1741161367829, + "modified": false + }, + "l2_f_1_4.jpg": { + "type": "-", + "size": 7427, + "lmtime": 1741161367907, + "modified": false + }, + "l2_f_1_4.webp": { + "type": "-", + "size": 3136, + "lmtime": 1741161367976, + "modified": false + } + }, + "2": { + "l2_f_2_1.jpg": { + "type": "-", + "size": 20956, + "lmtime": 1741161368062, + "modified": false + }, + "l2_f_2_1.webp": { + "type": "-", + "size": 10402, + "lmtime": 1741161368148, + "modified": false + }, + "l2_f_2_2.jpg": { + "type": "-", + "size": 22209, + "lmtime": 1741161368237, + "modified": false + }, + "l2_f_2_2.webp": { + "type": "-", + "size": 10990, + "lmtime": 1741161368330, + "modified": false + }, + "l2_f_2_3.jpg": { + "type": "-", + "size": 36315, + "lmtime": 1741161368419, + "modified": false + }, + "l2_f_2_3.webp": { + "type": "-", + "size": 19306, + "lmtime": 1741161368517, + "modified": false + }, + "l2_f_2_4.jpg": { + "type": "-", + "size": 28094, + "lmtime": 1741161368616, + "modified": false + }, + "l2_f_2_4.webp": { + "type": "-", + "size": 19248, + "lmtime": 1741161368705, + "modified": false + } + }, + "3": { + "l2_f_3_1.jpg": { + "type": "-", + "size": 67731, + "lmtime": 1741161368822, + "modified": false + }, + "l2_f_3_1.webp": { + "type": "-", + "size": 53706, + "lmtime": 1741161368930, + "modified": false + }, + "l2_f_3_2.jpg": { + "type": "-", + "size": 35096, + "lmtime": 1741161369029, + "modified": false + }, + "l2_f_3_2.webp": { + "type": "-", + "size": 18134, + "lmtime": 1741161369128, + "modified": false + }, + "l2_f_3_3.jpg": { + "type": "-", + "size": 47557, + "lmtime": 1741161369243, + "modified": false + }, + "l2_f_3_3.webp": { + "type": "-", + "size": 28656, + "lmtime": 1741161369477, + "modified": false + }, + "l2_f_3_4.jpg": { + "type": "-", + "size": 26956, + "lmtime": 1741161369575, + "modified": false + }, + "l2_f_3_4.webp": { + "type": "-", + "size": 18776, + "lmtime": 1741161369679, + "modified": false + } + }, + "4": { + "l2_f_4_1.jpg": { + "type": "-", + "size": 26091, + "lmtime": 1741161369769, + "modified": false + }, + "l2_f_4_1.webp": { + "type": "-", + "size": 17694, + "lmtime": 1741161369875, + "modified": false + }, + "l2_f_4_2.jpg": { + "type": "-", + "size": 9000, + "lmtime": 1741161369975, + "modified": false + }, + "l2_f_4_2.webp": { + "type": "-", + "size": 2790, + "lmtime": 1741161370063, + "modified": false + }, + "l2_f_4_3.jpg": { + "type": "-", + "size": 19536, + "lmtime": 1741161370177, + "modified": false + }, + "l2_f_4_3.webp": { + "type": "-", + "size": 11620, + "lmtime": 1741161370265, + "modified": false + }, + "l2_f_4_4.jpg": { + "type": "-", + "size": 10298, + "lmtime": 1741161370362, + "modified": false + }, + "l2_f_4_4.webp": { + "type": "-", + "size": 6074, + "lmtime": 1741161370457, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_f_1_1.jpg": { + "type": "-", + "size": 17535, + "lmtime": 1741161370592, + "modified": false + }, + "l3_f_1_1.webp": { + "type": "-", + "size": 5926, + "lmtime": 1741161370691, + "modified": false + }, + "l3_f_1_2.jpg": { + "type": "-", + "size": 16811, + "lmtime": 1741161370791, + "modified": false + }, + "l3_f_1_2.webp": { + "type": "-", + "size": 5216, + "lmtime": 1741161370887, + "modified": false + }, + "l3_f_1_3.jpg": { + "type": "-", + "size": 15604, + "lmtime": 1741161370985, + "modified": false + }, + "l3_f_1_3.webp": { + "type": "-", + "size": 4296, + "lmtime": 1741161371058, + "modified": false + }, + "l3_f_1_4.jpg": { + "type": "-", + "size": 11824, + "lmtime": 1741161371138, + "modified": false + }, + "l3_f_1_4.webp": { + "type": "-", + "size": 2602, + "lmtime": 1741161371208, + "modified": false + }, + "l3_f_1_5.jpg": { + "type": "-", + "size": 10699, + "lmtime": 1741161371279, + "modified": false + }, + "l3_f_1_5.webp": { + "type": "-", + "size": 1994, + "lmtime": 1741161371353, + "modified": false + }, + "l3_f_1_6.jpg": { + "type": "-", + "size": 12125, + "lmtime": 1741161371429, + "modified": false + }, + "l3_f_1_6.webp": { + "type": "-", + "size": 2948, + "lmtime": 1741161371500, + "modified": false + }, + "l3_f_1_7.jpg": { + "type": "-", + "size": 12411, + "lmtime": 1741161371579, + "modified": false + }, + "l3_f_1_7.webp": { + "type": "-", + "size": 3008, + "lmtime": 1741161371647, + "modified": false + }, + "l3_f_1_8.jpg": { + "type": "-", + "size": 7809, + "lmtime": 1741161371719, + "modified": false + }, + "l3_f_1_8.webp": { + "type": "-", + "size": 2340, + "lmtime": 1741161371794, + "modified": false + } + }, + "2": { + "l3_f_2_1.jpg": { + "type": "-", + "size": 15689, + "lmtime": 1741161371864, + "modified": false + }, + "l3_f_2_1.webp": { + "type": "-", + "size": 4556, + "lmtime": 1741161371943, + "modified": false + }, + "l3_f_2_2.jpg": { + "type": "-", + "size": 15041, + "lmtime": 1741161372016, + "modified": false + }, + "l3_f_2_2.webp": { + "type": "-", + "size": 3930, + "lmtime": 1741161372084, + "modified": false + }, + "l3_f_2_3.jpg": { + "type": "-", + "size": 17158, + "lmtime": 1741161372178, + "modified": false + }, + "l3_f_2_3.webp": { + "type": "-", + "size": 4870, + "lmtime": 1741161372259, + "modified": false + }, + "l3_f_2_4.jpg": { + "type": "-", + "size": 8520, + "lmtime": 1741161372344, + "modified": false + }, + "l3_f_2_4.webp": { + "type": "-", + "size": 1412, + "lmtime": 1741161372419, + "modified": false + }, + "l3_f_2_5.jpg": { + "type": "-", + "size": 17957, + "lmtime": 1741161372506, + "modified": false + }, + "l3_f_2_5.webp": { + "type": "-", + "size": 6562, + "lmtime": 1741161372576, + "modified": false + }, + "l3_f_2_6.jpg": { + "type": "-", + "size": 20557, + "lmtime": 1741161372667, + "modified": false + }, + "l3_f_2_6.webp": { + "type": "-", + "size": 7544, + "lmtime": 1741161372748, + "modified": false + }, + "l3_f_2_7.jpg": { + "type": "-", + "size": 17759, + "lmtime": 1741161372832, + "modified": false + }, + "l3_f_2_7.webp": { + "type": "-", + "size": 5994, + "lmtime": 1741161372914, + "modified": false + }, + "l3_f_2_8.jpg": { + "type": "-", + "size": 8396, + "lmtime": 1741161372993, + "modified": false + }, + "l3_f_2_8.webp": { + "type": "-", + "size": 2852, + "lmtime": 1741161373067, + "modified": false + } + }, + "3": { + "l3_f_3_1.jpg": { + "type": "-", + "size": 19740, + "lmtime": 1741161373159, + "modified": false + }, + "l3_f_3_1.webp": { + "type": "-", + "size": 7444, + "lmtime": 1741161373243, + "modified": false + }, + "l3_f_3_2.jpg": { + "type": "-", + "size": 16369, + "lmtime": 1741161373325, + "modified": false + }, + "l3_f_3_2.webp": { + "type": "-", + "size": 5318, + "lmtime": 1741161373398, + "modified": false + }, + "l3_f_3_3.jpg": { + "type": "-", + "size": 18371, + "lmtime": 1741161373495, + "modified": false + }, + "l3_f_3_3.webp": { + "type": "-", + "size": 6684, + "lmtime": 1741161373568, + "modified": false + }, + "l3_f_3_4.jpg": { + "type": "-", + "size": 9719, + "lmtime": 1741161373649, + "modified": false + }, + "l3_f_3_4.webp": { + "type": "-", + "size": 3060, + "lmtime": 1741161373733, + "modified": false + }, + "l3_f_3_5.jpg": { + "type": "-", + "size": 23800, + "lmtime": 1741161373818, + "modified": false + }, + "l3_f_3_5.webp": { + "type": "-", + "size": 10960, + "lmtime": 1741161373890, + "modified": false + }, + "l3_f_3_6.jpg": { + "type": "-", + "size": 22282, + "lmtime": 1741161373986, + "modified": false + }, + "l3_f_3_6.webp": { + "type": "-", + "size": 9578, + "lmtime": 1741161374060, + "modified": false + }, + "l3_f_3_7.jpg": { + "type": "-", + "size": 36188, + "lmtime": 1741161374153, + "modified": false + }, + "l3_f_3_7.webp": { + "type": "-", + "size": 18562, + "lmtime": 1741161374243, + "modified": false + }, + "l3_f_3_8.jpg": { + "type": "-", + "size": 16904, + "lmtime": 1741161374331, + "modified": false + }, + "l3_f_3_8.webp": { + "type": "-", + "size": 9042, + "lmtime": 1741161374412, + "modified": false + } + }, + "4": { + "l3_f_4_1.jpg": { + "type": "-", + "size": 18520, + "lmtime": 1741161374518, + "modified": false + }, + "l3_f_4_1.webp": { + "type": "-", + "size": 5954, + "lmtime": 1741161374612, + "modified": false + }, + "l3_f_4_2.jpg": { + "type": "-", + "size": 10316, + "lmtime": 1741161374691, + "modified": false + }, + "l3_f_4_2.webp": { + "type": "-", + "size": 2230, + "lmtime": 1741161374774, + "modified": false + }, + "l3_f_4_3.jpg": { + "type": "-", + "size": 22962, + "lmtime": 1741161374863, + "modified": false + }, + "l3_f_4_3.webp": { + "type": "-", + "size": 9300, + "lmtime": 1741161374940, + "modified": false + }, + "l3_f_4_4.jpg": { + "type": "-", + "size": 11555, + "lmtime": 1741161375017, + "modified": false + }, + "l3_f_4_4.webp": { + "type": "-", + "size": 2954, + "lmtime": 1741161375095, + "modified": false + }, + "l3_f_4_5.jpg": { + "type": "-", + "size": 23527, + "lmtime": 1741161375173, + "modified": false + }, + "l3_f_4_5.webp": { + "type": "-", + "size": 9806, + "lmtime": 1741161375251, + "modified": false + }, + "l3_f_4_6.jpg": { + "type": "-", + "size": 34163, + "lmtime": 1741161375340, + "modified": false + }, + "l3_f_4_6.webp": { + "type": "-", + "size": 15588, + "lmtime": 1741161375430, + "modified": false + }, + "l3_f_4_7.jpg": { + "type": "-", + "size": 35763, + "lmtime": 1741161375548, + "modified": false + }, + "l3_f_4_7.webp": { + "type": "-", + "size": 18052, + "lmtime": 1741161375642, + "modified": false + }, + "l3_f_4_8.jpg": { + "type": "-", + "size": 24794, + "lmtime": 1741161375747, + "modified": false + }, + "l3_f_4_8.webp": { + "type": "-", + "size": 15660, + "lmtime": 1741161375831, + "modified": false + } + }, + "5": { + "l3_f_5_1.jpg": { + "type": "-", + "size": 42742, + "lmtime": 1741161375941, + "modified": false + }, + "l3_f_5_1.webp": { + "type": "-", + "size": 24438, + "lmtime": 1741161376048, + "modified": false + }, + "l3_f_5_2.jpg": { + "type": "-", + "size": 41609, + "lmtime": 1741161376159, + "modified": false + }, + "l3_f_5_2.webp": { + "type": "-", + "size": 24548, + "lmtime": 1741161376282, + "modified": false + }, + "l3_f_5_3.jpg": { + "type": "-", + "size": 44236, + "lmtime": 1741161376399, + "modified": false + }, + "l3_f_5_3.webp": { + "type": "-", + "size": 26328, + "lmtime": 1741161376520, + "modified": false + }, + "l3_f_5_4.jpg": { + "type": "-", + "size": 45720, + "lmtime": 1741161376647, + "modified": false + }, + "l3_f_5_4.webp": { + "type": "-", + "size": 27148, + "lmtime": 1741161376763, + "modified": false + }, + "l3_f_5_5.jpg": { + "type": "-", + "size": 44456, + "lmtime": 1741161376868, + "modified": false + }, + "l3_f_5_5.webp": { + "type": "-", + "size": 25308, + "lmtime": 1741161376975, + "modified": false + }, + "l3_f_5_6.jpg": { + "type": "-", + "size": 40049, + "lmtime": 1741161377089, + "modified": false + }, + "l3_f_5_6.webp": { + "type": "-", + "size": 20506, + "lmtime": 1741161377195, + "modified": false + }, + "l3_f_5_7.jpg": { + "type": "-", + "size": 39543, + "lmtime": 1741161377299, + "modified": false + }, + "l3_f_5_7.webp": { + "type": "-", + "size": 21926, + "lmtime": 1741161377384, + "modified": false + }, + "l3_f_5_8.jpg": { + "type": "-", + "size": 22676, + "lmtime": 1741161377477, + "modified": false + }, + "l3_f_5_8.webp": { + "type": "-", + "size": 14348, + "lmtime": 1741161377557, + "modified": false + } + }, + "6": { + "l3_f_6_1.jpg": { + "type": "-", + "size": 59365, + "lmtime": 1741161377658, + "modified": false + }, + "l3_f_6_1.webp": { + "type": "-", + "size": 41930, + "lmtime": 1741161377753, + "modified": false + }, + "l3_f_6_2.jpg": { + "type": "-", + "size": 55725, + "lmtime": 1741161377860, + "modified": false + }, + "l3_f_6_2.webp": { + "type": "-", + "size": 36852, + "lmtime": 1741161377949, + "modified": false + }, + "l3_f_6_3.jpg": { + "type": "-", + "size": 23717, + "lmtime": 1741161378037, + "modified": false + }, + "l3_f_6_3.webp": { + "type": "-", + "size": 9470, + "lmtime": 1741161378109, + "modified": false + }, + "l3_f_6_4.jpg": { + "type": "-", + "size": 19083, + "lmtime": 1741161378205, + "modified": false + }, + "l3_f_6_4.webp": { + "type": "-", + "size": 6366, + "lmtime": 1741161378284, + "modified": false + }, + "l3_f_6_5.jpg": { + "type": "-", + "size": 37254, + "lmtime": 1741161378373, + "modified": false + }, + "l3_f_6_5.webp": { + "type": "-", + "size": 18070, + "lmtime": 1741161378464, + "modified": false + }, + "l3_f_6_6.jpg": { + "type": "-", + "size": 37388, + "lmtime": 1741161378572, + "modified": false + }, + "l3_f_6_6.webp": { + "type": "-", + "size": 18788, + "lmtime": 1741161378656, + "modified": false + }, + "l3_f_6_7.jpg": { + "type": "-", + "size": 47552, + "lmtime": 1741161378756, + "modified": false + }, + "l3_f_6_7.webp": { + "type": "-", + "size": 28838, + "lmtime": 1741161378847, + "modified": false + }, + "l3_f_6_8.jpg": { + "type": "-", + "size": 25679, + "lmtime": 1741161378937, + "modified": false + }, + "l3_f_6_8.webp": { + "type": "-", + "size": 16246, + "lmtime": 1741161379028, + "modified": false + } + }, + "7": { + "l3_f_7_1.jpg": { + "type": "-", + "size": 56205, + "lmtime": 1741161379127, + "modified": false + }, + "l3_f_7_1.webp": { + "type": "-", + "size": 38342, + "lmtime": 1741161379216, + "modified": false + }, + "l3_f_7_2.jpg": { + "type": "-", + "size": 41945, + "lmtime": 1741161379314, + "modified": false + }, + "l3_f_7_2.webp": { + "type": "-", + "size": 22854, + "lmtime": 1741161379399, + "modified": false + }, + "l3_f_7_3.jpg": { + "type": "-", + "size": 24937, + "lmtime": 1741161379485, + "modified": false + }, + "l3_f_7_3.webp": { + "type": "-", + "size": 11146, + "lmtime": 1741161379560, + "modified": false + }, + "l3_f_7_4.jpg": { + "type": "-", + "size": 18898, + "lmtime": 1741161379643, + "modified": false + }, + "l3_f_7_4.webp": { + "type": "-", + "size": 7086, + "lmtime": 1741161379721, + "modified": false + }, + "l3_f_7_5.jpg": { + "type": "-", + "size": 33299, + "lmtime": 1741161379821, + "modified": false + }, + "l3_f_7_5.webp": { + "type": "-", + "size": 17886, + "lmtime": 1741161379908, + "modified": false + }, + "l3_f_7_6.jpg": { + "type": "-", + "size": 33227, + "lmtime": 1741161380050, + "modified": false + }, + "l3_f_7_6.webp": { + "type": "-", + "size": 17572, + "lmtime": 1741161380143, + "modified": false + }, + "l3_f_7_7.jpg": { + "type": "-", + "size": 32257, + "lmtime": 1741161380236, + "modified": false + }, + "l3_f_7_7.webp": { + "type": "-", + "size": 16396, + "lmtime": 1741161380322, + "modified": false + }, + "l3_f_7_8.jpg": { + "type": "-", + "size": 19699, + "lmtime": 1741161380415, + "modified": false + }, + "l3_f_7_8.webp": { + "type": "-", + "size": 10310, + "lmtime": 1741161380494, + "modified": false + } + }, + "8": { + "l3_f_8_1.jpg": { + "type": "-", + "size": 25147, + "lmtime": 1741161380580, + "modified": false + }, + "l3_f_8_1.webp": { + "type": "-", + "size": 15562, + "lmtime": 1741161380665, + "modified": false + }, + "l3_f_8_2.jpg": { + "type": "-", + "size": 14325, + "lmtime": 1741161380741, + "modified": false + }, + "l3_f_8_2.webp": { + "type": "-", + "size": 6848, + "lmtime": 1741161380814, + "modified": false + }, + "l3_f_8_3.jpg": { + "type": "-", + "size": 13594, + "lmtime": 1741161380890, + "modified": false + }, + "l3_f_8_3.webp": { + "type": "-", + "size": 6916, + "lmtime": 1741161380963, + "modified": false + }, + "l3_f_8_4.jpg": { + "type": "-", + "size": 13347, + "lmtime": 1741161381045, + "modified": false + }, + "l3_f_8_4.webp": { + "type": "-", + "size": 6812, + "lmtime": 1741161381126, + "modified": false + }, + "l3_f_8_5.jpg": { + "type": "-", + "size": 16045, + "lmtime": 1741161381203, + "modified": false + }, + "l3_f_8_5.webp": { + "type": "-", + "size": 9038, + "lmtime": 1741161381279, + "modified": false + }, + "l3_f_8_6.jpg": { + "type": "-", + "size": 18552, + "lmtime": 1741161381371, + "modified": false + }, + "l3_f_8_6.webp": { + "type": "-", + "size": 10248, + "lmtime": 1741161381447, + "modified": false + }, + "l3_f_8_7.jpg": { + "type": "-", + "size": 16325, + "lmtime": 1741161381534, + "modified": false + }, + "l3_f_8_7.webp": { + "type": "-", + "size": 7598, + "lmtime": 1741161381606, + "modified": false + }, + "l3_f_8_8.jpg": { + "type": "-", + "size": 7695, + "lmtime": 1741161381677, + "modified": false + }, + "l3_f_8_8.webp": { + "type": "-", + "size": 3738, + "lmtime": 1741161381757, + "modified": false + } + } + } + }, + "l": { + "l1": { + "1": { + "l1_l_1_1.jpg": { + "type": "-", + "size": 30191, + "lmtime": 1741161381864, + "modified": false + }, + "l1_l_1_1.webp": { + "type": "-", + "size": 17522, + "lmtime": 1741161381954, + "modified": false + }, + "l1_l_1_2.jpg": { + "type": "-", + "size": 29028, + "lmtime": 1741161382055, + "modified": false + }, + "l1_l_1_2.webp": { + "type": "-", + "size": 16740, + "lmtime": 1741161382162, + "modified": false + } + }, + "2": { + "l1_l_2_1.jpg": { + "type": "-", + "size": 45450, + "lmtime": 1741161382279, + "modified": false + }, + "l1_l_2_1.webp": { + "type": "-", + "size": 27388, + "lmtime": 1741161382399, + "modified": false + }, + "l1_l_2_2.jpg": { + "type": "-", + "size": 65569, + "lmtime": 1741161382512, + "modified": false + }, + "l1_l_2_2.webp": { + "type": "-", + "size": 51576, + "lmtime": 1741161382635, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_l_1_1.jpg": { + "type": "-", + "size": 15481, + "lmtime": 1741161382746, + "modified": false + }, + "l2_l_1_1.webp": { + "type": "-", + "size": 7130, + "lmtime": 1741161382855, + "modified": false + }, + "l2_l_1_2.jpg": { + "type": "-", + "size": 10806, + "lmtime": 1741161382948, + "modified": false + }, + "l2_l_1_2.webp": { + "type": "-", + "size": 4544, + "lmtime": 1741161383055, + "modified": false + }, + "l2_l_1_3.jpg": { + "type": "-", + "size": 14151, + "lmtime": 1741161383152, + "modified": false + }, + "l2_l_1_3.webp": { + "type": "-", + "size": 7688, + "lmtime": 1741161383244, + "modified": false + }, + "l2_l_1_4.jpg": { + "type": "-", + "size": 6286, + "lmtime": 1741161383333, + "modified": false + }, + "l2_l_1_4.webp": { + "type": "-", + "size": 2194, + "lmtime": 1741161383429, + "modified": false + } + }, + "2": { + "l2_l_2_1.jpg": { + "type": "-", + "size": 43061, + "lmtime": 1741161383542, + "modified": false + }, + "l2_l_2_1.webp": { + "type": "-", + "size": 25572, + "lmtime": 1741161383636, + "modified": false + }, + "l2_l_2_2.jpg": { + "type": "-", + "size": 53249, + "lmtime": 1741161383740, + "modified": false + }, + "l2_l_2_2.webp": { + "type": "-", + "size": 35334, + "lmtime": 1741161383846, + "modified": false + }, + "l2_l_2_3.jpg": { + "type": "-", + "size": 44123, + "lmtime": 1741161383951, + "modified": false + }, + "l2_l_2_3.webp": { + "type": "-", + "size": 26470, + "lmtime": 1741161384065, + "modified": false + }, + "l2_l_2_4.jpg": { + "type": "-", + "size": 15126, + "lmtime": 1741161384163, + "modified": false + }, + "l2_l_2_4.webp": { + "type": "-", + "size": 8494, + "lmtime": 1741161384266, + "modified": false + } + }, + "3": { + "l2_l_3_1.jpg": { + "type": "-", + "size": 40760, + "lmtime": 1741161384389, + "modified": false + }, + "l2_l_3_1.webp": { + "type": "-", + "size": 24602, + "lmtime": 1741161384508, + "modified": false + }, + "l2_l_3_2.jpg": { + "type": "-", + "size": 41244, + "lmtime": 1741161384631, + "modified": false + }, + "l2_l_3_2.webp": { + "type": "-", + "size": 24236, + "lmtime": 1741161384744, + "modified": false + }, + "l2_l_3_3.jpg": { + "type": "-", + "size": 64217, + "lmtime": 1741161384873, + "modified": false + }, + "l2_l_3_3.webp": { + "type": "-", + "size": 52818, + "lmtime": 1741161384987, + "modified": false + }, + "l2_l_3_4.jpg": { + "type": "-", + "size": 31951, + "lmtime": 1741161385083, + "modified": false + }, + "l2_l_3_4.webp": { + "type": "-", + "size": 24528, + "lmtime": 1741161385195, + "modified": false + } + }, + "4": { + "l2_l_4_1.jpg": { + "type": "-", + "size": 19764, + "lmtime": 1741161385303, + "modified": false + }, + "l2_l_4_1.webp": { + "type": "-", + "size": 10418, + "lmtime": 1741161385393, + "modified": false + }, + "l2_l_4_2.jpg": { + "type": "-", + "size": 15487, + "lmtime": 1741161385485, + "modified": false + }, + "l2_l_4_2.webp": { + "type": "-", + "size": 7902, + "lmtime": 1741161385584, + "modified": false + }, + "l2_l_4_3.jpg": { + "type": "-", + "size": 33152, + "lmtime": 1741161385700, + "modified": false + }, + "l2_l_4_3.webp": { + "type": "-", + "size": 26988, + "lmtime": 1741161385801, + "modified": false + }, + "l2_l_4_4.jpg": { + "type": "-", + "size": 18255, + "lmtime": 1741161385914, + "modified": false + }, + "l2_l_4_4.webp": { + "type": "-", + "size": 15678, + "lmtime": 1741161386000, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_l_1_1.jpg": { + "type": "-", + "size": 14722, + "lmtime": 1741161386121, + "modified": false + }, + "l3_l_1_1.webp": { + "type": "-", + "size": 3958, + "lmtime": 1741161386214, + "modified": false + }, + "l3_l_1_2.jpg": { + "type": "-", + "size": 13943, + "lmtime": 1741161386309, + "modified": false + }, + "l3_l_1_2.webp": { + "type": "-", + "size": 3784, + "lmtime": 1741161386395, + "modified": false + }, + "l3_l_1_3.jpg": { + "type": "-", + "size": 13544, + "lmtime": 1741161386486, + "modified": false + }, + "l3_l_1_3.webp": { + "type": "-", + "size": 3792, + "lmtime": 1741161386594, + "modified": false + }, + "l3_l_1_4.jpg": { + "type": "-", + "size": 12653, + "lmtime": 1741161386697, + "modified": false + }, + "l3_l_1_4.webp": { + "type": "-", + "size": 3334, + "lmtime": 1741161386813, + "modified": false + }, + "l3_l_1_5.jpg": { + "type": "-", + "size": 12875, + "lmtime": 1741161386904, + "modified": false + }, + "l3_l_1_5.webp": { + "type": "-", + "size": 3298, + "lmtime": 1741161386999, + "modified": false + }, + "l3_l_1_6.jpg": { + "type": "-", + "size": 9986, + "lmtime": 1741161387091, + "modified": false + }, + "l3_l_1_6.webp": { + "type": "-", + "size": 2246, + "lmtime": 1741161387190, + "modified": false + }, + "l3_l_1_7.jpg": { + "type": "-", + "size": 12038, + "lmtime": 1741161387282, + "modified": false + }, + "l3_l_1_7.webp": { + "type": "-", + "size": 3294, + "lmtime": 1741161387378, + "modified": false + }, + "l3_l_1_8.jpg": { + "type": "-", + "size": 9089, + "lmtime": 1741161387474, + "modified": false + }, + "l3_l_1_8.webp": { + "type": "-", + "size": 2962, + "lmtime": 1741161387568, + "modified": false + } + }, + "2": { + "l3_l_2_1.jpg": { + "type": "-", + "size": 12764, + "lmtime": 1741161387727, + "modified": false + }, + "l3_l_2_1.webp": { + "type": "-", + "size": 3130, + "lmtime": 1741161387745, + "modified": false + }, + "l3_l_2_2.jpg": { + "type": "-", + "size": 29242, + "lmtime": 1741161387842, + "modified": false + }, + "l3_l_2_2.webp": { + "type": "-", + "size": 13446, + "lmtime": 1741161387953, + "modified": false + }, + "l3_l_2_3.jpg": { + "type": "-", + "size": 14776, + "lmtime": 1741161388050, + "modified": false + }, + "l3_l_2_3.webp": { + "type": "-", + "size": 3582, + "lmtime": 1741161388158, + "modified": false + }, + "l3_l_2_4.jpg": { + "type": "-", + "size": 13560, + "lmtime": 1741161388261, + "modified": false + }, + "l3_l_2_4.webp": { + "type": "-", + "size": 3122, + "lmtime": 1741161388348, + "modified": false + }, + "l3_l_2_5.jpg": { + "type": "-", + "size": 32368, + "lmtime": 1741161388456, + "modified": false + }, + "l3_l_2_5.webp": { + "type": "-", + "size": 16748, + "lmtime": 1741161388563, + "modified": false + }, + "l3_l_2_6.jpg": { + "type": "-", + "size": 13130, + "lmtime": 1741161388656, + "modified": false + }, + "l3_l_2_6.webp": { + "type": "-", + "size": 4140, + "lmtime": 1741161388754, + "modified": false + }, + "l3_l_2_7.jpg": { + "type": "-", + "size": 16860, + "lmtime": 1741161388857, + "modified": false + }, + "l3_l_2_7.webp": { + "type": "-", + "size": 5002, + "lmtime": 1741161388965, + "modified": false + }, + "l3_l_2_8.jpg": { + "type": "-", + "size": 8038, + "lmtime": 1741161389068, + "modified": false + }, + "l3_l_2_8.webp": { + "type": "-", + "size": 2410, + "lmtime": 1741161389157, + "modified": false + } + }, + "3": { + "l3_l_3_1.jpg": { + "type": "-", + "size": 32717, + "lmtime": 1741161389271, + "modified": false + }, + "l3_l_3_1.webp": { + "type": "-", + "size": 15438, + "lmtime": 1741161389351, + "modified": false + }, + "l3_l_3_2.jpg": { + "type": "-", + "size": 16908, + "lmtime": 1741161389439, + "modified": false + }, + "l3_l_3_2.webp": { + "type": "-", + "size": 4878, + "lmtime": 1741161389543, + "modified": false + }, + "l3_l_3_3.jpg": { + "type": "-", + "size": 22227, + "lmtime": 1741161389657, + "modified": false + }, + "l3_l_3_3.webp": { + "type": "-", + "size": 8938, + "lmtime": 1741161389771, + "modified": false + }, + "l3_l_3_4.jpg": { + "type": "-", + "size": 18067, + "lmtime": 1741161389893, + "modified": false + }, + "l3_l_3_4.webp": { + "type": "-", + "size": 6570, + "lmtime": 1741161389993, + "modified": false + }, + "l3_l_3_5.jpg": { + "type": "-", + "size": 18576, + "lmtime": 1741161390112, + "modified": false + }, + "l3_l_3_5.webp": { + "type": "-", + "size": 8286, + "lmtime": 1741161390201, + "modified": false + }, + "l3_l_3_6.jpg": { + "type": "-", + "size": 18924, + "lmtime": 1741161390294, + "modified": false + }, + "l3_l_3_6.webp": { + "type": "-", + "size": 6962, + "lmtime": 1741161390369, + "modified": false + }, + "l3_l_3_7.jpg": { + "type": "-", + "size": 21932, + "lmtime": 1741161390477, + "modified": false + }, + "l3_l_3_7.webp": { + "type": "-", + "size": 8930, + "lmtime": 1741161390559, + "modified": false + }, + "l3_l_3_8.jpg": { + "type": "-", + "size": 12602, + "lmtime": 1741161390639, + "modified": false + }, + "l3_l_3_8.webp": { + "type": "-", + "size": 5764, + "lmtime": 1741161390733, + "modified": false + } + }, + "4": { + "l3_l_4_1.jpg": { + "type": "-", + "size": 48334, + "lmtime": 1741161390860, + "modified": false + }, + "l3_l_4_1.webp": { + "type": "-", + "size": 28108, + "lmtime": 1741161390984, + "modified": false + }, + "l3_l_4_2.jpg": { + "type": "-", + "size": 30759, + "lmtime": 1741161391094, + "modified": false + }, + "l3_l_4_2.webp": { + "type": "-", + "size": 14202, + "lmtime": 1741161391194, + "modified": false + }, + "l3_l_4_3.jpg": { + "type": "-", + "size": 51895, + "lmtime": 1741161391305, + "modified": false + }, + "l3_l_4_3.webp": { + "type": "-", + "size": 32454, + "lmtime": 1741161391430, + "modified": false + }, + "l3_l_4_4.jpg": { + "type": "-", + "size": 0, + "lmtime": 1741161391514, + "modified": false + }, + "l3_l_4_4.webp": { + "type": "-", + "size": 35946, + "lmtime": 1741161391636, + "modified": false + }, + "l3_l_4_5.jpg": { + "type": "-", + "size": 40368, + "lmtime": 1741161391745, + "modified": false + }, + "l3_l_4_5.webp": { + "type": "-", + "size": 22080, + "lmtime": 1741161391845, + "modified": false + }, + "l3_l_4_6.jpg": { + "type": "-", + "size": 44809, + "lmtime": 1741161391942, + "modified": false + }, + "l3_l_4_6.webp": { + "type": "-", + "size": 26358, + "lmtime": 1741161392043, + "modified": false + }, + "l3_l_4_7.jpg": { + "type": "-", + "size": 21969, + "lmtime": 1741161392151, + "modified": false + }, + "l3_l_4_7.webp": { + "type": "-", + "size": 8462, + "lmtime": 1741161392259, + "modified": false + }, + "l3_l_4_8.jpg": { + "type": "-", + "size": 10516, + "lmtime": 1741161392363, + "modified": false + }, + "l3_l_4_8.webp": { + "type": "-", + "size": 3304, + "lmtime": 1741161392463, + "modified": false + } + }, + "5": { + "l3_l_5_1.jpg": { + "type": "-", + "size": 31645, + "lmtime": 1741161392553, + "modified": false + }, + "l3_l_5_1.webp": { + "type": "-", + "size": 15322, + "lmtime": 1741161392631, + "modified": false + }, + "l3_l_5_2.jpg": { + "type": "-", + "size": 41623, + "lmtime": 1741161392728, + "modified": false + }, + "l3_l_5_2.webp": { + "type": "-", + "size": 24850, + "lmtime": 1741161392816, + "modified": false + }, + "l3_l_5_3.jpg": { + "type": "-", + "size": 49352, + "lmtime": 1741161392978, + "modified": false + }, + "l3_l_5_3.webp": { + "type": "-", + "size": 30742, + "lmtime": 1741161393079, + "modified": false + }, + "l3_l_5_4.jpg": { + "type": "-", + "size": 47310, + "lmtime": 1741161393177, + "modified": false + }, + "l3_l_5_4.webp": { + "type": "-", + "size": 27102, + "lmtime": 1741161393272, + "modified": false + }, + "l3_l_5_5.jpg": { + "type": "-", + "size": 44715, + "lmtime": 1741161393367, + "modified": false + }, + "l3_l_5_5.webp": { + "type": "-", + "size": 27204, + "lmtime": 1741161393470, + "modified": false + }, + "l3_l_5_6.jpg": { + "type": "-", + "size": 41005, + "lmtime": 1741161393581, + "modified": false + }, + "l3_l_5_6.webp": { + "type": "-", + "size": 22190, + "lmtime": 1741161393703, + "modified": false + }, + "l3_l_5_7.jpg": { + "type": "-", + "size": 40726, + "lmtime": 1741161393813, + "modified": false + }, + "l3_l_5_7.webp": { + "type": "-", + "size": 22618, + "lmtime": 1741161393909, + "modified": false + }, + "l3_l_5_8.jpg": { + "type": "-", + "size": 25257, + "lmtime": 1741161394000, + "modified": false + }, + "l3_l_5_8.webp": { + "type": "-", + "size": 15890, + "lmtime": 1741161394090, + "modified": false + } + }, + "6": { + "l3_l_6_1.jpg": { + "type": "-", + "size": 32004, + "lmtime": 1741161394192, + "modified": false + }, + "l3_l_6_1.webp": { + "type": "-", + "size": 16966, + "lmtime": 1741161394320, + "modified": false + }, + "l3_l_6_2.jpg": { + "type": "-", + "size": 35379, + "lmtime": 1741161394441, + "modified": false + }, + "l3_l_6_2.webp": { + "type": "-", + "size": 19556, + "lmtime": 1741161394554, + "modified": false + }, + "l3_l_6_3.jpg": { + "type": "-", + "size": 35157, + "lmtime": 1741161394669, + "modified": false + }, + "l3_l_6_3.webp": { + "type": "-", + "size": 16988, + "lmtime": 1741161394766, + "modified": false + }, + "l3_l_6_4.jpg": { + "type": "-", + "size": 24463, + "lmtime": 1741161394897, + "modified": false + }, + "l3_l_6_4.webp": { + "type": "-", + "size": 10478, + "lmtime": 1741161394999, + "modified": false + }, + "l3_l_6_5.jpg": { + "type": "-", + "size": 56342, + "lmtime": 1741161395107, + "modified": false + }, + "l3_l_6_5.webp": { + "type": "-", + "size": 41904, + "lmtime": 1741161395202, + "modified": false + }, + "l3_l_6_6.jpg": { + "type": "-", + "size": 65795, + "lmtime": 1741161395314, + "modified": false + }, + "l3_l_6_6.webp": { + "type": "-", + "size": 51164, + "lmtime": 1741161395431, + "modified": false + }, + "l3_l_6_7.jpg": { + "type": "-", + "size": 58703, + "lmtime": 1741161395550, + "modified": false + }, + "l3_l_6_7.webp": { + "type": "-", + "size": 40952, + "lmtime": 1741161395660, + "modified": false + }, + "l3_l_6_8.jpg": { + "type": "-", + "size": 17104, + "lmtime": 1741161395763, + "modified": false + }, + "l3_l_6_8.webp": { + "type": "-", + "size": 9258, + "lmtime": 1741161395849, + "modified": false + } + }, + "7": { + "l3_l_7_1.jpg": { + "type": "-", + "size": 28680, + "lmtime": 1741161395952, + "modified": false + }, + "l3_l_7_1.webp": { + "type": "-", + "size": 12638, + "lmtime": 1741161396038, + "modified": false + }, + "l3_l_7_2.jpg": { + "type": "-", + "size": 30721, + "lmtime": 1741161396140, + "modified": false + }, + "l3_l_7_2.webp": { + "type": "-", + "size": 13352, + "lmtime": 1741161396236, + "modified": false + }, + "l3_l_7_3.jpg": { + "type": "-", + "size": 34134, + "lmtime": 1741161396350, + "modified": false + }, + "l3_l_7_3.webp": { + "type": "-", + "size": 16956, + "lmtime": 1741161396454, + "modified": false + }, + "l3_l_7_4.jpg": { + "type": "-", + "size": 27164, + "lmtime": 1741161396545, + "modified": false + }, + "l3_l_7_4.webp": { + "type": "-", + "size": 13210, + "lmtime": 1741161396632, + "modified": false + }, + "l3_l_7_5.jpg": { + "type": "-", + "size": 66896, + "lmtime": 1741161396752, + "modified": false + }, + "l3_l_7_5.webp": { + "type": "-", + "size": 50844, + "lmtime": 1741161396862, + "modified": false + }, + "l3_l_7_6.jpg": { + "type": "-", + "size": 50799, + "lmtime": 1741161396984, + "modified": false + }, + "l3_l_7_6.webp": { + "type": "-", + "size": 34472, + "lmtime": 1741161397082, + "modified": false + }, + "l3_l_7_7.jpg": { + "type": "-", + "size": 67788, + "lmtime": 1741161397192, + "modified": false + }, + "l3_l_7_7.webp": { + "type": "-", + "size": 49602, + "lmtime": 1741161397289, + "modified": false + }, + "l3_l_7_8.jpg": { + "type": "-", + "size": 29643, + "lmtime": 1741161397378, + "modified": false + }, + "l3_l_7_8.webp": { + "type": "-", + "size": 19020, + "lmtime": 1741161397464, + "modified": false + } + }, + "8": { + "l3_l_8_1.jpg": { + "type": "-", + "size": 11889, + "lmtime": 1741161397538, + "modified": false + }, + "l3_l_8_1.webp": { + "type": "-", + "size": 5384, + "lmtime": 1741161397615, + "modified": false + }, + "l3_l_8_2.jpg": { + "type": "-", + "size": 17345, + "lmtime": 1741161397702, + "modified": false + }, + "l3_l_8_2.webp": { + "type": "-", + "size": 8222, + "lmtime": 1741161397773, + "modified": false + }, + "l3_l_8_3.jpg": { + "type": "-", + "size": 18090, + "lmtime": 1741161397867, + "modified": false + }, + "l3_l_8_3.webp": { + "type": "-", + "size": 10214, + "lmtime": 1741161397938, + "modified": false + }, + "l3_l_8_4.jpg": { + "type": "-", + "size": 16704, + "lmtime": 1741161398023, + "modified": false + }, + "l3_l_8_4.webp": { + "type": "-", + "size": 9896, + "lmtime": 1741161398105, + "modified": false + }, + "l3_l_8_5.jpg": { + "type": "-", + "size": 26929, + "lmtime": 1741161398193, + "modified": false + }, + "l3_l_8_5.webp": { + "type": "-", + "size": 18670, + "lmtime": 1741161398279, + "modified": false + }, + "l3_l_8_6.jpg": { + "type": "-", + "size": 22990, + "lmtime": 1741161398364, + "modified": false + }, + "l3_l_8_6.webp": { + "type": "-", + "size": 14226, + "lmtime": 1741161398436, + "modified": false + }, + "l3_l_8_7.jpg": { + "type": "-", + "size": 20452, + "lmtime": 1741161398523, + "modified": false + }, + "l3_l_8_7.webp": { + "type": "-", + "size": 11452, + "lmtime": 1741161398599, + "modified": false + }, + "l3_l_8_8.jpg": { + "type": "-", + "size": 12803, + "lmtime": 1741161398677, + "modified": false + }, + "l3_l_8_8.webp": { + "type": "-", + "size": 7628, + "lmtime": 1741161398743, + "modified": false + } + } + } + }, + "preview.jpg": { + "type": "-", + "size": 54549, + "lmtime": 1741161334684, + "modified": false + }, + "preview.webp": { + "type": "-", + "size": 28690, + "lmtime": 1741161334769, + "modified": false + }, + "r": { + "l1": { + "1": { + "l1_r_1_1.jpg": { + "type": "-", + "size": 32655, + "lmtime": 1741161398842, + "modified": false + }, + "l1_r_1_1.webp": { + "type": "-", + "size": 18746, + "lmtime": 1741161398926, + "modified": false + }, + "l1_r_1_2.jpg": { + "type": "-", + "size": 33333, + "lmtime": 1741161399021, + "modified": false + }, + "l1_r_1_2.webp": { + "type": "-", + "size": 18312, + "lmtime": 1741161399121, + "modified": false + } + }, + "2": { + "l1_r_2_1.jpg": { + "type": "-", + "size": 58186, + "lmtime": 1741161399222, + "modified": false + }, + "l1_r_2_1.webp": { + "type": "-", + "size": 42196, + "lmtime": 1741161399316, + "modified": false + }, + "l1_r_2_2.jpg": { + "type": "-", + "size": 49714, + "lmtime": 1741161399417, + "modified": false + }, + "l1_r_2_2.webp": { + "type": "-", + "size": 31124, + "lmtime": 1741161399503, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_r_1_1.jpg": { + "type": "-", + "size": 10087, + "lmtime": 1741161399583, + "modified": false + }, + "l2_r_1_1.webp": { + "type": "-", + "size": 4072, + "lmtime": 1741161399662, + "modified": false + }, + "l2_r_1_2.jpg": { + "type": "-", + "size": 10679, + "lmtime": 1741161399733, + "modified": false + }, + "l2_r_1_2.webp": { + "type": "-", + "size": 4284, + "lmtime": 1741161399803, + "modified": false + }, + "l2_r_1_3.jpg": { + "type": "-", + "size": 11840, + "lmtime": 1741161399884, + "modified": false + }, + "l2_r_1_3.webp": { + "type": "-", + "size": 4594, + "lmtime": 1741161399953, + "modified": false + }, + "l2_r_1_4.jpg": { + "type": "-", + "size": 4652, + "lmtime": 1741161400036, + "modified": false + }, + "l2_r_1_4.webp": { + "type": "-", + "size": 1636, + "lmtime": 1741161400119, + "modified": false + } + }, + "2": { + "l2_r_2_1.jpg": { + "type": "-", + "size": 52026, + "lmtime": 1741161400220, + "modified": false + }, + "l2_r_2_1.webp": { + "type": "-", + "size": 34612, + "lmtime": 1741161400333, + "modified": false + }, + "l2_r_2_2.jpg": { + "type": "-", + "size": 57877, + "lmtime": 1741161400460, + "modified": false + }, + "l2_r_2_2.webp": { + "type": "-", + "size": 42844, + "lmtime": 1741161400559, + "modified": false + }, + "l2_r_2_3.jpg": { + "type": "-", + "size": 46982, + "lmtime": 1741161400684, + "modified": false + }, + "l2_r_2_3.webp": { + "type": "-", + "size": 28266, + "lmtime": 1741161400781, + "modified": false + }, + "l2_r_2_4.jpg": { + "type": "-", + "size": 25389, + "lmtime": 1741161400865, + "modified": false + }, + "l2_r_2_4.webp": { + "type": "-", + "size": 16440, + "lmtime": 1741161400956, + "modified": false + } + }, + "3": { + "l2_r_3_1.jpg": { + "type": "-", + "size": 54736, + "lmtime": 1741161401067, + "modified": false + }, + "l2_r_3_1.webp": { + "type": "-", + "size": 38420, + "lmtime": 1741161401158, + "modified": false + }, + "l2_r_3_2.jpg": { + "type": "-", + "size": 59252, + "lmtime": 1741161401258, + "modified": false + }, + "l2_r_3_2.webp": { + "type": "-", + "size": 45060, + "lmtime": 1741161401363, + "modified": false + }, + "l2_r_3_3.jpg": { + "type": "-", + "size": 46615, + "lmtime": 1741161401469, + "modified": false + }, + "l2_r_3_3.webp": { + "type": "-", + "size": 28446, + "lmtime": 1741161401557, + "modified": false + }, + "l2_r_3_4.jpg": { + "type": "-", + "size": 18630, + "lmtime": 1741161401642, + "modified": false + }, + "l2_r_3_4.webp": { + "type": "-", + "size": 11024, + "lmtime": 1741161401730, + "modified": false + } + }, + "4": { + "l2_r_4_1.jpg": { + "type": "-", + "size": 22325, + "lmtime": 1741161401820, + "modified": false + }, + "l2_r_4_1.webp": { + "type": "-", + "size": 14828, + "lmtime": 1741161401898, + "modified": false + }, + "l2_r_4_2.jpg": { + "type": "-", + "size": 27837, + "lmtime": 1741161402003, + "modified": false + }, + "l2_r_4_2.webp": { + "type": "-", + "size": 22008, + "lmtime": 1741161402091, + "modified": false + }, + "l2_r_4_3.jpg": { + "type": "-", + "size": 24350, + "lmtime": 1741161402202, + "modified": false + }, + "l2_r_4_3.webp": { + "type": "-", + "size": 14220, + "lmtime": 1741161402277, + "modified": false + }, + "l2_r_4_4.jpg": { + "type": "-", + "size": 10792, + "lmtime": 1741161402350, + "modified": false + }, + "l2_r_4_4.webp": { + "type": "-", + "size": 6644, + "lmtime": 1741161402441, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_r_1_1.jpg": { + "type": "-", + "size": 12254, + "lmtime": 1741161402526, + "modified": false + }, + "l3_r_1_1.webp": { + "type": "-", + "size": 3226, + "lmtime": 1741161402598, + "modified": false + }, + "l3_r_1_2.jpg": { + "type": "-", + "size": 11339, + "lmtime": 1741161402676, + "modified": false + }, + "l3_r_1_2.webp": { + "type": "-", + "size": 2650, + "lmtime": 1741161402761, + "modified": false + }, + "l3_r_1_3.jpg": { + "type": "-", + "size": 10470, + "lmtime": 1741161402835, + "modified": false + }, + "l3_r_1_3.webp": { + "type": "-", + "size": 2216, + "lmtime": 1741161402922, + "modified": false + }, + "l3_r_1_4.jpg": { + "type": "-", + "size": 13881, + "lmtime": 1741161403017, + "modified": false + }, + "l3_r_1_4.webp": { + "type": "-", + "size": 4060, + "lmtime": 1741161403119, + "modified": false + }, + "l3_r_1_5.jpg": { + "type": "-", + "size": 16942, + "lmtime": 1741161403235, + "modified": false + }, + "l3_r_1_6.jpg": { + "type": "-", + "size": 17549, + "lmtime": 1741161403353, + "modified": false + }, + "l3_r_1_7.jpg": { + "type": "-", + "size": 12379, + "lmtime": 1741161403450, + "modified": false + }, + "l3_r_1_8.jpg": { + "type": "-", + "size": 6300, + "lmtime": 1741161403544, + "modified": false + } + }, + "2": { + "l3_r_2_1.jpg": { + "type": "-", + "size": 12059, + "lmtime": 1741161403644, + "modified": false + }, + "l3_r_2_1.webp": { + "type": "-", + "size": 3476, + "lmtime": 1741161403730, + "modified": false + }, + "l3_r_2_2.jpg": { + "type": "-", + "size": 10015, + "lmtime": 1741161403824, + "modified": false + }, + "l3_r_2_2.webp": { + "type": "-", + "size": 3114, + "lmtime": 1741161403920, + "modified": false + }, + "l3_r_2_3.jpg": { + "type": "-", + "size": 10426, + "lmtime": 1741161404020, + "modified": false + }, + "l3_r_2_3.webp": { + "type": "-", + "size": 3180, + "lmtime": 1741161404121, + "modified": false + }, + "l3_r_2_4.jpg": { + "type": "-", + "size": 12703, + "lmtime": 1741161404208, + "modified": false + }, + "l3_r_2_4.webp": { + "type": "-", + "size": 3870, + "lmtime": 1741161404319, + "modified": false + }, + "l3_r_2_5.jpg": { + "type": "-", + "size": 13903, + "lmtime": 1741161404409, + "modified": false + }, + "l3_r_2_5.webp": { + "type": "-", + "size": 4142, + "lmtime": 1741161404498, + "modified": false + }, + "l3_r_2_6.jpg": { + "type": "-", + "size": 15001, + "lmtime": 1741161404578, + "modified": false + }, + "l3_r_2_6.webp": { + "type": "-", + "size": 4458, + "lmtime": 1741161404664, + "modified": false + }, + "l3_r_2_7.jpg": { + "type": "-", + "size": 13508, + "lmtime": 1741161404740, + "modified": false + }, + "l3_r_2_7.webp": { + "type": "-", + "size": 4040, + "lmtime": 1741161404846, + "modified": false + }, + "l3_r_2_8.jpg": { + "type": "-", + "size": 5277, + "lmtime": 1741161404924, + "modified": false + }, + "l3_r_2_8.webp": { + "type": "-", + "size": 1436, + "lmtime": 1741161405024, + "modified": false + } + }, + "3": { + "l3_r_3_1.jpg": { + "type": "-", + "size": 30428, + "lmtime": 1741161405125, + "modified": false + }, + "l3_r_3_1.webp": { + "type": "-", + "size": 12046, + "lmtime": 1741161405213, + "modified": false + }, + "l3_r_3_2.jpg": { + "type": "-", + "size": 34424, + "lmtime": 1741161405305, + "modified": false + }, + "l3_r_3_2.webp": { + "type": "-", + "size": 16016, + "lmtime": 1741161405398, + "modified": false + }, + "l3_r_3_3.jpg": { + "type": "-", + "size": 30397, + "lmtime": 1741161405505, + "modified": false + }, + "l3_r_3_3.webp": { + "type": "-", + "size": 13728, + "lmtime": 1741161405593, + "modified": false + }, + "l3_r_3_4.jpg": { + "type": "-", + "size": 26822, + "lmtime": 1741161405714, + "modified": false + }, + "l3_r_3_4.webp": { + "type": "-", + "size": 12610, + "lmtime": 1741161405802, + "modified": false + }, + "l3_r_3_5.jpg": { + "type": "-", + "size": 30969, + "lmtime": 1741161405915, + "modified": false + }, + "l3_r_3_5.webp": { + "type": "-", + "size": 14528, + "lmtime": 1741161406017, + "modified": false + }, + "l3_r_3_6.jpg": { + "type": "-", + "size": 39680, + "lmtime": 1741161406121, + "modified": false + }, + "l3_r_3_6.webp": { + "type": "-", + "size": 21260, + "lmtime": 1741161406211, + "modified": false + }, + "l3_r_3_7.jpg": { + "type": "-", + "size": 40294, + "lmtime": 1741161406328, + "modified": false + }, + "l3_r_3_7.webp": { + "type": "-", + "size": 21606, + "lmtime": 1741161406435, + "modified": false + }, + "l3_r_3_8.jpg": { + "type": "-", + "size": 23711, + "lmtime": 1741161406540, + "modified": false + }, + "l3_r_3_8.webp": { + "type": "-", + "size": 14130, + "lmtime": 1741161406623, + "modified": false + } + }, + "4": { + "l3_r_4_1.jpg": { + "type": "-", + "size": 41302, + "lmtime": 1741161406728, + "modified": false + }, + "l3_r_4_1.webp": { + "type": "-", + "size": 21422, + "lmtime": 1741161406835, + "modified": false + }, + "l3_r_4_2.jpg": { + "type": "-", + "size": 55680, + "lmtime": 1741161406961, + "modified": false + }, + "l3_r_4_2.webp": { + "type": "-", + "size": 39362, + "lmtime": 1741161407077, + "modified": false + }, + "l3_r_4_3.jpg": { + "type": "-", + "size": 72731, + "lmtime": 1741161407200, + "modified": false + }, + "l3_r_4_3.webp": { + "type": "-", + "size": 62636, + "lmtime": 1741161407327, + "modified": false + }, + "l3_r_4_4.jpg": { + "type": "-", + "size": 46277, + "lmtime": 1741161407447, + "modified": false + }, + "l3_r_4_4.webp": { + "type": "-", + "size": 31848, + "lmtime": 1741161407564, + "modified": false + }, + "l3_r_4_5.jpg": { + "type": "-", + "size": 30581, + "lmtime": 1741161407669, + "modified": false + }, + "l3_r_4_5.webp": { + "type": "-", + "size": 14890, + "lmtime": 1741161407764, + "modified": false + }, + "l3_r_4_6.jpg": { + "type": "-", + "size": 37855, + "lmtime": 1741161407881, + "modified": false + }, + "l3_r_4_6.webp": { + "type": "-", + "size": 22006, + "lmtime": 1741161407978, + "modified": false + }, + "l3_r_4_7.jpg": { + "type": "-", + "size": 22757, + "lmtime": 1741161408083, + "modified": false + }, + "l3_r_4_7.webp": { + "type": "-", + "size": 10228, + "lmtime": 1741161408207, + "modified": false + }, + "l3_r_4_8.jpg": { + "type": "-", + "size": 21911, + "lmtime": 1741161408332, + "modified": false + }, + "l3_r_4_8.webp": { + "type": "-", + "size": 13170, + "lmtime": 1741161408429, + "modified": false + } + }, + "5": { + "l3_r_5_1.jpg": { + "type": "-", + "size": 35453, + "lmtime": 1741161408551, + "modified": false + }, + "l3_r_5_1.webp": { + "type": "-", + "size": 16690, + "lmtime": 1741161408651, + "modified": false + }, + "l3_r_5_2.jpg": { + "type": "-", + "size": 42960, + "lmtime": 1741161408780, + "modified": false + }, + "l3_r_5_2.webp": { + "type": "-", + "size": 26244, + "lmtime": 1741161408871, + "modified": false + }, + "l3_r_5_3.jpg": { + "type": "-", + "size": 55349, + "lmtime": 1741161408976, + "modified": false + }, + "l3_r_5_3.webp": { + "type": "-", + "size": 41728, + "lmtime": 1741161409101, + "modified": false + }, + "l3_r_5_4.jpg": { + "type": "-", + "size": 61765, + "lmtime": 1741161409228, + "modified": false + }, + "l3_r_5_4.webp": { + "type": "-", + "size": 48672, + "lmtime": 1741161409341, + "modified": false + }, + "l3_r_5_5.jpg": { + "type": "-", + "size": 43789, + "lmtime": 1741161409440, + "modified": false + }, + "l3_r_5_5.webp": { + "type": "-", + "size": 25542, + "lmtime": 1741161409525, + "modified": false + }, + "l3_r_5_6.jpg": { + "type": "-", + "size": 31579, + "lmtime": 1741161409620, + "modified": false + }, + "l3_r_5_6.webp": { + "type": "-", + "size": 16280, + "lmtime": 1741161409704, + "modified": false + }, + "l3_r_5_7.jpg": { + "type": "-", + "size": 18466, + "lmtime": 1741161409788, + "modified": false + }, + "l3_r_5_7.webp": { + "type": "-", + "size": 6016, + "lmtime": 1741161409862, + "modified": false + }, + "l3_r_5_8.jpg": { + "type": "-", + "size": 14727, + "lmtime": 1741161409956, + "modified": false + }, + "l3_r_5_8.webp": { + "type": "-", + "size": 6668, + "lmtime": 1741161410068, + "modified": false + } + }, + "6": { + "l3_r_6_1.jpg": { + "type": "-", + "size": 48642, + "lmtime": 1741161410173, + "modified": false + }, + "l3_r_6_1.webp": { + "type": "-", + "size": 28334, + "lmtime": 1741161410271, + "modified": false + }, + "l3_r_6_2.jpg": { + "type": "-", + "size": 57716, + "lmtime": 1741161410377, + "modified": false + }, + "l3_r_6_2.webp": { + "type": "-", + "size": 40944, + "lmtime": 1741161410480, + "modified": false + }, + "l3_r_6_3.jpg": { + "type": "-", + "size": 61404, + "lmtime": 1741161410587, + "modified": false + }, + "l3_r_6_3.webp": { + "type": "-", + "size": 46330, + "lmtime": 1741161410683, + "modified": false + }, + "l3_r_6_4.jpg": { + "type": "-", + "size": 61684, + "lmtime": 1741161410793, + "modified": false + }, + "l3_r_6_4.webp": { + "type": "-", + "size": 44520, + "lmtime": 1741161410887, + "modified": false + }, + "l3_r_6_5.jpg": { + "type": "-", + "size": 54930, + "lmtime": 1741161410988, + "modified": false + }, + "l3_r_6_5.webp": { + "type": "-", + "size": 34464, + "lmtime": 1741161411087, + "modified": false + }, + "l3_r_6_6.jpg": { + "type": "-", + "size": 42738, + "lmtime": 1741161411178, + "modified": false + }, + "l3_r_6_6.webp": { + "type": "-", + "size": 24000, + "lmtime": 1741161411264, + "modified": false + }, + "l3_r_6_7.jpg": { + "type": "-", + "size": 27376, + "lmtime": 1741161411361, + "modified": false + }, + "l3_r_6_7.webp": { + "type": "-", + "size": 12922, + "lmtime": 1741161411435, + "modified": false + }, + "l3_r_6_8.jpg": { + "type": "-", + "size": 18087, + "lmtime": 1741161411520, + "modified": false + }, + "l3_r_6_8.webp": { + "type": "-", + "size": 9980, + "lmtime": 1741161411606, + "modified": false + } + }, + "7": { + "l3_r_7_1.jpg": { + "type": "-", + "size": 37633, + "lmtime": 1741161411705, + "modified": false + }, + "l3_r_7_1.webp": { + "type": "-", + "size": 21862, + "lmtime": 1741161411794, + "modified": false + }, + "l3_r_7_2.jpg": { + "type": "-", + "size": 38676, + "lmtime": 1741161411888, + "modified": false + }, + "l3_r_7_2.webp": { + "type": "-", + "size": 22408, + "lmtime": 1741161411978, + "modified": false + }, + "l3_r_7_3.jpg": { + "type": "-", + "size": 48059, + "lmtime": 1741161412077, + "modified": false + }, + "l3_r_7_3.webp": { + "type": "-", + "size": 33686, + "lmtime": 1741161412167, + "modified": false + }, + "l3_r_7_4.jpg": { + "type": "-", + "size": 44802, + "lmtime": 1741161412270, + "modified": false + }, + "l3_r_7_4.webp": { + "type": "-", + "size": 29042, + "lmtime": 1741161412365, + "modified": false + }, + "l3_r_7_5.jpg": { + "type": "-", + "size": 26217, + "lmtime": 1741161412457, + "modified": false + }, + "l3_r_7_5.webp": { + "type": "-", + "size": 11176, + "lmtime": 1741161412529, + "modified": false + }, + "l3_r_7_6.jpg": { + "type": "-", + "size": 46523, + "lmtime": 1741161412645, + "modified": false + }, + "l3_r_7_6.webp": { + "type": "-", + "size": 23722, + "lmtime": 1741161412750, + "modified": false + }, + "l3_r_7_7.jpg": { + "type": "-", + "size": 35478, + "lmtime": 1741161412857, + "modified": false + }, + "l3_r_7_7.webp": { + "type": "-", + "size": 19716, + "lmtime": 1741161412947, + "modified": false + }, + "l3_r_7_8.jpg": { + "type": "-", + "size": 17821, + "lmtime": 1741161413053, + "modified": false + }, + "l3_r_7_8.webp": { + "type": "-", + "size": 9666, + "lmtime": 1741161413125, + "modified": false + } + }, + "8": { + "l3_r_8_1.jpg": { + "type": "-", + "size": 15349, + "lmtime": 1741161413195, + "modified": false + }, + "l3_r_8_1.webp": { + "type": "-", + "size": 8382, + "lmtime": 1741161413276, + "modified": false + }, + "l3_r_8_2.jpg": { + "type": "-", + "size": 19036, + "lmtime": 1741161413370, + "modified": false + }, + "l3_r_8_2.webp": { + "type": "-", + "size": 11376, + "lmtime": 1741161413443, + "modified": false + }, + "l3_r_8_3.jpg": { + "type": "-", + "size": 22187, + "lmtime": 1741161413537, + "modified": false + }, + "l3_r_8_3.webp": { + "type": "-", + "size": 14926, + "lmtime": 1741161413615, + "modified": false + }, + "l3_r_8_4.jpg": { + "type": "-", + "size": 25252, + "lmtime": 1741161413703, + "modified": false + }, + "l3_r_8_4.webp": { + "type": "-", + "size": 16718, + "lmtime": 1741161413797, + "modified": false + }, + "l3_r_8_5.jpg": { + "type": "-", + "size": 17060, + "lmtime": 1741161413881, + "modified": false + }, + "l3_r_8_5.webp": { + "type": "-", + "size": 9336, + "lmtime": 1741161413952, + "modified": false + }, + "l3_r_8_6.jpg": { + "type": "-", + "size": 22286, + "lmtime": 1741161414049, + "modified": false + }, + "l3_r_8_6.webp": { + "type": "-", + "size": 11880, + "lmtime": 1741161414130, + "modified": false + }, + "l3_r_8_7.jpg": { + "type": "-", + "size": 20434, + "lmtime": 1741161414227, + "modified": false + }, + "l3_r_8_7.webp": { + "type": "-", + "size": 10288, + "lmtime": 1741161414306, + "modified": false + }, + "l3_r_8_8.jpg": { + "type": "-", + "size": 7681, + "lmtime": 1741161414404, + "modified": false + }, + "l3_r_8_8.webp": { + "type": "-", + "size": 3770, + "lmtime": 1741161414505, + "modified": false + } + } + } + }, + "thumb.jpg": { + "type": "-", + "size": 13219, + "lmtime": 1741161334845, + "modified": false + }, + "thumb.webp": { + "type": "-", + "size": 8710, + "lmtime": 1741161334920, + "modified": false + }, + "u": { + "l1": { + "1": { + "l1_u_1_1.jpg": { + "type": "-", + "size": 11580, + "lmtime": 1741161414584, + "modified": false + }, + "l1_u_1_1.webp": { + "type": "-", + "size": 6410, + "lmtime": 1741161414663, + "modified": false + }, + "l1_u_1_2.jpg": { + "type": "-", + "size": 16351, + "lmtime": 1741161414765, + "modified": false + }, + "l1_u_1_2.webp": { + "type": "-", + "size": 8348, + "lmtime": 1741161414836, + "modified": false + } + }, + "2": { + "l1_u_2_1.jpg": { + "type": "-", + "size": 17583, + "lmtime": 1741161414947, + "modified": false + }, + "l1_u_2_1.webp": { + "type": "-", + "size": 10006, + "lmtime": 1741161415061, + "modified": false + }, + "l1_u_2_2.jpg": { + "type": "-", + "size": 16794, + "lmtime": 1741161415180, + "modified": false + }, + "l1_u_2_2.webp": { + "type": "-", + "size": 9096, + "lmtime": 1741161415251, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_u_1_1.jpg": { + "type": "-", + "size": 20974, + "lmtime": 1741161415348, + "modified": false + }, + "l2_u_1_1.webp": { + "type": "-", + "size": 10920, + "lmtime": 1741161415424, + "modified": false + }, + "l2_u_1_2.jpg": { + "type": "-", + "size": 6671, + "lmtime": 1741161415525, + "modified": false + }, + "l2_u_1_2.webp": { + "type": "-", + "size": 3020, + "lmtime": 1741161415626, + "modified": false + }, + "l2_u_1_3.jpg": { + "type": "-", + "size": 17803, + "lmtime": 1741161415727, + "modified": false + }, + "l2_u_1_3.webp": { + "type": "-", + "size": 9424, + "lmtime": 1741161415799, + "modified": false + }, + "l2_u_1_4.jpg": { + "type": "-", + "size": 16871, + "lmtime": 1741161415886, + "modified": false + }, + "l2_u_1_4.webp": { + "type": "-", + "size": 9916, + "lmtime": 1741161415957, + "modified": false + } + }, + "2": { + "l2_u_2_1.jpg": { + "type": "-", + "size": 3802, + "lmtime": 1741161416026, + "modified": false + }, + "l2_u_2_1.webp": { + "type": "-", + "size": 1318, + "lmtime": 1741161416110, + "modified": false + }, + "l2_u_2_2.jpg": { + "type": "-", + "size": 3000, + "lmtime": 1741161416173, + "modified": false + }, + "l2_u_2_2.webp": { + "type": "-", + "size": 1066, + "lmtime": 1741161416247, + "modified": false + }, + "l2_u_2_3.jpg": { + "type": "-", + "size": 6675, + "lmtime": 1741161416324, + "modified": false + }, + "l2_u_2_3.webp": { + "type": "-", + "size": 2420, + "lmtime": 1741161416393, + "modified": false + }, + "l2_u_2_4.jpg": { + "type": "-", + "size": 5906, + "lmtime": 1741161416467, + "modified": false + }, + "l2_u_2_4.webp": { + "type": "-", + "size": 1704, + "lmtime": 1741161416545, + "modified": false + } + }, + "3": { + "l2_u_3_1.jpg": { + "type": "-", + "size": 38142, + "lmtime": 1741161416634, + "modified": false + }, + "l2_u_3_1.webp": { + "type": "-", + "size": 22906, + "lmtime": 1741161416731, + "modified": false + }, + "l2_u_3_2.jpg": { + "type": "-", + "size": 3361, + "lmtime": 1741161416801, + "modified": false + }, + "l2_u_3_2.webp": { + "type": "-", + "size": 1226, + "lmtime": 1741161416874, + "modified": false + }, + "l2_u_3_3.jpg": { + "type": "-", + "size": 23062, + "lmtime": 1741161416962, + "modified": false + }, + "l2_u_3_3.webp": { + "type": "-", + "size": 11652, + "lmtime": 1741161417040, + "modified": false + }, + "l2_u_3_4.jpg": { + "type": "-", + "size": 9953, + "lmtime": 1741161417110, + "modified": false + }, + "l2_u_3_4.webp": { + "type": "-", + "size": 5068, + "lmtime": 1741161417181, + "modified": false + } + }, + "4": { + "l2_u_4_1.jpg": { + "type": "-", + "size": 2592, + "lmtime": 1741161417252, + "modified": false + }, + "l2_u_4_1.webp": { + "type": "-", + "size": 928, + "lmtime": 1741161417333, + "modified": false + }, + "l2_u_4_2.jpg": { + "type": "-", + "size": 2689, + "lmtime": 1741161417411, + "modified": false + }, + "l2_u_4_2.webp": { + "type": "-", + "size": 910, + "lmtime": 1741161417483, + "modified": false + }, + "l2_u_4_3.jpg": { + "type": "-", + "size": 6052, + "lmtime": 1741161417567, + "modified": false + }, + "l2_u_4_3.webp": { + "type": "-", + "size": 2924, + "lmtime": 1741161417632, + "modified": false + }, + "l2_u_4_4.jpg": { + "type": "-", + "size": 3219, + "lmtime": 1741161417705, + "modified": false + }, + "l2_u_4_4.webp": { + "type": "-", + "size": 1384, + "lmtime": 1741161417784, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_u_1_1.jpg": { + "type": "-", + "size": 20792, + "lmtime": 1741161417889, + "modified": false + }, + "l3_u_1_1.webp": { + "type": "-", + "size": 7162, + "lmtime": 1741161417961, + "modified": false + }, + "l3_u_1_2.jpg": { + "type": "-", + "size": 42677, + "lmtime": 1741161418057, + "modified": false + }, + "l3_u_1_3.jpg": { + "type": "-", + "size": 22941, + "lmtime": 1741161418143, + "modified": false + }, + "l3_u_1_4.jpg": { + "type": "-", + "size": 10560, + "lmtime": 1741161418216, + "modified": false + }, + "l3_u_1_5.jpg": { + "type": "-", + "size": 10180, + "lmtime": 1741161418299, + "modified": false + }, + "l3_u_1_6.jpg": { + "type": "-", + "size": 26386, + "lmtime": 1741161418384, + "modified": false + }, + "l3_u_1_7.jpg": { + "type": "-", + "size": 55419, + "lmtime": 1741161418488, + "modified": false + }, + "l3_u_1_8.jpg": { + "type": "-", + "size": 15522, + "lmtime": 1741161418572, + "modified": false + } + }, + "2": { + "l3_u_2_1.jpg": { + "type": "-", + "size": 11484, + "lmtime": 1741161418653, + "modified": false + }, + "l3_u_2_2.jpg": { + "type": "-", + "size": 10288, + "lmtime": 1741161418727, + "modified": false + }, + "l3_u_2_3.jpg": { + "type": "-", + "size": 8744, + "lmtime": 1741161418806, + "modified": false + }, + "l3_u_2_4.jpg": { + "type": "-", + "size": 8376, + "lmtime": 1741161418887, + "modified": false + }, + "l3_u_2_5.jpg": { + "type": "-", + "size": 8528, + "lmtime": 1741161418957, + "modified": false + }, + "l3_u_2_6.jpg": { + "type": "-", + "size": 11292, + "lmtime": 1741161419042, + "modified": false + }, + "l3_u_2_7.jpg": { + "type": "-", + "size": 17749, + "lmtime": 1741161419128, + "modified": false + }, + "l3_u_2_8.jpg": { + "type": "-", + "size": 8894, + "lmtime": 1741161419205, + "modified": false + } + }, + "3": { + "l3_u_3_1.jpg": { + "type": "-", + "size": 10572, + "lmtime": 1741161419275, + "modified": false + }, + "l3_u_3_2.jpg": { + "type": "-", + "size": 9523, + "lmtime": 1741161419351, + "modified": false + }, + "l3_u_3_3.jpg": { + "type": "-", + "size": 8040, + "lmtime": 1741161419427, + "modified": false + }, + "l3_u_3_4.jpg": { + "type": "-", + "size": 7025, + "lmtime": 1741161419521, + "modified": false + }, + "l3_u_3_5.jpg": { + "type": "-", + "size": 7295, + "lmtime": 1741161419592, + "modified": false + }, + "l3_u_3_6.jpg": { + "type": "-", + "size": 10024, + "lmtime": 1741161419674, + "modified": false + }, + "l3_u_3_7.jpg": { + "type": "-", + "size": 13336, + "lmtime": 1741161419738, + "modified": false + }, + "l3_u_3_8.jpg": { + "type": "-", + "size": 9381, + "lmtime": 1741161419813, + "modified": false + } + }, + "4": { + "l3_u_4_1.jpg": { + "type": "-", + "size": 11171, + "lmtime": 1741161419891, + "modified": false + }, + "l3_u_4_2.jpg": { + "type": "-", + "size": 9246, + "lmtime": 1741161419965, + "modified": false + }, + "l3_u_4_3.jpg": { + "type": "-", + "size": 7123, + "lmtime": 1741161420037, + "modified": false + }, + "l3_u_4_4.jpg": { + "type": "-", + "size": 5681, + "lmtime": 1741161420109, + "modified": false + }, + "l3_u_4_5.jpg": { + "type": "-", + "size": 5920, + "lmtime": 1741161420183, + "modified": false + }, + "l3_u_4_6.jpg": { + "type": "-", + "size": 9419, + "lmtime": 1741161420272, + "modified": false + }, + "l3_u_4_7.jpg": { + "type": "-", + "size": 13320, + "lmtime": 1741161420388, + "modified": false + }, + "l3_u_4_8.jpg": { + "type": "-", + "size": 8362, + "lmtime": 1741161420474, + "modified": false + } + }, + "5": { + "l3_u_5_1.jpg": { + "type": "-", + "size": 17474, + "lmtime": 1741161420564, + "modified": false + }, + "l3_u_5_1.webp": { + "type": "-", + "size": 5768, + "lmtime": 1741161420658, + "modified": false + }, + "l3_u_5_2.jpg": { + "type": "-", + "size": 18715, + "lmtime": 1741161420765, + "modified": false + }, + "l3_u_5_2.webp": { + "type": "-", + "size": 7466, + "lmtime": 1741161420853, + "modified": false + }, + "l3_u_5_3.jpg": { + "type": "-", + "size": 8153, + "lmtime": 1741161420942, + "modified": false + }, + "l3_u_5_4.jpg": { + "type": "-", + "size": 6340, + "lmtime": 1741161421027, + "modified": false + }, + "l3_u_5_5.jpg": { + "type": "-", + "size": 6531, + "lmtime": 1741161421109, + "modified": false + }, + "l3_u_5_6.jpg": { + "type": "-", + "size": 7986, + "lmtime": 1741161421181, + "modified": false + }, + "l3_u_5_7.jpg": { + "type": "-", + "size": 9281, + "lmtime": 1741161421260, + "modified": false + }, + "l3_u_5_8.jpg": { + "type": "-", + "size": 5967, + "lmtime": 1741161421335, + "modified": false + } + }, + "6": { + "l3_u_6_1.jpg": { + "type": "-", + "size": 35130, + "lmtime": 1741161421433, + "modified": false + }, + "l3_u_6_1.webp": { + "type": "-", + "size": 17762, + "lmtime": 1741161421543, + "modified": false + }, + "l3_u_6_2.jpg": { + "type": "-", + "size": 50291, + "lmtime": 1741161421649, + "modified": false + }, + "l3_u_6_3.jpg": { + "type": "-", + "size": 11871, + "lmtime": 1741161421741, + "modified": false + }, + "l3_u_6_4.jpg": { + "type": "-", + "size": 8851, + "lmtime": 1741161421839, + "modified": false + }, + "l3_u_6_5.jpg": { + "type": "-", + "size": 8325, + "lmtime": 1741161421919, + "modified": false + }, + "l3_u_6_6.jpg": { + "type": "-", + "size": 34855, + "lmtime": 1741161422023, + "modified": false + }, + "l3_u_6_7.jpg": { + "type": "-", + "size": 41899, + "lmtime": 1741161422122, + "modified": false + }, + "l3_u_6_8.jpg": { + "type": "-", + "size": 5607, + "lmtime": 1741161422211, + "modified": false + } + }, + "7": { + "l3_u_7_1.jpg": { + "type": "-", + "size": 20178, + "lmtime": 1741161422314, + "modified": false + }, + "l3_u_7_1.webp": { + "type": "-", + "size": 8238, + "lmtime": 1741161422407, + "modified": false + }, + "l3_u_7_2.jpg": { + "type": "-", + "size": 26486, + "lmtime": 1741161422516, + "modified": false + }, + "l3_u_7_3.jpg": { + "type": "-", + "size": 12618, + "lmtime": 1741161422609, + "modified": false + }, + "l3_u_7_4.jpg": { + "type": "-", + "size": 11127, + "lmtime": 1741161422695, + "modified": false + }, + "l3_u_7_5.jpg": { + "type": "-", + "size": 9990, + "lmtime": 1741161422783, + "modified": false + }, + "l3_u_7_6.jpg": { + "type": "-", + "size": 29239, + "lmtime": 1741161422865, + "modified": false + }, + "l3_u_7_7.jpg": { + "type": "-", + "size": 34010, + "lmtime": 1741161422962, + "modified": false + }, + "l3_u_7_8.jpg": { + "type": "-", + "size": 5545, + "lmtime": 1741161423035, + "modified": false + }, + "l3_u_7_8.webp": { + "type": "-", + "size": 1092, + "lmtime": 1741161423105, + "modified": false + } + }, + "8": { + "l3_u_8_1.jpg": { + "type": "-", + "size": 4385, + "lmtime": 1741161423201, + "modified": false + }, + "l3_u_8_1.webp": { + "type": "-", + "size": 778, + "lmtime": 1741161423296, + "modified": false + }, + "l3_u_8_2.jpg": { + "type": "-", + "size": 5870, + "lmtime": 1741161423378, + "modified": false + }, + "l3_u_8_3.jpg": { + "type": "-", + "size": 6426, + "lmtime": 1741161423481, + "modified": false + }, + "l3_u_8_4.jpg": { + "type": "-", + "size": 5851, + "lmtime": 1741161423584, + "modified": false + }, + "l3_u_8_5.jpg": { + "type": "-", + "size": 5102, + "lmtime": 1741161423660, + "modified": false + }, + "l3_u_8_6.jpg": { + "type": "-", + "size": 5265, + "lmtime": 1741161423761, + "modified": false + }, + "l3_u_8_7.jpg": { + "type": "-", + "size": 5631, + "lmtime": 1741161423868, + "modified": false + }, + "l3_u_8_8.jpg": { + "type": "-", + "size": 3030, + "lmtime": 1741161423966, + "modified": false + }, + "l3_u_8_8.webp": { + "type": "-", + "size": 640, + "lmtime": 1741161424055, + "modified": false + } + } + } + } + }, + "panorama_4.tiles": { + "b": { + "l1": { + "1": { + "l1_b_1_1.jpg": { + "type": "-", + "size": 17630, + "lmtime": 1741161424605, + "modified": false + }, + "l1_b_1_1.webp": { + "type": "-", + "size": 8772, + "lmtime": 1741161424702, + "modified": false + }, + "l1_b_1_2.jpg": { + "type": "-", + "size": 23011, + "lmtime": 1741161424809, + "modified": false + }, + "l1_b_1_2.webp": { + "type": "-", + "size": 12658, + "lmtime": 1741161424926, + "modified": false + } + }, + "2": { + "l1_b_2_1.jpg": { + "type": "-", + "size": 33412, + "lmtime": 1741161425041, + "modified": false + }, + "l1_b_2_1.webp": { + "type": "-", + "size": 18376, + "lmtime": 1741161425149, + "modified": false + }, + "l1_b_2_2.jpg": { + "type": "-", + "size": 34119, + "lmtime": 1741161425265, + "modified": false + }, + "l1_b_2_2.webp": { + "type": "-", + "size": 17874, + "lmtime": 1741161425359, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_b_1_1.jpg": { + "type": "-", + "size": 15994, + "lmtime": 1741161425449, + "modified": false + }, + "l2_b_1_1.webp": { + "type": "-", + "size": 8464, + "lmtime": 1741161425525, + "modified": false + }, + "l2_b_1_2.jpg": { + "type": "-", + "size": 8376, + "lmtime": 1741161425595, + "modified": false + }, + "l2_b_1_2.webp": { + "type": "-", + "size": 3822, + "lmtime": 1741161425685, + "modified": false + }, + "l2_b_1_3.jpg": { + "type": "-", + "size": 17350, + "lmtime": 1741161425791, + "modified": false + }, + "l2_b_1_3.webp": { + "type": "-", + "size": 10006, + "lmtime": 1741161425874, + "modified": false + }, + "l2_b_1_4.jpg": { + "type": "-", + "size": 6703, + "lmtime": 1741161425955, + "modified": false + }, + "l2_b_1_4.webp": { + "type": "-", + "size": 2454, + "lmtime": 1741161426024, + "modified": false + } + }, + "2": { + "l2_b_2_1.jpg": { + "type": "-", + "size": 26123, + "lmtime": 1741161426113, + "modified": false + }, + "l2_b_2_1.webp": { + "type": "-", + "size": 14440, + "lmtime": 1741161426210, + "modified": false + }, + "l2_b_2_2.jpg": { + "type": "-", + "size": 22016, + "lmtime": 1741161426331, + "modified": false + }, + "l2_b_2_2.webp": { + "type": "-", + "size": 11116, + "lmtime": 1741161426420, + "modified": false + }, + "l2_b_2_3.jpg": { + "type": "-", + "size": 25241, + "lmtime": 1741161426524, + "modified": false + }, + "l2_b_2_3.webp": { + "type": "-", + "size": 12328, + "lmtime": 1741161426620, + "modified": false + }, + "l2_b_2_4.jpg": { + "type": "-", + "size": 5403, + "lmtime": 1741161426722, + "modified": false + }, + "l2_b_2_4.webp": { + "type": "-", + "size": 2052, + "lmtime": 1741161426818, + "modified": false + } + }, + "3": { + "l2_b_3_1.jpg": { + "type": "-", + "size": 32354, + "lmtime": 1741161426942, + "modified": false + }, + "l2_b_3_1.webp": { + "type": "-", + "size": 17960, + "lmtime": 1741161427036, + "modified": false + }, + "l2_b_3_2.jpg": { + "type": "-", + "size": 23340, + "lmtime": 1741161427139, + "modified": false + }, + "l2_b_3_2.webp": { + "type": "-", + "size": 11026, + "lmtime": 1741161427237, + "modified": false + }, + "l2_b_3_3.jpg": { + "type": "-", + "size": 34935, + "lmtime": 1741161427349, + "modified": false + }, + "l2_b_3_3.webp": { + "type": "-", + "size": 19288, + "lmtime": 1741161427443, + "modified": false + }, + "l2_b_3_4.jpg": { + "type": "-", + "size": 17369, + "lmtime": 1741161427528, + "modified": false + }, + "l2_b_3_4.webp": { + "type": "-", + "size": 9220, + "lmtime": 1741161427600, + "modified": false + } + }, + "4": { + "l2_b_4_1.jpg": { + "type": "-", + "size": 12660, + "lmtime": 1741161427681, + "modified": false + }, + "l2_b_4_1.webp": { + "type": "-", + "size": 6096, + "lmtime": 1741161427773, + "modified": false + }, + "l2_b_4_2.jpg": { + "type": "-", + "size": 14706, + "lmtime": 1741161427862, + "modified": false + }, + "l2_b_4_2.webp": { + "type": "-", + "size": 7424, + "lmtime": 1741161427944, + "modified": false + }, + "l2_b_4_3.jpg": { + "type": "-", + "size": 13565, + "lmtime": 1741161428043, + "modified": false + }, + "l2_b_4_3.webp": { + "type": "-", + "size": 6540, + "lmtime": 1741161428127, + "modified": false + }, + "l2_b_4_4.jpg": { + "type": "-", + "size": 11024, + "lmtime": 1741161428214, + "modified": false + }, + "l2_b_4_4.webp": { + "type": "-", + "size": 6830, + "lmtime": 1741161428318, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_b_1_1.jpg": { + "type": "-", + "size": 21596, + "lmtime": 1741161428440, + "modified": false + }, + "l3_b_1_1.webp": { + "type": "-", + "size": 8418, + "lmtime": 1741161428526, + "modified": false + }, + "l3_b_1_2.jpg": { + "type": "-", + "size": 8425, + "lmtime": 1741161428591, + "modified": false + }, + "l3_b_1_2.webp": { + "type": "-", + "size": 1564, + "lmtime": 1741161428667, + "modified": false + }, + "l3_b_1_3.jpg": { + "type": "-", + "size": 7514, + "lmtime": 1741161428755, + "modified": false + }, + "l3_b_1_3.webp": { + "type": "-", + "size": 1496, + "lmtime": 1741161428830, + "modified": false + }, + "l3_b_1_4.jpg": { + "type": "-", + "size": 7908, + "lmtime": 1741161428909, + "modified": false + }, + "l3_b_1_4.webp": { + "type": "-", + "size": 1398, + "lmtime": 1741161428992, + "modified": false + }, + "l3_b_1_5.jpg": { + "type": "-", + "size": 15237, + "lmtime": 1741161429087, + "modified": false + }, + "l3_b_1_5.webp": { + "type": "-", + "size": 5582, + "lmtime": 1741161429180, + "modified": false + }, + "l3_b_1_6.jpg": { + "type": "-", + "size": 11559, + "lmtime": 1741161429276, + "modified": false + }, + "l3_b_1_6.webp": { + "type": "-", + "size": 2862, + "lmtime": 1741161429369, + "modified": false + }, + "l3_b_1_7.jpg": { + "type": "-", + "size": 9934, + "lmtime": 1741161429469, + "modified": false + }, + "l3_b_1_7.webp": { + "type": "-", + "size": 2302, + "lmtime": 1741161429564, + "modified": false + }, + "l3_b_1_8.jpg": { + "type": "-", + "size": 7769, + "lmtime": 1741161429654, + "modified": false + }, + "l3_b_1_8.webp": { + "type": "-", + "size": 2510, + "lmtime": 1741161429741, + "modified": false + } + }, + "2": { + "l3_b_2_1.jpg": { + "type": "-", + "size": 22662, + "lmtime": 1741161429856, + "modified": false + }, + "l3_b_2_1.webp": { + "type": "-", + "size": 9148, + "lmtime": 1741161429952, + "modified": false + }, + "l3_b_2_2.jpg": { + "type": "-", + "size": 13572, + "lmtime": 1741161430037, + "modified": false + }, + "l3_b_2_2.webp": { + "type": "-", + "size": 4148, + "lmtime": 1741161430131, + "modified": false + }, + "l3_b_2_3.jpg": { + "type": "-", + "size": 8792, + "lmtime": 1741161430228, + "modified": false + }, + "l3_b_2_3.webp": { + "type": "-", + "size": 2256, + "lmtime": 1741161430326, + "modified": false + }, + "l3_b_2_4.jpg": { + "type": "-", + "size": 8522, + "lmtime": 1741161430424, + "modified": false + }, + "l3_b_2_4.webp": { + "type": "-", + "size": 2126, + "lmtime": 1741161430518, + "modified": false + }, + "l3_b_2_5.jpg": { + "type": "-", + "size": 30672, + "lmtime": 1741161430619, + "modified": false + }, + "l3_b_2_5.webp": { + "type": "-", + "size": 16200, + "lmtime": 1741161430756, + "modified": false + }, + "l3_b_2_6.jpg": { + "type": "-", + "size": 13109, + "lmtime": 1741161430838, + "modified": false + }, + "l3_b_2_6.webp": { + "type": "-", + "size": 4686, + "lmtime": 1741161430940, + "modified": false + }, + "l3_b_2_7.jpg": { + "type": "-", + "size": 17388, + "lmtime": 1741161431057, + "modified": false + }, + "l3_b_2_7.webp": { + "type": "-", + "size": 5628, + "lmtime": 1741161431135, + "modified": false + }, + "l3_b_2_8.jpg": { + "type": "-", + "size": 8900, + "lmtime": 1741161431225, + "modified": false + }, + "l3_b_2_8.webp": { + "type": "-", + "size": 2734, + "lmtime": 1741161431316, + "modified": false + } + }, + "3": { + "l3_b_3_1.jpg": { + "type": "-", + "size": 11419, + "lmtime": 1741161431414, + "modified": false + }, + "l3_b_3_1.webp": { + "type": "-", + "size": 2916, + "lmtime": 1741161431501, + "modified": false + }, + "l3_b_3_2.jpg": { + "type": "-", + "size": 17860, + "lmtime": 1741161431631, + "modified": false + }, + "l3_b_3_2.webp": { + "type": "-", + "size": 7822, + "lmtime": 1741161431728, + "modified": false + }, + "l3_b_3_3.jpg": { + "type": "-", + "size": 15519, + "lmtime": 1741161431840, + "modified": false + }, + "l3_b_3_3.webp": { + "type": "-", + "size": 5552, + "lmtime": 1741161431945, + "modified": false + }, + "l3_b_3_4.jpg": { + "type": "-", + "size": 23714, + "lmtime": 1741161432043, + "modified": false + }, + "l3_b_3_4.webp": { + "type": "-", + "size": 10108, + "lmtime": 1741161432142, + "modified": false + }, + "l3_b_3_5.jpg": { + "type": "-", + "size": 20022, + "lmtime": 1741161432238, + "modified": false + }, + "l3_b_3_5.webp": { + "type": "-", + "size": 9256, + "lmtime": 1741161432332, + "modified": false + }, + "l3_b_3_6.jpg": { + "type": "-", + "size": 20380, + "lmtime": 1741161432424, + "modified": false + }, + "l3_b_3_6.webp": { + "type": "-", + "size": 7752, + "lmtime": 1741161432511, + "modified": false + }, + "l3_b_3_7.jpg": { + "type": "-", + "size": 16185, + "lmtime": 1741161432613, + "modified": false + }, + "l3_b_3_7.webp": { + "type": "-", + "size": 5350, + "lmtime": 1741161432707, + "modified": false + }, + "l3_b_3_8.jpg": { + "type": "-", + "size": 7411, + "lmtime": 1741161432808, + "modified": false + }, + "l3_b_3_8.webp": { + "type": "-", + "size": 2228, + "lmtime": 1741161432901, + "modified": false + } + }, + "4": { + "l3_b_4_1.jpg": { + "type": "-", + "size": 19797, + "lmtime": 1741161433021, + "modified": false + }, + "l3_b_4_1.webp": { + "type": "-", + "size": 7164, + "lmtime": 1741161433117, + "modified": false + }, + "l3_b_4_2.jpg": { + "type": "-", + "size": 22136, + "lmtime": 1741161433224, + "modified": false + }, + "l3_b_4_2.webp": { + "type": "-", + "size": 9460, + "lmtime": 1741161433320, + "modified": false + }, + "l3_b_4_3.jpg": { + "type": "-", + "size": 10962, + "lmtime": 1741161433423, + "modified": false + }, + "l3_b_4_3.webp": { + "type": "-", + "size": 3368, + "lmtime": 1741161433495, + "modified": false + }, + "l3_b_4_4.jpg": { + "type": "-", + "size": 18707, + "lmtime": 1741161433607, + "modified": false + }, + "l3_b_4_4.webp": { + "type": "-", + "size": 7512, + "lmtime": 1741161433716, + "modified": false + }, + "l3_b_4_5.jpg": { + "type": "-", + "size": 22462, + "lmtime": 1741161433830, + "modified": false + }, + "l3_b_4_5.webp": { + "type": "-", + "size": 9258, + "lmtime": 1741161433919, + "modified": false + }, + "l3_b_4_6.jpg": { + "type": "-", + "size": 26799, + "lmtime": 1741161434029, + "modified": false + }, + "l3_b_4_6.webp": { + "type": "-", + "size": 11856, + "lmtime": 1741161434128, + "modified": false + }, + "l3_b_4_7.jpg": { + "type": "-", + "size": 8653, + "lmtime": 1741161434207, + "modified": false + }, + "l3_b_4_7.webp": { + "type": "-", + "size": 2020, + "lmtime": 1741161434294, + "modified": false + }, + "l3_b_4_8.jpg": { + "type": "-", + "size": 6415, + "lmtime": 1741161434370, + "modified": false + }, + "l3_b_4_8.webp": { + "type": "-", + "size": 1464, + "lmtime": 1741161434443, + "modified": false + } + }, + "5": { + "l3_b_5_1.jpg": { + "type": "-", + "size": 22136, + "lmtime": 1741161434543, + "modified": false + }, + "l3_b_5_1.webp": { + "type": "-", + "size": 8980, + "lmtime": 1741161434631, + "modified": false + }, + "l3_b_5_2.jpg": { + "type": "-", + "size": 33385, + "lmtime": 1741161434741, + "modified": false + }, + "l3_b_5_2.webp": { + "type": "-", + "size": 17386, + "lmtime": 1741161434856, + "modified": false + }, + "l3_b_5_3.jpg": { + "type": "-", + "size": 16046, + "lmtime": 1741161434968, + "modified": false + }, + "l3_b_5_3.webp": { + "type": "-", + "size": 6158, + "lmtime": 1741161435070, + "modified": false + }, + "l3_b_5_4.jpg": { + "type": "-", + "size": 18811, + "lmtime": 1741161435181, + "modified": false + }, + "l3_b_5_4.webp": { + "type": "-", + "size": 7596, + "lmtime": 1741161435275, + "modified": false + }, + "l3_b_5_5.jpg": { + "type": "-", + "size": 42670, + "lmtime": 1741161435395, + "modified": false + }, + "l3_b_5_5.webp": { + "type": "-", + "size": 26720, + "lmtime": 1741161435505, + "modified": false + }, + "l3_b_5_6.jpg": { + "type": "-", + "size": 31734, + "lmtime": 1741161435631, + "modified": false + }, + "l3_b_5_6.webp": { + "type": "-", + "size": 16762, + "lmtime": 1741161435743, + "modified": false + }, + "l3_b_5_7.jpg": { + "type": "-", + "size": 26409, + "lmtime": 1741161435855, + "modified": false + }, + "l3_b_5_7.webp": { + "type": "-", + "size": 11670, + "lmtime": 1741161435939, + "modified": false + }, + "l3_b_5_8.jpg": { + "type": "-", + "size": 8630, + "lmtime": 1741161436031, + "modified": false + }, + "l3_b_5_8.webp": { + "type": "-", + "size": 2596, + "lmtime": 1741161436122, + "modified": false + } + }, + "6": { + "l3_b_6_1.jpg": { + "type": "-", + "size": 22841, + "lmtime": 1741161436223, + "modified": false + }, + "l3_b_6_1.webp": { + "type": "-", + "size": 9514, + "lmtime": 1741161436329, + "modified": false + }, + "l3_b_6_2.jpg": { + "type": "-", + "size": 32596, + "lmtime": 1741161436434, + "modified": false + }, + "l3_b_6_2.webp": { + "type": "-", + "size": 15916, + "lmtime": 1741161436532, + "modified": false + }, + "l3_b_6_3.jpg": { + "type": "-", + "size": 19653, + "lmtime": 1741161436639, + "modified": false + }, + "l3_b_6_3.webp": { + "type": "-", + "size": 7316, + "lmtime": 1741161436738, + "modified": false + }, + "l3_b_6_4.jpg": { + "type": "-", + "size": 18542, + "lmtime": 1741161436848, + "modified": false + }, + "l3_b_6_4.webp": { + "type": "-", + "size": 6740, + "lmtime": 1741161436948, + "modified": false + }, + "l3_b_6_5.jpg": { + "type": "-", + "size": 34761, + "lmtime": 1741161437055, + "modified": false + }, + "l3_b_6_5.webp": { + "type": "-", + "size": 19714, + "lmtime": 1741161437167, + "modified": false + }, + "l3_b_6_6.jpg": { + "type": "-", + "size": 30445, + "lmtime": 1741161437273, + "modified": false + }, + "l3_b_6_6.webp": { + "type": "-", + "size": 14040, + "lmtime": 1741161437372, + "modified": false + }, + "l3_b_6_7.jpg": { + "type": "-", + "size": 28799, + "lmtime": 1741161437491, + "modified": false + }, + "l3_b_6_7.webp": { + "type": "-", + "size": 14170, + "lmtime": 1741161437630, + "modified": false + }, + "l3_b_6_8.jpg": { + "type": "-", + "size": 15391, + "lmtime": 1741161437719, + "modified": false + }, + "l3_b_6_8.webp": { + "type": "-", + "size": 7168, + "lmtime": 1741161437798, + "modified": false + } + }, + "7": { + "l3_b_7_1.jpg": { + "type": "-", + "size": 16582, + "lmtime": 1741161437885, + "modified": false + }, + "l3_b_7_1.webp": { + "type": "-", + "size": 5518, + "lmtime": 1741161437973, + "modified": false + }, + "l3_b_7_2.jpg": { + "type": "-", + "size": 20230, + "lmtime": 1741161438083, + "modified": false + }, + "l3_b_7_2.webp": { + "type": "-", + "size": 7692, + "lmtime": 1741161438172, + "modified": false + }, + "l3_b_7_3.jpg": { + "type": "-", + "size": 32864, + "lmtime": 1741161438302, + "modified": false + }, + "l3_b_7_3.webp": { + "type": "-", + "size": 16612, + "lmtime": 1741161438405, + "modified": false + }, + "l3_b_7_4.jpg": { + "type": "-", + "size": 32344, + "lmtime": 1741161438519, + "modified": false + }, + "l3_b_7_4.webp": { + "type": "-", + "size": 18754, + "lmtime": 1741161438629, + "modified": false + }, + "l3_b_7_5.jpg": { + "type": "-", + "size": 29577, + "lmtime": 1741161438735, + "modified": false + }, + "l3_b_7_5.webp": { + "type": "-", + "size": 16580, + "lmtime": 1741161438857, + "modified": false + }, + "l3_b_7_6.jpg": { + "type": "-", + "size": 28008, + "lmtime": 1741161438984, + "modified": false + }, + "l3_b_7_6.webp": { + "type": "-", + "size": 13494, + "lmtime": 1741161439077, + "modified": false + }, + "l3_b_7_7.jpg": { + "type": "-", + "size": 31357, + "lmtime": 1741161439181, + "modified": false + }, + "l3_b_7_7.webp": { + "type": "-", + "size": 14956, + "lmtime": 1741161439279, + "modified": false + }, + "l3_b_7_8.jpg": { + "type": "-", + "size": 17007, + "lmtime": 1741161439401, + "modified": false + }, + "l3_b_7_8.webp": { + "type": "-", + "size": 9048, + "lmtime": 1741161439499, + "modified": false + } + }, + "8": { + "l3_b_8_1.jpg": { + "type": "-", + "size": 11347, + "lmtime": 1741161439594, + "modified": false + }, + "l3_b_8_1.webp": { + "type": "-", + "size": 4648, + "lmtime": 1741161439668, + "modified": false + }, + "l3_b_8_2.jpg": { + "type": "-", + "size": 11467, + "lmtime": 1741161439758, + "modified": false + }, + "l3_b_8_2.webp": { + "type": "-", + "size": 4700, + "lmtime": 1741161439846, + "modified": false + }, + "l3_b_8_3.jpg": { + "type": "-", + "size": 15191, + "lmtime": 1741161439917, + "modified": false + }, + "l3_b_8_3.webp": { + "type": "-", + "size": 7758, + "lmtime": 1741161440031, + "modified": false + }, + "l3_b_8_4.jpg": { + "type": "-", + "size": 17011, + "lmtime": 1741161440153, + "modified": false + }, + "l3_b_8_4.webp": { + "type": "-", + "size": 10258, + "lmtime": 1741161440259, + "modified": false + }, + "l3_b_8_5.jpg": { + "type": "-", + "size": 15831, + "lmtime": 1741161440356, + "modified": false + }, + "l3_b_8_5.webp": { + "type": "-", + "size": 9084, + "lmtime": 1741161440441, + "modified": false + }, + "l3_b_8_6.jpg": { + "type": "-", + "size": 15008, + "lmtime": 1741161440550, + "modified": false + }, + "l3_b_8_6.webp": { + "type": "-", + "size": 8214, + "lmtime": 1741161440653, + "modified": false + }, + "l3_b_8_7.jpg": { + "type": "-", + "size": 16480, + "lmtime": 1741161440764, + "modified": false + }, + "l3_b_8_7.webp": { + "type": "-", + "size": 8790, + "lmtime": 1741161440864, + "modified": false + }, + "l3_b_8_8.jpg": { + "type": "-", + "size": 9193, + "lmtime": 1741161440963, + "modified": false + }, + "l3_b_8_8.webp": { + "type": "-", + "size": 5078, + "lmtime": 1741161441059, + "modified": false + } + } + } + }, + "d": { + "l1": { + "1": { + "l1_d_1_1.jpg": { + "type": "-", + "size": 28780, + "lmtime": 1741161441174, + "modified": false + }, + "l1_d_1_1.webp": { + "type": "-", + "size": 14082, + "lmtime": 1741161441279, + "modified": false + }, + "l1_d_1_2.jpg": { + "type": "-", + "size": 31413, + "lmtime": 1741161441388, + "modified": false + }, + "l1_d_1_2.webp": { + "type": "-", + "size": 16152, + "lmtime": 1741161441493, + "modified": false + } + }, + "2": { + "l1_d_2_1.jpg": { + "type": "-", + "size": 30655, + "lmtime": 1741161441616, + "modified": false + }, + "l1_d_2_1.webp": { + "type": "-", + "size": 15276, + "lmtime": 1741161441709, + "modified": false + }, + "l1_d_2_2.jpg": { + "type": "-", + "size": 31392, + "lmtime": 1741161441832, + "modified": false + }, + "l1_d_2_2.webp": { + "type": "-", + "size": 16350, + "lmtime": 1741161441933, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_d_1_1.jpg": { + "type": "-", + "size": 33439, + "lmtime": 1741161442053, + "modified": false + }, + "l2_d_1_1.webp": { + "type": "-", + "size": 20424, + "lmtime": 1741161442164, + "modified": false + }, + "l2_d_1_2.jpg": { + "type": "-", + "size": 34830, + "lmtime": 1741161442279, + "modified": false + }, + "l2_d_1_2.webp": { + "type": "-", + "size": 22618, + "lmtime": 1741161442371, + "modified": false + }, + "l2_d_1_3.jpg": { + "type": "-", + "size": 29079, + "lmtime": 1741161442470, + "modified": false + }, + "l2_d_1_3.webp": { + "type": "-", + "size": 14636, + "lmtime": 1741161442558, + "modified": false + }, + "l2_d_1_4.jpg": { + "type": "-", + "size": 10668, + "lmtime": 1741161442633, + "modified": false + }, + "l2_d_1_4.webp": { + "type": "-", + "size": 5444, + "lmtime": 1741161442731, + "modified": false + } + }, + "2": { + "l2_d_2_1.jpg": { + "type": "-", + "size": 29530, + "lmtime": 1741161442831, + "modified": false + }, + "l2_d_2_1.webp": { + "type": "-", + "size": 14536, + "lmtime": 1741161442917, + "modified": false + }, + "l2_d_2_2.jpg": { + "type": "-", + "size": 30913, + "lmtime": 1741161443014, + "modified": false + }, + "l2_d_2_2.webp": { + "type": "-", + "size": 15914, + "lmtime": 1741161443125, + "modified": false + }, + "l2_d_2_3.jpg": { + "type": "-", + "size": 35948, + "lmtime": 1741161443238, + "modified": false + }, + "l2_d_2_3.webp": { + "type": "-", + "size": 22906, + "lmtime": 1741161443345, + "modified": false + }, + "l2_d_2_4.jpg": { + "type": "-", + "size": 20924, + "lmtime": 1741161443445, + "modified": false + }, + "l2_d_2_4.webp": { + "type": "-", + "size": 15094, + "lmtime": 1741161443531, + "modified": false + } + }, + "3": { + "l2_d_3_1.jpg": { + "type": "-", + "size": 40323, + "lmtime": 1741161443660, + "modified": false + }, + "l2_d_3_1.webp": { + "type": "-", + "size": 28616, + "lmtime": 1741161443778, + "modified": false + }, + "l2_d_3_2.jpg": { + "type": "-", + "size": 30097, + "lmtime": 1741161443883, + "modified": false + }, + "l2_d_3_2.webp": { + "type": "-", + "size": 16304, + "lmtime": 1741161443982, + "modified": false + }, + "l2_d_3_3.jpg": { + "type": "-", + "size": 35240, + "lmtime": 1741161444098, + "modified": false + }, + "l2_d_3_3.webp": { + "type": "-", + "size": 21860, + "lmtime": 1741161444200, + "modified": false + }, + "l2_d_3_4.jpg": { + "type": "-", + "size": 17571, + "lmtime": 1741161444314, + "modified": false + }, + "l2_d_3_4.webp": { + "type": "-", + "size": 10846, + "lmtime": 1741161444401, + "modified": false + } + }, + "4": { + "l2_d_4_1.jpg": { + "type": "-", + "size": 22916, + "lmtime": 1741161444511, + "modified": false + }, + "l2_d_4_1.webp": { + "type": "-", + "size": 17094, + "lmtime": 1741161444607, + "modified": false + }, + "l2_d_4_2.jpg": { + "type": "-", + "size": 15501, + "lmtime": 1741161444698, + "modified": false + }, + "l2_d_4_2.webp": { + "type": "-", + "size": 8996, + "lmtime": 1741161444783, + "modified": false + }, + "l2_d_4_3.jpg": { + "type": "-", + "size": 13251, + "lmtime": 1741161444870, + "modified": false + }, + "l2_d_4_3.webp": { + "type": "-", + "size": 6216, + "lmtime": 1741161444945, + "modified": false + }, + "l2_d_4_4.jpg": { + "type": "-", + "size": 6315, + "lmtime": 1741161445027, + "modified": false + }, + "l2_d_4_4.webp": { + "type": "-", + "size": 2924, + "lmtime": 1741161445098, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_d_1_1.jpg": { + "type": "-", + "size": 24302, + "lmtime": 1741161445231, + "modified": false + }, + "l3_d_1_1.webp": { + "type": "-", + "size": 10206, + "lmtime": 1741161445321, + "modified": false + }, + "l3_d_1_2.jpg": { + "type": "-", + "size": 28312, + "lmtime": 1741161445421, + "modified": false + }, + "l3_d_1_2.webp": { + "type": "-", + "size": 14228, + "lmtime": 1741161445530, + "modified": false + }, + "l3_d_1_3.jpg": { + "type": "-", + "size": 37078, + "lmtime": 1741161445648, + "modified": false + }, + "l3_d_1_3.webp": { + "type": "-", + "size": 22842, + "lmtime": 1741161445788, + "modified": false + }, + "l3_d_1_4.jpg": { + "type": "-", + "size": 38010, + "lmtime": 1741161445980, + "modified": false + }, + "l3_d_1_4.webp": { + "type": "-", + "size": 23278, + "lmtime": 1741161446232, + "modified": false + }, + "l3_d_1_5.jpg": { + "type": "-", + "size": 38490, + "lmtime": 1741161446586, + "modified": false + }, + "l3_d_1_5.webp": { + "type": "-", + "size": 24140, + "lmtime": 1741161447012, + "modified": false + }, + "l3_d_1_6.jpg": { + "type": "-", + "size": 24427, + "lmtime": 1741161447125, + "modified": false + }, + "l3_d_1_6.webp": { + "type": "-", + "size": 10454, + "lmtime": 1741161447224, + "modified": false + }, + "l3_d_1_7.jpg": { + "type": "-", + "size": 18271, + "lmtime": 1741161447332, + "modified": false + }, + "l3_d_1_7.webp": { + "type": "-", + "size": 6792, + "lmtime": 1741161447435, + "modified": false + }, + "l3_d_1_8.jpg": { + "type": "-", + "size": 6935, + "lmtime": 1741161447552, + "modified": false + }, + "l3_d_1_8.webp": { + "type": "-", + "size": 2698, + "lmtime": 1741161447632, + "modified": false + } + }, + "2": { + "l3_d_2_1.jpg": { + "type": "-", + "size": 36662, + "lmtime": 1741161447736, + "modified": false + }, + "l3_d_2_1.webp": { + "type": "-", + "size": 23188, + "lmtime": 1741161447843, + "modified": false + }, + "l3_d_2_2.jpg": { + "type": "-", + "size": 40979, + "lmtime": 1741161447972, + "modified": false + }, + "l3_d_2_2.webp": { + "type": "-", + "size": 28332, + "lmtime": 1741161448084, + "modified": false + }, + "l3_d_2_3.jpg": { + "type": "-", + "size": 41114, + "lmtime": 1741161448187, + "modified": false + }, + "l3_d_2_3.webp": { + "type": "-", + "size": 28670, + "lmtime": 1741161448278, + "modified": false + }, + "l3_d_2_4.jpg": { + "type": "-", + "size": 39430, + "lmtime": 1741161448381, + "modified": false + }, + "l3_d_2_4.webp": { + "type": "-", + "size": 26648, + "lmtime": 1741161448470, + "modified": false + }, + "l3_d_2_5.jpg": { + "type": "-", + "size": 39715, + "lmtime": 1741161448567, + "modified": false + }, + "l3_d_2_5.webp": { + "type": "-", + "size": 26330, + "lmtime": 1741161448659, + "modified": false + }, + "l3_d_2_6.jpg": { + "type": "-", + "size": 23219, + "lmtime": 1741161448746, + "modified": false + }, + "l3_d_2_6.webp": { + "type": "-", + "size": 9848, + "lmtime": 1741161448830, + "modified": false + }, + "l3_d_2_7.jpg": { + "type": "-", + "size": 17344, + "lmtime": 1741161448942, + "modified": false + }, + "l3_d_2_7.webp": { + "type": "-", + "size": 6786, + "lmtime": 1741161449037, + "modified": false + }, + "l3_d_2_8.jpg": { + "type": "-", + "size": 7622, + "lmtime": 1741161449139, + "modified": false + }, + "l3_d_2_8.webp": { + "type": "-", + "size": 2930, + "lmtime": 1741161449222, + "modified": false + } + }, + "3": { + "l3_d_3_1.jpg": { + "type": "-", + "size": 35675, + "lmtime": 1741161449323, + "modified": false + }, + "l3_d_3_1.webp": { + "type": "-", + "size": 21054, + "lmtime": 1741161449412, + "modified": false + }, + "l3_d_3_2.jpg": { + "type": "-", + "size": 37781, + "lmtime": 1741161449502, + "modified": false + }, + "l3_d_3_2.webp": { + "type": "-", + "size": 23710, + "lmtime": 1741161449586, + "modified": false + }, + "l3_d_3_3.jpg": { + "type": "-", + "size": 39384, + "lmtime": 1741161449683, + "modified": false + }, + "l3_d_3_3.webp": { + "type": "-", + "size": 25794, + "lmtime": 1741161449767, + "modified": false + }, + "l3_d_3_4.jpg": { + "type": "-", + "size": 35785, + "lmtime": 1741161449860, + "modified": false + }, + "l3_d_3_4.webp": { + "type": "-", + "size": 21822, + "lmtime": 1741161449947, + "modified": false + }, + "l3_d_3_5.jpg": { + "type": "-", + "size": 38851, + "lmtime": 1741161450043, + "modified": false + }, + "l3_d_3_5.webp": { + "type": "-", + "size": 25952, + "lmtime": 1741161450134, + "modified": false + }, + "l3_d_3_6.jpg": { + "type": "-", + "size": 30836, + "lmtime": 1741161450233, + "modified": false + }, + "l3_d_3_6.webp": { + "type": "-", + "size": 16678, + "lmtime": 1741161450327, + "modified": false + }, + "l3_d_3_7.jpg": { + "type": "-", + "size": 32590, + "lmtime": 1741161450417, + "modified": false + }, + "l3_d_3_7.webp": { + "type": "-", + "size": 18586, + "lmtime": 1741161450513, + "modified": false + }, + "l3_d_3_8.jpg": { + "type": "-", + "size": 16880, + "lmtime": 1741161450598, + "modified": false + }, + "l3_d_3_8.webp": { + "type": "-", + "size": 9696, + "lmtime": 1741161450670, + "modified": false + } + }, + "4": { + "l3_d_4_1.jpg": { + "type": "-", + "size": 33077, + "lmtime": 1741161450771, + "modified": false + }, + "l3_d_4_1.webp": { + "type": "-", + "size": 19538, + "lmtime": 1741161450856, + "modified": false + }, + "l3_d_4_2.jpg": { + "type": "-", + "size": 31614, + "lmtime": 1741161450951, + "modified": false + }, + "l3_d_4_2.webp": { + "type": "-", + "size": 16756, + "lmtime": 1741161451042, + "modified": false + }, + "l3_d_4_3.jpg": { + "type": "-", + "size": 32344, + "lmtime": 1741161451133, + "modified": false + }, + "l3_d_4_3.webp": { + "type": "-", + "size": 18086, + "lmtime": 1741161451216, + "modified": false + }, + "l3_d_4_4.jpg": { + "type": "-", + "size": 12933, + "lmtime": 1741161451296, + "modified": false + }, + "l3_d_4_4.webp": { + "type": "-", + "size": 6314, + "lmtime": 1741161451367, + "modified": false + }, + "l3_d_4_5.jpg": { + "type": "-", + "size": 25463, + "lmtime": 1741161451463, + "modified": false + }, + "l3_d_4_5.webp": { + "type": "-", + "size": 12236, + "lmtime": 1741161451533, + "modified": false + }, + "l3_d_4_6.jpg": { + "type": "-", + "size": 44630, + "lmtime": 1741161451625, + "modified": false + }, + "l3_d_4_6.webp": { + "type": "-", + "size": 32236, + "lmtime": 1741161451851, + "modified": false + }, + "l3_d_4_7.jpg": { + "type": "-", + "size": 46816, + "lmtime": 1741161452161, + "modified": false + }, + "l3_d_4_7.webp": { + "type": "-", + "size": 34152, + "lmtime": 1741161452264, + "modified": false + }, + "l3_d_4_8.jpg": { + "type": "-", + "size": 22525, + "lmtime": 1741161452348, + "modified": false + }, + "l3_d_4_8.webp": { + "type": "-", + "size": 15790, + "lmtime": 1741161452433, + "modified": false + } + }, + "5": { + "l3_d_5_1.jpg": { + "type": "-", + "size": 37733, + "lmtime": 1741161452527, + "modified": false + }, + "l3_d_5_1.webp": { + "type": "-", + "size": 24278, + "lmtime": 1741161452612, + "modified": false + }, + "l3_d_5_2.jpg": { + "type": "-", + "size": 34453, + "lmtime": 1741161452708, + "modified": false + }, + "l3_d_5_2.webp": { + "type": "-", + "size": 19564, + "lmtime": 1741161452815, + "modified": false + }, + "l3_d_5_3.jpg": { + "type": "-", + "size": 33856, + "lmtime": 1741161452943, + "modified": false + }, + "l3_d_5_3.webp": { + "type": "-", + "size": 19912, + "lmtime": 1741161453049, + "modified": false + }, + "l3_d_5_4.jpg": { + "type": "-", + "size": 27286, + "lmtime": 1741161453164, + "modified": false + }, + "l3_d_5_4.webp": { + "type": "-", + "size": 12712, + "lmtime": 1741161453255, + "modified": false + }, + "l3_d_5_5.jpg": { + "type": "-", + "size": 38170, + "lmtime": 1741161453356, + "modified": false + }, + "l3_d_5_5.webp": { + "type": "-", + "size": 24370, + "lmtime": 1741161453451, + "modified": false + }, + "l3_d_5_6.jpg": { + "type": "-", + "size": 45465, + "lmtime": 1741161453573, + "modified": false + }, + "l3_d_5_6.webp": { + "type": "-", + "size": 33092, + "lmtime": 1741161453684, + "modified": false + }, + "l3_d_5_7.jpg": { + "type": "-", + "size": 46194, + "lmtime": 1741161453783, + "modified": false + }, + "l3_d_5_7.webp": { + "type": "-", + "size": 33502, + "lmtime": 1741161453885, + "modified": false + }, + "l3_d_5_8.jpg": { + "type": "-", + "size": 22076, + "lmtime": 1741161453984, + "modified": false + }, + "l3_d_5_8.webp": { + "type": "-", + "size": 15194, + "lmtime": 1741161454073, + "modified": false + } + }, + "6": { + "l3_d_6_1.jpg": { + "type": "-", + "size": 59020, + "lmtime": 1741161454184, + "modified": false + }, + "l3_d_6_1.webp": { + "type": "-", + "size": 47418, + "lmtime": 1741161454305, + "modified": false + }, + "l3_d_6_2.jpg": { + "type": "-", + "size": 31763, + "lmtime": 1741161454416, + "modified": false + }, + "l3_d_6_2.webp": { + "type": "-", + "size": 15984, + "lmtime": 1741161454527, + "modified": false + }, + "l3_d_6_3.jpg": { + "type": "-", + "size": 34494, + "lmtime": 1741161454639, + "modified": false + }, + "l3_d_6_3.webp": { + "type": "-", + "size": 21290, + "lmtime": 1741161454757, + "modified": false + }, + "l3_d_6_4.jpg": { + "type": "-", + "size": 36726, + "lmtime": 1741161454882, + "modified": false + }, + "l3_d_6_4.webp": { + "type": "-", + "size": 23168, + "lmtime": 1741161454999, + "modified": false + }, + "l3_d_6_5.jpg": { + "type": "-", + "size": 36620, + "lmtime": 1741161455123, + "modified": false + }, + "l3_d_6_5.webp": { + "type": "-", + "size": 22724, + "lmtime": 1741161455236, + "modified": false + }, + "l3_d_6_6.jpg": { + "type": "-", + "size": 36799, + "lmtime": 1741161455351, + "modified": false + }, + "l3_d_6_6.webp": { + "type": "-", + "size": 21732, + "lmtime": 1741161455439, + "modified": false + }, + "l3_d_6_7.jpg": { + "type": "-", + "size": 32204, + "lmtime": 1741161455557, + "modified": false + }, + "l3_d_6_7.webp": { + "type": "-", + "size": 17378, + "lmtime": 1741161455662, + "modified": false + }, + "l3_d_6_8.jpg": { + "type": "-", + "size": 15789, + "lmtime": 1741161455736, + "modified": false + }, + "l3_d_6_8.webp": { + "type": "-", + "size": 8092, + "lmtime": 1741161455821, + "modified": false + } + }, + "7": { + "l3_d_7_1.jpg": { + "type": "-", + "size": 57843, + "lmtime": 1741161455940, + "modified": false + }, + "l3_d_7_1.webp": { + "type": "-", + "size": 43634, + "lmtime": 1741161456048, + "modified": false + }, + "l3_d_7_2.jpg": { + "type": "-", + "size": 29631, + "lmtime": 1741161456153, + "modified": false + }, + "l3_d_7_2.webp": { + "type": "-", + "size": 14242, + "lmtime": 1741161456254, + "modified": false + }, + "l3_d_7_3.jpg": { + "type": "-", + "size": 34410, + "lmtime": 1741161456356, + "modified": false + }, + "l3_d_7_3.webp": { + "type": "-", + "size": 21006, + "lmtime": 1741161456468, + "modified": false + }, + "l3_d_7_4.jpg": { + "type": "-", + "size": 36758, + "lmtime": 1741161456578, + "modified": false + }, + "l3_d_7_4.webp": { + "type": "-", + "size": 22736, + "lmtime": 1741161456681, + "modified": false + }, + "l3_d_7_5.jpg": { + "type": "-", + "size": 35823, + "lmtime": 1741161456808, + "modified": false + }, + "l3_d_7_5.webp": { + "type": "-", + "size": 22402, + "lmtime": 1741161456918, + "modified": false + }, + "l3_d_7_6.jpg": { + "type": "-", + "size": 28038, + "lmtime": 1741161457022, + "modified": false + }, + "l3_d_7_6.webp": { + "type": "-", + "size": 13220, + "lmtime": 1741161457117, + "modified": false + }, + "l3_d_7_7.jpg": { + "type": "-", + "size": 14641, + "lmtime": 1741161457213, + "modified": false + }, + "l3_d_7_7.webp": { + "type": "-", + "size": 4870, + "lmtime": 1741161457310, + "modified": false + }, + "l3_d_7_8.jpg": { + "type": "-", + "size": 6436, + "lmtime": 1741161457421, + "modified": false + }, + "l3_d_7_8.webp": { + "type": "-", + "size": 1810, + "lmtime": 1741161457519, + "modified": false + } + }, + "8": { + "l3_d_8_1.jpg": { + "type": "-", + "size": 25992, + "lmtime": 1741161457636, + "modified": false + }, + "l3_d_8_1.webp": { + "type": "-", + "size": 18390, + "lmtime": 1741161457785, + "modified": false + }, + "l3_d_8_2.jpg": { + "type": "-", + "size": 16869, + "lmtime": 1741161457895, + "modified": false + }, + "l3_d_8_2.webp": { + "type": "-", + "size": 9068, + "lmtime": 1741161457999, + "modified": false + }, + "l3_d_8_3.jpg": { + "type": "-", + "size": 16164, + "lmtime": 1741161458105, + "modified": false + }, + "l3_d_8_3.webp": { + "type": "-", + "size": 9392, + "lmtime": 1741161458192, + "modified": false + }, + "l3_d_8_4.jpg": { + "type": "-", + "size": 17326, + "lmtime": 1741161458306, + "modified": false + }, + "l3_d_8_4.webp": { + "type": "-", + "size": 10308, + "lmtime": 1741161458386, + "modified": false + }, + "l3_d_8_5.jpg": { + "type": "-", + "size": 17604, + "lmtime": 1741161458504, + "modified": false + }, + "l3_d_8_5.webp": { + "type": "-", + "size": 10384, + "lmtime": 1741161458606, + "modified": false + }, + "l3_d_8_6.jpg": { + "type": "-", + "size": 14172, + "lmtime": 1741161458708, + "modified": false + }, + "l3_d_8_6.webp": { + "type": "-", + "size": 6544, + "lmtime": 1741161458799, + "modified": false + }, + "l3_d_8_7.jpg": { + "type": "-", + "size": 10670, + "lmtime": 1741161458910, + "modified": false + }, + "l3_d_8_7.webp": { + "type": "-", + "size": 4116, + "lmtime": 1741161459012, + "modified": false + }, + "l3_d_8_8.jpg": { + "type": "-", + "size": 2772, + "lmtime": 1741161459114, + "modified": false + }, + "l3_d_8_8.webp": { + "type": "-", + "size": 774, + "lmtime": 1741161459215, + "modified": false + } + } + } + }, + "f": { + "l1": { + "1": { + "l1_f_1_1.jpg": { + "type": "-", + "size": 23041, + "lmtime": 1741161459339, + "modified": false + }, + "l1_f_1_1.webp": { + "type": "-", + "size": 12132, + "lmtime": 1741161459453, + "modified": false + }, + "l1_f_1_2.jpg": { + "type": "-", + "size": 26216, + "lmtime": 1741161459564, + "modified": false + }, + "l1_f_1_2.webp": { + "type": "-", + "size": 15330, + "lmtime": 1741161459665, + "modified": false + } + }, + "2": { + "l1_f_2_1.jpg": { + "type": "-", + "size": 36874, + "lmtime": 1741161459790, + "modified": false + }, + "l1_f_2_1.webp": { + "type": "-", + "size": 20328, + "lmtime": 1741161459888, + "modified": false + }, + "l1_f_2_2.jpg": { + "type": "-", + "size": 40797, + "lmtime": 1741161459983, + "modified": false + }, + "l1_f_2_2.webp": { + "type": "-", + "size": 23306, + "lmtime": 1741161460077, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_f_1_1.jpg": { + "type": "-", + "size": 20054, + "lmtime": 1741161460181, + "modified": false + }, + "l2_f_1_1.webp": { + "type": "-", + "size": 9826, + "lmtime": 1741161460253, + "modified": false + }, + "l2_f_1_2.jpg": { + "type": "-", + "size": 17407, + "lmtime": 1741161460340, + "modified": false + }, + "l2_f_1_2.webp": { + "type": "-", + "size": 8418, + "lmtime": 1741161460417, + "modified": false + }, + "l2_f_1_3.jpg": { + "type": "-", + "size": 12004, + "lmtime": 1741161460486, + "modified": false + }, + "l2_f_1_3.webp": { + "type": "-", + "size": 5160, + "lmtime": 1741161460559, + "modified": false + }, + "l2_f_1_4.jpg": { + "type": "-", + "size": 16339, + "lmtime": 1741161460645, + "modified": false + }, + "l2_f_1_4.webp": { + "type": "-", + "size": 9042, + "lmtime": 1741161460729, + "modified": false + } + }, + "2": { + "l2_f_2_1.jpg": { + "type": "-", + "size": 16590, + "lmtime": 1741161460819, + "modified": false + }, + "l2_f_2_1.webp": { + "type": "-", + "size": 6632, + "lmtime": 1741161460890, + "modified": false + }, + "l2_f_2_2.jpg": { + "type": "-", + "size": 43129, + "lmtime": 1741161460985, + "modified": false + }, + "l2_f_2_2.webp": { + "type": "-", + "size": 27168, + "lmtime": 1741161461080, + "modified": false + }, + "l2_f_2_3.jpg": { + "type": "-", + "size": 45751, + "lmtime": 1741161461193, + "modified": false + }, + "l2_f_2_3.webp": { + "type": "-", + "size": 30446, + "lmtime": 1741161461281, + "modified": false + }, + "l2_f_2_4.jpg": { + "type": "-", + "size": 5699, + "lmtime": 1741161461366, + "modified": false + }, + "l2_f_2_4.webp": { + "type": "-", + "size": 1982, + "lmtime": 1741161461433, + "modified": false + } + }, + "3": { + "l2_f_3_1.jpg": { + "type": "-", + "size": 29007, + "lmtime": 1741161461526, + "modified": false + }, + "l2_f_3_1.webp": { + "type": "-", + "size": 15688, + "lmtime": 1741161461602, + "modified": false + }, + "l2_f_3_2.jpg": { + "type": "-", + "size": 34617, + "lmtime": 1741161461692, + "modified": false + }, + "l2_f_3_2.webp": { + "type": "-", + "size": 17696, + "lmtime": 1741161461778, + "modified": false + }, + "l2_f_3_3.jpg": { + "type": "-", + "size": 40047, + "lmtime": 1741161461873, + "modified": false + }, + "l2_f_3_3.webp": { + "type": "-", + "size": 22334, + "lmtime": 1741161461971, + "modified": false + }, + "l2_f_3_4.jpg": { + "type": "-", + "size": 5429, + "lmtime": 1741161462046, + "modified": false + }, + "l2_f_3_4.webp": { + "type": "-", + "size": 1738, + "lmtime": 1741161462116, + "modified": false + } + }, + "4": { + "l2_f_4_1.jpg": { + "type": "-", + "size": 12418, + "lmtime": 1741161462210, + "modified": false + }, + "l2_f_4_1.webp": { + "type": "-", + "size": 5906, + "lmtime": 1741161462282, + "modified": false + }, + "l2_f_4_2.jpg": { + "type": "-", + "size": 17912, + "lmtime": 1741161462376, + "modified": false + }, + "l2_f_4_2.webp": { + "type": "-", + "size": 11232, + "lmtime": 1741161462453, + "modified": false + }, + "l2_f_4_3.jpg": { + "type": "-", + "size": 23369, + "lmtime": 1741161462539, + "modified": false + }, + "l2_f_4_3.webp": { + "type": "-", + "size": 14248, + "lmtime": 1741161462617, + "modified": false + }, + "l2_f_4_4.jpg": { + "type": "-", + "size": 5743, + "lmtime": 1741161462700, + "modified": false + }, + "l2_f_4_4.webp": { + "type": "-", + "size": 2754, + "lmtime": 1741161462774, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_f_1_1.jpg": { + "type": "-", + "size": 17262, + "lmtime": 1741161462873, + "modified": false + }, + "l3_f_1_1.webp": { + "type": "-", + "size": 5856, + "lmtime": 1741161462960, + "modified": false + }, + "l3_f_1_2.jpg": { + "type": "-", + "size": 41234, + "lmtime": 1741161463049, + "modified": false + }, + "l3_f_1_2.webp": { + "type": "-", + "size": 24472, + "lmtime": 1741161463140, + "modified": false + }, + "l3_f_1_3.jpg": { + "type": "-", + "size": 34576, + "lmtime": 1741161463240, + "modified": false + }, + "l3_f_1_3.webp": { + "type": "-", + "size": 20252, + "lmtime": 1741161463337, + "modified": false + }, + "l3_f_1_4.jpg": { + "type": "-", + "size": 18342, + "lmtime": 1741161463421, + "modified": false + }, + "l3_f_1_4.webp": { + "type": "-", + "size": 6182, + "lmtime": 1741161463503, + "modified": false + }, + "l3_f_1_5.jpg": { + "type": "-", + "size": 17462, + "lmtime": 1741161463589, + "modified": false + }, + "l3_f_1_5.webp": { + "type": "-", + "size": 6078, + "lmtime": 1741161463668, + "modified": false + }, + "l3_f_1_6.jpg": { + "type": "-", + "size": 17033, + "lmtime": 1741161463758, + "modified": false + }, + "l3_f_1_6.webp": { + "type": "-", + "size": 5306, + "lmtime": 1741161463835, + "modified": false + }, + "l3_f_1_7.jpg": { + "type": "-", + "size": 36924, + "lmtime": 1741161463929, + "modified": false + }, + "l3_f_1_7.webp": { + "type": "-", + "size": 19442, + "lmtime": 1741161464019, + "modified": false + }, + "l3_f_1_8.jpg": { + "type": "-", + "size": 19975, + "lmtime": 1741161464115, + "modified": false + }, + "l3_f_1_8.webp": { + "type": "-", + "size": 11580, + "lmtime": 1741161464192, + "modified": false + } + }, + "2": { + "l3_f_2_1.jpg": { + "type": "-", + "size": 15740, + "lmtime": 1741161464264, + "modified": false + }, + "l3_f_2_1.webp": { + "type": "-", + "size": 4804, + "lmtime": 1741161464351, + "modified": false + }, + "l3_f_2_2.jpg": { + "type": "-", + "size": 16149, + "lmtime": 1741161464440, + "modified": false + }, + "l3_f_2_2.webp": { + "type": "-", + "size": 4424, + "lmtime": 1741161464512, + "modified": false + }, + "l3_f_2_3.jpg": { + "type": "-", + "size": 17972, + "lmtime": 1741161464603, + "modified": false + }, + "l3_f_2_3.webp": { + "type": "-", + "size": 5938, + "lmtime": 1741161464695, + "modified": false + }, + "l3_f_2_4.jpg": { + "type": "-", + "size": 15209, + "lmtime": 1741161464769, + "modified": false + }, + "l3_f_2_4.webp": { + "type": "-", + "size": 4166, + "lmtime": 1741161464852, + "modified": false + }, + "l3_f_2_5.jpg": { + "type": "-", + "size": 13105, + "lmtime": 1741161464938, + "modified": false + }, + "l3_f_2_5.webp": { + "type": "-", + "size": 3098, + "lmtime": 1741161465032, + "modified": false + }, + "l3_f_2_6.jpg": { + "type": "-", + "size": 12269, + "lmtime": 1741161465103, + "modified": false + }, + "l3_f_2_6.webp": { + "type": "-", + "size": 2794, + "lmtime": 1741161465173, + "modified": false + }, + "l3_f_2_7.jpg": { + "type": "-", + "size": 16299, + "lmtime": 1741161465261, + "modified": false + }, + "l3_f_2_7.webp": { + "type": "-", + "size": 5124, + "lmtime": 1741161465337, + "modified": false + }, + "l3_f_2_8.jpg": { + "type": "-", + "size": 9172, + "lmtime": 1741161465416, + "modified": false + }, + "l3_f_2_8.webp": { + "type": "-", + "size": 3278, + "lmtime": 1741161465487, + "modified": false + } + }, + "3": { + "l3_f_3_1.jpg": { + "type": "-", + "size": 11848, + "lmtime": 1741161465557, + "modified": false + }, + "l3_f_3_1.webp": { + "type": "-", + "size": 2770, + "lmtime": 1741161465637, + "modified": false + }, + "l3_f_3_2.jpg": { + "type": "-", + "size": 18508, + "lmtime": 1741161465740, + "modified": false + }, + "l3_f_3_2.webp": { + "type": "-", + "size": 6210, + "lmtime": 1741161465826, + "modified": false + }, + "l3_f_3_3.jpg": { + "type": "-", + "size": 23716, + "lmtime": 1741161465911, + "modified": false + }, + "l3_f_3_3.webp": { + "type": "-", + "size": 12178, + "lmtime": 1741161465985, + "modified": false + }, + "l3_f_3_4.jpg": { + "type": "-", + "size": 19549, + "lmtime": 1741161466087, + "modified": false + }, + "l3_f_3_4.webp": { + "type": "-", + "size": 9126, + "lmtime": 1741161466166, + "modified": false + }, + "l3_f_3_5.jpg": { + "type": "-", + "size": 20198, + "lmtime": 1741161466264, + "modified": false + }, + "l3_f_3_5.webp": { + "type": "-", + "size": 9368, + "lmtime": 1741161466366, + "modified": false + }, + "l3_f_3_6.jpg": { + "type": "-", + "size": 21501, + "lmtime": 1741161466455, + "modified": false + }, + "l3_f_3_6.webp": { + "type": "-", + "size": 10694, + "lmtime": 1741161466535, + "modified": false + }, + "l3_f_3_7.jpg": { + "type": "-", + "size": 17904, + "lmtime": 1741161466633, + "modified": false + }, + "l3_f_3_7.webp": { + "type": "-", + "size": 6740, + "lmtime": 1741161466725, + "modified": false + }, + "l3_f_3_8.jpg": { + "type": "-", + "size": 7165, + "lmtime": 1741161466812, + "modified": false + }, + "l3_f_3_8.webp": { + "type": "-", + "size": 2022, + "lmtime": 1741161466895, + "modified": false + } + }, + "4": { + "l3_f_4_1.jpg": { + "type": "-", + "size": 13383, + "lmtime": 1741161466965, + "modified": false + }, + "l3_f_4_1.webp": { + "type": "-", + "size": 2764, + "lmtime": 1741161467045, + "modified": false + }, + "l3_f_4_2.jpg": { + "type": "-", + "size": 16536, + "lmtime": 1741161467131, + "modified": false + }, + "l3_f_4_2.webp": { + "type": "-", + "size": 5198, + "lmtime": 1741161467208, + "modified": false + }, + "l3_f_4_3.jpg": { + "type": "-", + "size": 23672, + "lmtime": 1741161467298, + "modified": false + }, + "l3_f_4_3.webp": { + "type": "-", + "size": 10860, + "lmtime": 1741161467387, + "modified": false + }, + "l3_f_4_4.jpg": { + "type": "-", + "size": 48703, + "lmtime": 1741161467482, + "modified": false + }, + "l3_f_4_4.webp": { + "type": "-", + "size": 31680, + "lmtime": 1741161467580, + "modified": false + }, + "l3_f_4_5.jpg": { + "type": "-", + "size": 48951, + "lmtime": 1741161467682, + "modified": false + }, + "l3_f_4_5.webp": { + "type": "-", + "size": 31006, + "lmtime": 1741161467773, + "modified": false + }, + "l3_f_4_6.jpg": { + "type": "-", + "size": 42951, + "lmtime": 1741161467865, + "modified": false + }, + "l3_f_4_6.webp": { + "type": "-", + "size": 25666, + "lmtime": 1741161467958, + "modified": false + }, + "l3_f_4_7.jpg": { + "type": "-", + "size": 22462, + "lmtime": 1741161468048, + "modified": false + }, + "l3_f_4_7.webp": { + "type": "-", + "size": 9986, + "lmtime": 1741161468132, + "modified": false + }, + "l3_f_4_8.jpg": { + "type": "-", + "size": 5935, + "lmtime": 1741161468207, + "modified": false + }, + "l3_f_4_8.webp": { + "type": "-", + "size": 1352, + "lmtime": 1741161468293, + "modified": false + } + }, + "5": { + "l3_f_5_1.jpg": { + "type": "-", + "size": 22327, + "lmtime": 1741161468396, + "modified": false + }, + "l3_f_5_1.webp": { + "type": "-", + "size": 9564, + "lmtime": 1741161468484, + "modified": false + }, + "l3_f_5_2.jpg": { + "type": "-", + "size": 35767, + "lmtime": 1741161468586, + "modified": false + }, + "l3_f_5_2.webp": { + "type": "-", + "size": 19044, + "lmtime": 1741161468689, + "modified": false + }, + "l3_f_5_3.jpg": { + "type": "-", + "size": 35904, + "lmtime": 1741161468796, + "modified": false + }, + "l3_f_5_3.webp": { + "type": "-", + "size": 19054, + "lmtime": 1741161468888, + "modified": false + }, + "l3_f_5_4.jpg": { + "type": "-", + "size": 40080, + "lmtime": 1741161468980, + "modified": false + }, + "l3_f_5_4.webp": { + "type": "-", + "size": 22316, + "lmtime": 1741161469077, + "modified": false + }, + "l3_f_5_5.jpg": { + "type": "-", + "size": 42370, + "lmtime": 1741161469173, + "modified": false + }, + "l3_f_5_5.webp": { + "type": "-", + "size": 24314, + "lmtime": 1741161469269, + "modified": false + }, + "l3_f_5_6.jpg": { + "type": "-", + "size": 47178, + "lmtime": 1741161469397, + "modified": false + }, + "l3_f_5_6.webp": { + "type": "-", + "size": 29468, + "lmtime": 1741161469495, + "modified": false + }, + "l3_f_5_7.jpg": { + "type": "-", + "size": 22087, + "lmtime": 1741161469593, + "modified": false + }, + "l3_f_5_7.webp": { + "type": "-", + "size": 8862, + "lmtime": 1741161469711, + "modified": false + }, + "l3_f_5_8.jpg": { + "type": "-", + "size": 7134, + "lmtime": 1741161469811, + "modified": false + }, + "l3_f_5_8.webp": { + "type": "-", + "size": 1862, + "lmtime": 1741161469912, + "modified": false + } + }, + "6": { + "l3_f_6_1.jpg": { + "type": "-", + "size": 15812, + "lmtime": 1741161470004, + "modified": false + }, + "l3_f_6_1.webp": { + "type": "-", + "size": 5938, + "lmtime": 1741161470121, + "modified": false + }, + "l3_f_6_2.jpg": { + "type": "-", + "size": 19993, + "lmtime": 1741161470217, + "modified": false + }, + "l3_f_6_2.webp": { + "type": "-", + "size": 7968, + "lmtime": 1741161470329, + "modified": false + }, + "l3_f_6_3.jpg": { + "type": "-", + "size": 30406, + "lmtime": 1741161470449, + "modified": false + }, + "l3_f_6_3.webp": { + "type": "-", + "size": 14868, + "lmtime": 1741161470533, + "modified": false + }, + "l3_f_6_4.jpg": { + "type": "-", + "size": 36527, + "lmtime": 1741161470623, + "modified": false + }, + "l3_f_6_4.webp": { + "type": "-", + "size": 20636, + "lmtime": 1741161470722, + "modified": false + }, + "l3_f_6_5.jpg": { + "type": "-", + "size": 35832, + "lmtime": 1741161470825, + "modified": false + }, + "l3_f_6_5.webp": { + "type": "-", + "size": 19612, + "lmtime": 1741161470930, + "modified": false + }, + "l3_f_6_6.jpg": { + "type": "-", + "size": 31837, + "lmtime": 1741161471041, + "modified": false + }, + "l3_f_6_6.webp": { + "type": "-", + "size": 15272, + "lmtime": 1741161471137, + "modified": false + }, + "l3_f_6_7.jpg": { + "type": "-", + "size": 18075, + "lmtime": 1741161471234, + "modified": false + }, + "l3_f_6_7.webp": { + "type": "-", + "size": 5596, + "lmtime": 1741161471328, + "modified": false + }, + "l3_f_6_8.jpg": { + "type": "-", + "size": 7509, + "lmtime": 1741161471426, + "modified": false + }, + "l3_f_6_8.webp": { + "type": "-", + "size": 2066, + "lmtime": 1741161471495, + "modified": false + } + }, + "7": { + "l3_f_7_1.jpg": { + "type": "-", + "size": 27998, + "lmtime": 1741161471582, + "modified": false + }, + "l3_f_7_1.webp": { + "type": "-", + "size": 12392, + "lmtime": 1741161471660, + "modified": false + }, + "l3_f_7_2.jpg": { + "type": "-", + "size": 19031, + "lmtime": 1741161471752, + "modified": false + }, + "l3_f_7_2.webp": { + "type": "-", + "size": 7590, + "lmtime": 1741161471829, + "modified": false + }, + "l3_f_7_3.jpg": { + "type": "-", + "size": 32340, + "lmtime": 1741161471922, + "modified": false + }, + "l3_f_7_3.webp": { + "type": "-", + "size": 17724, + "lmtime": 1741161472028, + "modified": false + }, + "l3_f_7_4.jpg": { + "type": "-", + "size": 38624, + "lmtime": 1741161472127, + "modified": false + }, + "l3_f_7_4.webp": { + "type": "-", + "size": 24418, + "lmtime": 1741161472240, + "modified": false + }, + "l3_f_7_5.jpg": { + "type": "-", + "size": 42633, + "lmtime": 1741161472348, + "modified": false + }, + "l3_f_7_5.webp": { + "type": "-", + "size": 27256, + "lmtime": 1741161472442, + "modified": false + }, + "l3_f_7_6.jpg": { + "type": "-", + "size": 40745, + "lmtime": 1741161472538, + "modified": false + }, + "l3_f_7_6.webp": { + "type": "-", + "size": 21308, + "lmtime": 1741161472636, + "modified": false + }, + "l3_f_7_7.jpg": { + "type": "-", + "size": 24024, + "lmtime": 1741161472731, + "modified": false + }, + "l3_f_7_7.webp": { + "type": "-", + "size": 10170, + "lmtime": 1741161472810, + "modified": false + }, + "l3_f_7_8.jpg": { + "type": "-", + "size": 9726, + "lmtime": 1741161472882, + "modified": false + }, + "l3_f_7_8.webp": { + "type": "-", + "size": 3948, + "lmtime": 1741161472954, + "modified": false + } + }, + "8": { + "l3_f_8_1.jpg": { + "type": "-", + "size": 10904, + "lmtime": 1741161473037, + "modified": false + }, + "l3_f_8_1.webp": { + "type": "-", + "size": 4208, + "lmtime": 1741161473120, + "modified": false + }, + "l3_f_8_2.jpg": { + "type": "-", + "size": 5753, + "lmtime": 1741161473194, + "modified": false + }, + "l3_f_8_2.webp": { + "type": "-", + "size": 1462, + "lmtime": 1741161473268, + "modified": false + }, + "l3_f_8_3.jpg": { + "type": "-", + "size": 16606, + "lmtime": 1741161473352, + "modified": false + }, + "l3_f_8_3.webp": { + "type": "-", + "size": 9150, + "lmtime": 1741161473428, + "modified": false + }, + "l3_f_8_4.jpg": { + "type": "-", + "size": 18969, + "lmtime": 1741161473511, + "modified": false + }, + "l3_f_8_4.webp": { + "type": "-", + "size": 11654, + "lmtime": 1741161473586, + "modified": false + }, + "l3_f_8_5.jpg": { + "type": "-", + "size": 18921, + "lmtime": 1741161473677, + "modified": false + }, + "l3_f_8_5.webp": { + "type": "-", + "size": 11522, + "lmtime": 1741161473748, + "modified": false + }, + "l3_f_8_6.jpg": { + "type": "-", + "size": 17186, + "lmtime": 1741161473854, + "modified": false + }, + "l3_f_8_6.webp": { + "type": "-", + "size": 8440, + "lmtime": 1741161473927, + "modified": false + }, + "l3_f_8_7.jpg": { + "type": "-", + "size": 6830, + "lmtime": 1741161473996, + "modified": false + }, + "l3_f_8_7.webp": { + "type": "-", + "size": 2360, + "lmtime": 1741161474072, + "modified": false + }, + "l3_f_8_8.jpg": { + "type": "-", + "size": 4302, + "lmtime": 1741161474150, + "modified": false + }, + "l3_f_8_8.webp": { + "type": "-", + "size": 1508, + "lmtime": 1741161474229, + "modified": false + } + } + } + }, + "l": { + "l1": { + "1": { + "l1_l_1_1.jpg": { + "type": "-", + "size": 25599, + "lmtime": 1741161474324, + "modified": false + }, + "l1_l_1_1.webp": { + "type": "-", + "size": 13740, + "lmtime": 1741161474401, + "modified": false + }, + "l1_l_1_2.jpg": { + "type": "-", + "size": 20869, + "lmtime": 1741161474486, + "modified": false + }, + "l1_l_1_2.webp": { + "type": "-", + "size": 11260, + "lmtime": 1741161474568, + "modified": false + } + }, + "2": { + "l1_l_2_1.jpg": { + "type": "-", + "size": 39559, + "lmtime": 1741161474677, + "modified": false + }, + "l1_l_2_1.webp": { + "type": "-", + "size": 22572, + "lmtime": 1741161474773, + "modified": false + }, + "l1_l_2_2.jpg": { + "type": "-", + "size": 36367, + "lmtime": 1741161474890, + "modified": false + }, + "l1_l_2_2.webp": { + "type": "-", + "size": 20050, + "lmtime": 1741161474994, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_l_1_1.jpg": { + "type": "-", + "size": 10052, + "lmtime": 1741161475085, + "modified": false + }, + "l2_l_1_1.webp": { + "type": "-", + "size": 3080, + "lmtime": 1741161475186, + "modified": false + }, + "l2_l_1_2.jpg": { + "type": "-", + "size": 8192, + "lmtime": 1741161475287, + "modified": false + }, + "l2_l_1_2.webp": { + "type": "-", + "size": 2998, + "lmtime": 1741161475375, + "modified": false + }, + "l2_l_1_3.jpg": { + "type": "-", + "size": 9047, + "lmtime": 1741161475466, + "modified": false + }, + "l2_l_1_3.webp": { + "type": "-", + "size": 3340, + "lmtime": 1741161475542, + "modified": false + }, + "l2_l_1_4.jpg": { + "type": "-", + "size": 3798, + "lmtime": 1741161475649, + "modified": false + }, + "l2_l_1_4.webp": { + "type": "-", + "size": 1020, + "lmtime": 1741161475748, + "modified": false + } + }, + "2": { + "l2_l_2_1.jpg": { + "type": "-", + "size": 43613, + "lmtime": 1741161475868, + "modified": false + }, + "l2_l_2_1.webp": { + "type": "-", + "size": 27394, + "lmtime": 1741161475980, + "modified": false + }, + "l2_l_2_2.jpg": { + "type": "-", + "size": 22555, + "lmtime": 1741161476085, + "modified": false + }, + "l2_l_2_2.webp": { + "type": "-", + "size": 10208, + "lmtime": 1741161476178, + "modified": false + }, + "l2_l_2_3.jpg": { + "type": "-", + "size": 41270, + "lmtime": 1741161476293, + "modified": false + }, + "l2_l_2_3.webp": { + "type": "-", + "size": 25308, + "lmtime": 1741161476397, + "modified": false + }, + "l2_l_2_4.jpg": { + "type": "-", + "size": 11828, + "lmtime": 1741161476476, + "modified": false + }, + "l2_l_2_4.webp": { + "type": "-", + "size": 6476, + "lmtime": 1741161476558, + "modified": false + } + }, + "3": { + "l2_l_3_1.jpg": { + "type": "-", + "size": 33711, + "lmtime": 1741161476649, + "modified": false + }, + "l2_l_3_1.webp": { + "type": "-", + "size": 17520, + "lmtime": 1741161476736, + "modified": false + }, + "l2_l_3_2.jpg": { + "type": "-", + "size": 23432, + "lmtime": 1741161476825, + "modified": false + }, + "l2_l_3_2.webp": { + "type": "-", + "size": 10920, + "lmtime": 1741161476899, + "modified": false + }, + "l2_l_3_3.jpg": { + "type": "-", + "size": 33470, + "lmtime": 1741161476988, + "modified": false + }, + "l2_l_3_3.webp": { + "type": "-", + "size": 19120, + "lmtime": 1741161477119, + "modified": false + }, + "l2_l_3_4.jpg": { + "type": "-", + "size": 17386, + "lmtime": 1741161477205, + "modified": false + }, + "l2_l_3_4.webp": { + "type": "-", + "size": 9228, + "lmtime": 1741161477306, + "modified": false + } + }, + "4": { + "l2_l_4_1.jpg": { + "type": "-", + "size": 29249, + "lmtime": 1741161477404, + "modified": false + }, + "l2_l_4_1.webp": { + "type": "-", + "size": 24298, + "lmtime": 1741161477489, + "modified": false + }, + "l2_l_4_2.jpg": { + "type": "-", + "size": 20554, + "lmtime": 1741161477579, + "modified": false + }, + "l2_l_4_2.webp": { + "type": "-", + "size": 12050, + "lmtime": 1741161477660, + "modified": false + }, + "l2_l_4_3.jpg": { + "type": "-", + "size": 14562, + "lmtime": 1741161477725, + "modified": false + }, + "l2_l_4_3.webp": { + "type": "-", + "size": 6630, + "lmtime": 1741161477803, + "modified": false + }, + "l2_l_4_4.jpg": { + "type": "-", + "size": 3812, + "lmtime": 1741161477884, + "modified": false + }, + "l2_l_4_4.webp": { + "type": "-", + "size": 1342, + "lmtime": 1741161477960, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_l_1_1.jpg": { + "type": "-", + "size": 14743, + "lmtime": 1741161478039, + "modified": false + }, + "l3_l_1_1.webp": { + "type": "-", + "size": 4336, + "lmtime": 1741161478119, + "modified": false + }, + "l3_l_1_2.jpg": { + "type": "-", + "size": 13018, + "lmtime": 1741161478196, + "modified": false + }, + "l3_l_1_2.webp": { + "type": "-", + "size": 3644, + "lmtime": 1741161478299, + "modified": false + }, + "l3_l_1_3.jpg": { + "type": "-", + "size": 11472, + "lmtime": 1741161478379, + "modified": false + }, + "l3_l_1_3.webp": { + "type": "-", + "size": 3204, + "lmtime": 1741161478475, + "modified": false + }, + "l3_l_1_4.jpg": { + "type": "-", + "size": 10991, + "lmtime": 1741161478571, + "modified": false + }, + "l3_l_1_4.webp": { + "type": "-", + "size": 2636, + "lmtime": 1741161478678, + "modified": false + }, + "l3_l_1_5.jpg": { + "type": "-", + "size": 11615, + "lmtime": 1741161478791, + "modified": false + }, + "l3_l_1_5.webp": { + "type": "-", + "size": 2864, + "lmtime": 1741161478872, + "modified": false + }, + "l3_l_1_6.jpg": { + "type": "-", + "size": 13101, + "lmtime": 1741161478966, + "modified": false + }, + "l3_l_1_6.webp": { + "type": "-", + "size": 3024, + "lmtime": 1741161479043, + "modified": false + }, + "l3_l_1_7.jpg": { + "type": "-", + "size": 14693, + "lmtime": 1741161479122, + "modified": false + }, + "l3_l_1_7.webp": { + "type": "-", + "size": 3964, + "lmtime": 1741161479196, + "modified": false + }, + "l3_l_1_8.jpg": { + "type": "-", + "size": 7734, + "lmtime": 1741161479286, + "modified": false + }, + "l3_l_1_8.webp": { + "type": "-", + "size": 2324, + "lmtime": 1741161479364, + "modified": false + } + }, + "2": { + "l3_l_2_1.jpg": { + "type": "-", + "size": 16185, + "lmtime": 1741161479446, + "modified": false + }, + "l3_l_2_1.webp": { + "type": "-", + "size": 4480, + "lmtime": 1741161479532, + "modified": false + }, + "l3_l_2_2.jpg": { + "type": "-", + "size": 15804, + "lmtime": 1741161479641, + "modified": false + }, + "l3_l_2_2.webp": { + "type": "-", + "size": 4678, + "lmtime": 1741161479716, + "modified": false + }, + "l3_l_2_3.jpg": { + "type": "-", + "size": 13986, + "lmtime": 1741161479823, + "modified": false + }, + "l3_l_2_3.webp": { + "type": "-", + "size": 3530, + "lmtime": 1741161479909, + "modified": false + }, + "l3_l_2_4.jpg": { + "type": "-", + "size": 11724, + "lmtime": 1741161479992, + "modified": false + }, + "l3_l_2_4.webp": { + "type": "-", + "size": 2830, + "lmtime": 1741161480083, + "modified": false + }, + "l3_l_2_5.jpg": { + "type": "-", + "size": 11921, + "lmtime": 1741161480178, + "modified": false + }, + "l3_l_2_5.webp": { + "type": "-", + "size": 3276, + "lmtime": 1741161480271, + "modified": false + }, + "l3_l_2_6.jpg": { + "type": "-", + "size": 11835, + "lmtime": 1741161480368, + "modified": false + }, + "l3_l_2_6.webp": { + "type": "-", + "size": 2694, + "lmtime": 1741161480473, + "modified": false + }, + "l3_l_2_7.jpg": { + "type": "-", + "size": 11763, + "lmtime": 1741161480573, + "modified": false + }, + "l3_l_2_7.webp": { + "type": "-", + "size": 2522, + "lmtime": 1741161480672, + "modified": false + }, + "l3_l_2_8.jpg": { + "type": "-", + "size": 5846, + "lmtime": 1741161480763, + "modified": false + }, + "l3_l_2_8.webp": { + "type": "-", + "size": 1318, + "lmtime": 1741161480867, + "modified": false + } + }, + "3": { + "l3_l_3_1.jpg": { + "type": "-", + "size": 36058, + "lmtime": 1741161480961, + "modified": false + }, + "l3_l_3_1.webp": { + "type": "-", + "size": 18310, + "lmtime": 1741161481067, + "modified": false + }, + "l3_l_3_2.jpg": { + "type": "-", + "size": 36193, + "lmtime": 1741161481167, + "modified": false + }, + "l3_l_3_2.webp": { + "type": "-", + "size": 18460, + "lmtime": 1741161481269, + "modified": false + }, + "l3_l_3_3.jpg": { + "type": "-", + "size": 26781, + "lmtime": 1741161481358, + "modified": false + }, + "l3_l_3_3.webp": { + "type": "-", + "size": 12566, + "lmtime": 1741161481433, + "modified": false + }, + "l3_l_3_4.jpg": { + "type": "-", + "size": 11930, + "lmtime": 1741161481505, + "modified": false + }, + "l3_l_3_4.webp": { + "type": "-", + "size": 3926, + "lmtime": 1741161481578, + "modified": false + }, + "l3_l_3_5.jpg": { + "type": "-", + "size": 26605, + "lmtime": 1741161481674, + "modified": false + }, + "l3_l_3_5.webp": { + "type": "-", + "size": 11412, + "lmtime": 1741161481752, + "modified": false + }, + "l3_l_3_6.jpg": { + "type": "-", + "size": 27472, + "lmtime": 1741161481861, + "modified": false + }, + "l3_l_3_6.webp": { + "type": "-", + "size": 11718, + "lmtime": 1741161481939, + "modified": false + }, + "l3_l_3_7.jpg": { + "type": "-", + "size": 24960, + "lmtime": 1741161482026, + "modified": false + }, + "l3_l_3_7.webp": { + "type": "-", + "size": 10524, + "lmtime": 1741161482102, + "modified": false + }, + "l3_l_3_8.jpg": { + "type": "-", + "size": 4733, + "lmtime": 1741161482191, + "modified": false + }, + "l3_l_3_8.webp": { + "type": "-", + "size": 870, + "lmtime": 1741161482267, + "modified": false + } + }, + "4": { + "l3_l_4_1.jpg": { + "type": "-", + "size": 37982, + "lmtime": 1741161482361, + "modified": false + }, + "l3_l_4_1.webp": { + "type": "-", + "size": 21304, + "lmtime": 1741161482461, + "modified": false + }, + "l3_l_4_2.jpg": { + "type": "-", + "size": 28474, + "lmtime": 1741161482560, + "modified": false + }, + "l3_l_4_2.webp": { + "type": "-", + "size": 13176, + "lmtime": 1741161482630, + "modified": false + }, + "l3_l_4_3.jpg": { + "type": "-", + "size": 28159, + "lmtime": 1741161482728, + "modified": false + }, + "l3_l_4_3.webp": { + "type": "-", + "size": 13034, + "lmtime": 1741161482799, + "modified": false + }, + "l3_l_4_4.jpg": { + "type": "-", + "size": 11931, + "lmtime": 1741161482878, + "modified": false + }, + "l3_l_4_4.webp": { + "type": "-", + "size": 3956, + "lmtime": 1741161482944, + "modified": false + }, + "l3_l_4_5.jpg": { + "type": "-", + "size": 26913, + "lmtime": 1741161483038, + "modified": false + }, + "l3_l_4_5.webp": { + "type": "-", + "size": 11732, + "lmtime": 1741161483109, + "modified": false + }, + "l3_l_4_6.jpg": { + "type": "-", + "size": 32982, + "lmtime": 1741161483203, + "modified": false + }, + "l3_l_4_6.webp": { + "type": "-", + "size": 15432, + "lmtime": 1741161483285, + "modified": false + }, + "l3_l_4_7.jpg": { + "type": "-", + "size": 33701, + "lmtime": 1741161483385, + "modified": false + }, + "l3_l_4_7.webp": { + "type": "-", + "size": 17188, + "lmtime": 1741161483498, + "modified": false + }, + "l3_l_4_8.jpg": { + "type": "-", + "size": 5422, + "lmtime": 1741161483606, + "modified": false + }, + "l3_l_4_8.webp": { + "type": "-", + "size": 1222, + "lmtime": 1741161483703, + "modified": false + } + }, + "5": { + "l3_l_5_1.jpg": { + "type": "-", + "size": 18020, + "lmtime": 1741161483809, + "modified": false + }, + "l3_l_5_1.webp": { + "type": "-", + "size": 6122, + "lmtime": 1741161483949, + "modified": false + }, + "l3_l_5_2.jpg": { + "type": "-", + "size": 17866, + "lmtime": 1741161484065, + "modified": false + }, + "l3_l_5_2.webp": { + "type": "-", + "size": 6298, + "lmtime": 1741161484143, + "modified": false + }, + "l3_l_5_3.jpg": { + "type": "-", + "size": 25861, + "lmtime": 1741161484243, + "modified": false + }, + "l3_l_5_3.webp": { + "type": "-", + "size": 12212, + "lmtime": 1741161484339, + "modified": false + }, + "l3_l_5_4.jpg": { + "type": "-", + "size": 20897, + "lmtime": 1741161484453, + "modified": false + }, + "l3_l_5_4.webp": { + "type": "-", + "size": 8768, + "lmtime": 1741161484557, + "modified": false + }, + "l3_l_5_5.jpg": { + "type": "-", + "size": 31787, + "lmtime": 1741161484666, + "modified": false + }, + "l3_l_5_5.webp": { + "type": "-", + "size": 16830, + "lmtime": 1741161484759, + "modified": false + }, + "l3_l_5_6.jpg": { + "type": "-", + "size": 36102, + "lmtime": 1741161484881, + "modified": false + }, + "l3_l_5_6.webp": { + "type": "-", + "size": 21256, + "lmtime": 1741161485000, + "modified": false + }, + "l3_l_5_7.jpg": { + "type": "-", + "size": 38446, + "lmtime": 1741161485112, + "modified": false + }, + "l3_l_5_7.webp": { + "type": "-", + "size": 23530, + "lmtime": 1741161485218, + "modified": false + }, + "l3_l_5_8.jpg": { + "type": "-", + "size": 15879, + "lmtime": 1741161485330, + "modified": false + }, + "l3_l_5_8.webp": { + "type": "-", + "size": 8162, + "lmtime": 1741161485439, + "modified": false + } + }, + "6": { + "l3_l_6_1.jpg": { + "type": "-", + "size": 37406, + "lmtime": 1741161485561, + "modified": false + }, + "l3_l_6_1.webp": { + "type": "-", + "size": 19910, + "lmtime": 1741161485651, + "modified": false + }, + "l3_l_6_2.jpg": { + "type": "-", + "size": 30570, + "lmtime": 1741161485763, + "modified": false + }, + "l3_l_6_2.webp": { + "type": "-", + "size": 13710, + "lmtime": 1741161485860, + "modified": false + }, + "l3_l_6_3.jpg": { + "type": "-", + "size": 25432, + "lmtime": 1741161485967, + "modified": false + }, + "l3_l_6_3.webp": { + "type": "-", + "size": 11994, + "lmtime": 1741161486054, + "modified": false + }, + "l3_l_6_4.jpg": { + "type": "-", + "size": 21937, + "lmtime": 1741161486172, + "modified": false + }, + "l3_l_6_4.webp": { + "type": "-", + "size": 8658, + "lmtime": 1741161486266, + "modified": false + }, + "l3_l_6_5.jpg": { + "type": "-", + "size": 24881, + "lmtime": 1741161486366, + "modified": false + }, + "l3_l_6_5.webp": { + "type": "-", + "size": 10416, + "lmtime": 1741161486455, + "modified": false + }, + "l3_l_6_6.jpg": { + "type": "-", + "size": 22065, + "lmtime": 1741161486544, + "modified": false + }, + "l3_l_6_6.webp": { + "type": "-", + "size": 10324, + "lmtime": 1741161486618, + "modified": false + }, + "l3_l_6_7.jpg": { + "type": "-", + "size": 39782, + "lmtime": 1741161486716, + "modified": false + }, + "l3_l_6_7.webp": { + "type": "-", + "size": 24436, + "lmtime": 1741161486808, + "modified": false + }, + "l3_l_6_8.jpg": { + "type": "-", + "size": 13692, + "lmtime": 1741161486879, + "modified": false + }, + "l3_l_6_8.webp": { + "type": "-", + "size": 5936, + "lmtime": 1741161486951, + "modified": false + } + }, + "7": { + "l3_l_7_1.jpg": { + "type": "-", + "size": 41155, + "lmtime": 1741161487041, + "modified": false + }, + "l3_l_7_1.webp": { + "type": "-", + "size": 23640, + "lmtime": 1741161487132, + "modified": false + }, + "l3_l_7_2.jpg": { + "type": "-", + "size": 34104, + "lmtime": 1741161487221, + "modified": false + }, + "l3_l_7_2.webp": { + "type": "-", + "size": 19380, + "lmtime": 1741161487304, + "modified": false + }, + "l3_l_7_3.jpg": { + "type": "-", + "size": 33660, + "lmtime": 1741161487394, + "modified": false + }, + "l3_l_7_3.webp": { + "type": "-", + "size": 19502, + "lmtime": 1741161487485, + "modified": false + }, + "l3_l_7_4.jpg": { + "type": "-", + "size": 22269, + "lmtime": 1741161487578, + "modified": false + }, + "l3_l_7_4.webp": { + "type": "-", + "size": 10474, + "lmtime": 1741161487654, + "modified": false + }, + "l3_l_7_5.jpg": { + "type": "-", + "size": 29175, + "lmtime": 1741161487749, + "modified": false + }, + "l3_l_7_5.webp": { + "type": "-", + "size": 13908, + "lmtime": 1741161487819, + "modified": false + }, + "l3_l_7_6.jpg": { + "type": "-", + "size": 29708, + "lmtime": 1741161488075, + "modified": false + }, + "l3_l_7_6.webp": { + "type": "-", + "size": 15110, + "lmtime": 1741161488153, + "modified": false + }, + "l3_l_7_7.jpg": { + "type": "-", + "size": 18676, + "lmtime": 1741161488243, + "modified": false + }, + "l3_l_7_7.webp": { + "type": "-", + "size": 6968, + "lmtime": 1741161488312, + "modified": false + }, + "l3_l_7_8.jpg": { + "type": "-", + "size": 8679, + "lmtime": 1741161488387, + "modified": false + }, + "l3_l_7_8.webp": { + "type": "-", + "size": 2886, + "lmtime": 1741161488465, + "modified": false + } + }, + "8": { + "l3_l_8_1.jpg": { + "type": "-", + "size": 22771, + "lmtime": 1741161488569, + "modified": false + }, + "l3_l_8_1.webp": { + "type": "-", + "size": 15180, + "lmtime": 1741161488639, + "modified": false + }, + "l3_l_8_2.jpg": { + "type": "-", + "size": 29468, + "lmtime": 1741161488737, + "modified": false + }, + "l3_l_8_2.webp": { + "type": "-", + "size": 20212, + "lmtime": 1741161488825, + "modified": false + }, + "l3_l_8_3.jpg": { + "type": "-", + "size": 27862, + "lmtime": 1741161488913, + "modified": false + }, + "l3_l_8_3.webp": { + "type": "-", + "size": 18784, + "lmtime": 1741161489001, + "modified": false + }, + "l3_l_8_4.jpg": { + "type": "-", + "size": 24904, + "lmtime": 1741161489109, + "modified": false + }, + "l3_l_8_4.webp": { + "type": "-", + "size": 15012, + "lmtime": 1741161489199, + "modified": false + }, + "l3_l_8_5.jpg": { + "type": "-", + "size": 15104, + "lmtime": 1741161489264, + "modified": false + }, + "l3_l_8_5.webp": { + "type": "-", + "size": 8470, + "lmtime": 1741161489346, + "modified": false + }, + "l3_l_8_6.jpg": { + "type": "-", + "size": 18025, + "lmtime": 1741161489438, + "modified": false + }, + "l3_l_8_6.webp": { + "type": "-", + "size": 10368, + "lmtime": 1741161489508, + "modified": false + }, + "l3_l_8_7.jpg": { + "type": "-", + "size": 10621, + "lmtime": 1741161489589, + "modified": false + }, + "l3_l_8_7.webp": { + "type": "-", + "size": 4136, + "lmtime": 1741161489671, + "modified": false + }, + "l3_l_8_8.jpg": { + "type": "-", + "size": 4522, + "lmtime": 1741161489744, + "modified": false + }, + "l3_l_8_8.webp": { + "type": "-", + "size": 1500, + "lmtime": 1741161489816, + "modified": false + } + } + } + }, + "preview.jpg": { + "type": "-", + "size": 46152, + "lmtime": 1741161424177, + "modified": false + }, + "preview.webp": { + "type": "-", + "size": 23214, + "lmtime": 1741161424285, + "modified": false + }, + "r": { + "l1": { + "1": { + "l1_r_1_1.jpg": { + "type": "-", + "size": 17725, + "lmtime": 1741161489925, + "modified": false + }, + "l1_r_1_1.webp": { + "type": "-", + "size": 7752, + "lmtime": 1741161490001, + "modified": false + }, + "l1_r_1_2.jpg": { + "type": "-", + "size": 15635, + "lmtime": 1741161490071, + "modified": false + }, + "l1_r_1_2.webp": { + "type": "-", + "size": 7120, + "lmtime": 1741161490141, + "modified": false + } + }, + "2": { + "l1_r_2_1.jpg": { + "type": "-", + "size": 26817, + "lmtime": 1741161490238, + "modified": false + }, + "l1_r_2_1.webp": { + "type": "-", + "size": 13682, + "lmtime": 1741161490321, + "modified": false + }, + "l1_r_2_2.jpg": { + "type": "-", + "size": 24785, + "lmtime": 1741161490401, + "modified": false + }, + "l1_r_2_2.webp": { + "type": "-", + "size": 11128, + "lmtime": 1741161490473, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_r_1_1.jpg": { + "type": "-", + "size": 16737, + "lmtime": 1741161490574, + "modified": false + }, + "l2_r_1_1.webp": { + "type": "-", + "size": 6376, + "lmtime": 1741161490649, + "modified": false + }, + "l2_r_1_2.jpg": { + "type": "-", + "size": 11997, + "lmtime": 1741161490729, + "modified": false + }, + "l2_r_1_2.webp": { + "type": "-", + "size": 5094, + "lmtime": 1741161490813, + "modified": false + }, + "l2_r_1_3.jpg": { + "type": "-", + "size": 12251, + "lmtime": 1741161491192, + "modified": false + }, + "l2_r_1_3.webp": { + "type": "-", + "size": 5450, + "lmtime": 1741161491286, + "modified": false + }, + "l2_r_1_4.jpg": { + "type": "-", + "size": 5636, + "lmtime": 1741161491380, + "modified": false + }, + "l2_r_1_4.webp": { + "type": "-", + "size": 2672, + "lmtime": 1741161491474, + "modified": false + } + }, + "2": { + "l2_r_2_1.jpg": { + "type": "-", + "size": 14346, + "lmtime": 1741161491555, + "modified": false + }, + "l2_r_2_1.webp": { + "type": "-", + "size": 5518, + "lmtime": 1741161491635, + "modified": false + }, + "l2_r_2_2.jpg": { + "type": "-", + "size": 17537, + "lmtime": 1741161491841, + "modified": false + }, + "l2_r_2_2.webp": { + "type": "-", + "size": 7842, + "lmtime": 1741161491933, + "modified": false + }, + "l2_r_2_3.jpg": { + "type": "-", + "size": 17650, + "lmtime": 1741161492023, + "modified": false + }, + "l2_r_2_3.webp": { + "type": "-", + "size": 7438, + "lmtime": 1741161492106, + "modified": false + }, + "l2_r_2_4.jpg": { + "type": "-", + "size": 6516, + "lmtime": 1741161492184, + "modified": false + }, + "l2_r_2_4.webp": { + "type": "-", + "size": 2480, + "lmtime": 1741161492268, + "modified": false + } + }, + "3": { + "l2_r_3_1.jpg": { + "type": "-", + "size": 27311, + "lmtime": 1741161492379, + "modified": false + }, + "l2_r_3_1.webp": { + "type": "-", + "size": 14626, + "lmtime": 1741161492470, + "modified": false + }, + "l2_r_3_2.jpg": { + "type": "-", + "size": 14124, + "lmtime": 1741161492546, + "modified": false + }, + "l2_r_3_2.webp": { + "type": "-", + "size": 5478, + "lmtime": 1741161492629, + "modified": false + }, + "l2_r_3_3.jpg": { + "type": "-", + "size": 27444, + "lmtime": 1741161492724, + "modified": false + }, + "l2_r_3_3.webp": { + "type": "-", + "size": 13074, + "lmtime": 1741161492795, + "modified": false + }, + "l2_r_3_4.jpg": { + "type": "-", + "size": 7024, + "lmtime": 1741161492880, + "modified": false + }, + "l2_r_3_4.webp": { + "type": "-", + "size": 2518, + "lmtime": 1741161492954, + "modified": false + } + }, + "4": { + "l2_r_4_1.jpg": { + "type": "-", + "size": 14458, + "lmtime": 1741161493036, + "modified": false + }, + "l2_r_4_1.webp": { + "type": "-", + "size": 7554, + "lmtime": 1741161493117, + "modified": false + }, + "l2_r_4_2.jpg": { + "type": "-", + "size": 21447, + "lmtime": 1741161493217, + "modified": false + }, + "l2_r_4_2.webp": { + "type": "-", + "size": 15068, + "lmtime": 1741161493301, + "modified": false + }, + "l2_r_4_3.jpg": { + "type": "-", + "size": 16755, + "lmtime": 1741161493383, + "modified": false + }, + "l2_r_4_3.webp": { + "type": "-", + "size": 8614, + "lmtime": 1741161493457, + "modified": false + }, + "l2_r_4_4.jpg": { + "type": "-", + "size": 4624, + "lmtime": 1741161493534, + "modified": false + }, + "l2_r_4_4.webp": { + "type": "-", + "size": 1834, + "lmtime": 1741161493611, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_r_1_1.jpg": { + "type": "-", + "size": 29767, + "lmtime": 1741161493714, + "modified": false + }, + "l3_r_1_1.webp": { + "type": "-", + "size": 12992, + "lmtime": 1741161493789, + "modified": false + }, + "l3_r_1_2.jpg": { + "type": "-", + "size": 16212, + "lmtime": 1741161493880, + "modified": false + }, + "l3_r_1_2.webp": { + "type": "-", + "size": 5068, + "lmtime": 1741161493952, + "modified": false + }, + "l3_r_1_3.jpg": { + "type": "-", + "size": 10924, + "lmtime": 1741161494031, + "modified": false + }, + "l3_r_1_3.webp": { + "type": "-", + "size": 2470, + "lmtime": 1741161494104, + "modified": false + }, + "l3_r_1_4.jpg": { + "type": "-", + "size": 16202, + "lmtime": 1741161494190, + "modified": false + }, + "l3_r_1_4.webp": { + "type": "-", + "size": 6448, + "lmtime": 1741161494270, + "modified": false + }, + "l3_r_1_5.jpg": { + "type": "-", + "size": 32014, + "lmtime": 1741161494368, + "modified": false + }, + "l3_r_1_5.webp": { + "type": "-", + "size": 17986, + "lmtime": 1741161494460, + "modified": false + }, + "l3_r_1_6.jpg": { + "type": "-", + "size": 9430, + "lmtime": 1741161494539, + "modified": false + }, + "l3_r_1_6.webp": { + "type": "-", + "size": 2110, + "lmtime": 1741161494614, + "modified": false + }, + "l3_r_1_7.jpg": { + "type": "-", + "size": 8131, + "lmtime": 1741161494688, + "modified": false + }, + "l3_r_1_7.webp": { + "type": "-", + "size": 1472, + "lmtime": 1741161494767, + "modified": false + }, + "l3_r_1_8.jpg": { + "type": "-", + "size": 9002, + "lmtime": 1741161494848, + "modified": false + }, + "l3_r_1_8.webp": { + "type": "-", + "size": 3240, + "lmtime": 1741161494919, + "modified": false + } + }, + "2": { + "l3_r_2_1.jpg": { + "type": "-", + "size": 16904, + "lmtime": 1741161495011, + "modified": false + }, + "l3_r_2_1.webp": { + "type": "-", + "size": 5348, + "lmtime": 1741161495094, + "modified": false + }, + "l3_r_2_2.jpg": { + "type": "-", + "size": 16448, + "lmtime": 1741161495199, + "modified": false + }, + "l3_r_2_2.webp": { + "type": "-", + "size": 4984, + "lmtime": 1741161495281, + "modified": false + }, + "l3_r_2_3.jpg": { + "type": "-", + "size": 14999, + "lmtime": 1741161495358, + "modified": false + }, + "l3_r_2_3.webp": { + "type": "-", + "size": 4184, + "lmtime": 1741161495456, + "modified": false + }, + "l3_r_2_4.jpg": { + "type": "-", + "size": 7420, + "lmtime": 1741161495553, + "modified": false + }, + "l3_r_2_4.webp": { + "type": "-", + "size": 1932, + "lmtime": 1741161495630, + "modified": false + }, + "l3_r_2_5.jpg": { + "type": "-", + "size": 7148, + "lmtime": 1741161495719, + "modified": false + }, + "l3_r_2_5.webp": { + "type": "-", + "size": 2030, + "lmtime": 1741161495821, + "modified": false + }, + "l3_r_2_6.jpg": { + "type": "-", + "size": 7767, + "lmtime": 1741161495909, + "modified": false + }, + "l3_r_2_6.webp": { + "type": "-", + "size": 1812, + "lmtime": 1741161495983, + "modified": false + }, + "l3_r_2_7.jpg": { + "type": "-", + "size": 8951, + "lmtime": 1741161496061, + "modified": false + }, + "l3_r_2_7.webp": { + "type": "-", + "size": 2186, + "lmtime": 1741161496137, + "modified": false + }, + "l3_r_2_8.jpg": { + "type": "-", + "size": 7594, + "lmtime": 1741161496210, + "modified": false + }, + "l3_r_2_8.webp": { + "type": "-", + "size": 2564, + "lmtime": 1741161496281, + "modified": false + } + }, + "3": { + "l3_r_3_1.jpg": { + "type": "-", + "size": 12106, + "lmtime": 1741161496361, + "modified": false + }, + "l3_r_3_1.webp": { + "type": "-", + "size": 2316, + "lmtime": 1741161496431, + "modified": false + }, + "l3_r_3_2.jpg": { + "type": "-", + "size": 17472, + "lmtime": 1741161496523, + "modified": false + }, + "l3_r_3_2.webp": { + "type": "-", + "size": 5208, + "lmtime": 1741161496593, + "modified": false + }, + "l3_r_3_3.jpg": { + "type": "-", + "size": 17327, + "lmtime": 1741161496687, + "modified": false + }, + "l3_r_3_3.webp": { + "type": "-", + "size": 6182, + "lmtime": 1741161496770, + "modified": false + }, + "l3_r_3_4.jpg": { + "type": "-", + "size": 11521, + "lmtime": 1741161496842, + "modified": false + }, + "l3_r_3_4.webp": { + "type": "-", + "size": 3782, + "lmtime": 1741161496919, + "modified": false + }, + "l3_r_3_5.jpg": { + "type": "-", + "size": 10981, + "lmtime": 1741161496993, + "modified": false + }, + "l3_r_3_5.webp": { + "type": "-", + "size": 3654, + "lmtime": 1741161497069, + "modified": false + }, + "l3_r_3_6.jpg": { + "type": "-", + "size": 9478, + "lmtime": 1741161497140, + "modified": false + }, + "l3_r_3_6.webp": { + "type": "-", + "size": 2534, + "lmtime": 1741161497210, + "modified": false + }, + "l3_r_3_7.jpg": { + "type": "-", + "size": 10279, + "lmtime": 1741161497292, + "modified": false + }, + "l3_r_3_7.webp": { + "type": "-", + "size": 2566, + "lmtime": 1741161497362, + "modified": false + }, + "l3_r_3_8.jpg": { + "type": "-", + "size": 5923, + "lmtime": 1741161497443, + "modified": false + }, + "l3_r_3_8.webp": { + "type": "-", + "size": 1456, + "lmtime": 1741161497526, + "modified": false + } + }, + "4": { + "l3_r_4_1.jpg": { + "type": "-", + "size": 12635, + "lmtime": 1741161497594, + "modified": false + }, + "l3_r_4_1.webp": { + "type": "-", + "size": 2696, + "lmtime": 1741161497676, + "modified": false + }, + "l3_r_4_2.jpg": { + "type": "-", + "size": 22517, + "lmtime": 1741161497770, + "modified": false + }, + "l3_r_4_2.webp": { + "type": "-", + "size": 8422, + "lmtime": 1741161497844, + "modified": false + }, + "l3_r_4_3.jpg": { + "type": "-", + "size": 29314, + "lmtime": 1741161497944, + "modified": false + }, + "l3_r_4_3.webp": { + "type": "-", + "size": 13346, + "lmtime": 1741161498018, + "modified": false + }, + "l3_r_4_4.jpg": { + "type": "-", + "size": 7135, + "lmtime": 1741161498091, + "modified": false + }, + "l3_r_4_4.webp": { + "type": "-", + "size": 1902, + "lmtime": 1741161498170, + "modified": false + }, + "l3_r_4_5.jpg": { + "type": "-", + "size": 13310, + "lmtime": 1741161498250, + "modified": false + }, + "l3_r_4_5.webp": { + "type": "-", + "size": 4488, + "lmtime": 1741161498322, + "modified": false + }, + "l3_r_4_6.jpg": { + "type": "-", + "size": 13611, + "lmtime": 1741161498394, + "modified": false + }, + "l3_r_4_6.webp": { + "type": "-", + "size": 4458, + "lmtime": 1741161498468, + "modified": false + }, + "l3_r_4_7.jpg": { + "type": "-", + "size": 14029, + "lmtime": 1741161498545, + "modified": false + }, + "l3_r_4_7.webp": { + "type": "-", + "size": 4218, + "lmtime": 1741161498617, + "modified": false + }, + "l3_r_4_8.jpg": { + "type": "-", + "size": 6784, + "lmtime": 1741161498689, + "modified": false + }, + "l3_r_4_8.webp": { + "type": "-", + "size": 1844, + "lmtime": 1741161498762, + "modified": false + } + }, + "5": { + "l3_r_5_1.jpg": { + "type": "-", + "size": 15962, + "lmtime": 1741161498839, + "modified": false + }, + "l3_r_5_1.webp": { + "type": "-", + "size": 4774, + "lmtime": 1741161498922, + "modified": false + }, + "l3_r_5_2.jpg": { + "type": "-", + "size": 20377, + "lmtime": 1741161499008, + "modified": false + }, + "l3_r_5_2.webp": { + "type": "-", + "size": 7590, + "lmtime": 1741161499081, + "modified": false + }, + "l3_r_5_3.jpg": { + "type": "-", + "size": 19540, + "lmtime": 1741161499173, + "modified": false + }, + "l3_r_5_3.webp": { + "type": "-", + "size": 7160, + "lmtime": 1741161499244, + "modified": false + }, + "l3_r_5_4.jpg": { + "type": "-", + "size": 6735, + "lmtime": 1741161499333, + "modified": false + }, + "l3_r_5_4.webp": { + "type": "-", + "size": 1892, + "lmtime": 1741161499406, + "modified": false + }, + "l3_r_5_5.jpg": { + "type": "-", + "size": 27380, + "lmtime": 1741161499505, + "modified": false + }, + "l3_r_5_5.webp": { + "type": "-", + "size": 12342, + "lmtime": 1741161499577, + "modified": false + }, + "l3_r_5_6.jpg": { + "type": "-", + "size": 31973, + "lmtime": 1741161499673, + "modified": false + }, + "l3_r_5_6.webp": { + "type": "-", + "size": 15628, + "lmtime": 1741161499748, + "modified": false + }, + "l3_r_5_7.jpg": { + "type": "-", + "size": 19137, + "lmtime": 1741161499832, + "modified": false + }, + "l3_r_5_7.webp": { + "type": "-", + "size": 6550, + "lmtime": 1741161499908, + "modified": false + }, + "l3_r_5_8.jpg": { + "type": "-", + "size": 5796, + "lmtime": 1741161499983, + "modified": false + }, + "l3_r_5_8.webp": { + "type": "-", + "size": 1596, + "lmtime": 1741161500055, + "modified": false + } + }, + "6": { + "l3_r_6_1.jpg": { + "type": "-", + "size": 16443, + "lmtime": 1741161500143, + "modified": false + }, + "l3_r_6_1.webp": { + "type": "-", + "size": 5138, + "lmtime": 1741161500226, + "modified": false + }, + "l3_r_6_2.jpg": { + "type": "-", + "size": 39873, + "lmtime": 1741161500314, + "modified": false + }, + "l3_r_6_2.webp": { + "type": "-", + "size": 22596, + "lmtime": 1741161500400, + "modified": false + }, + "l3_r_6_3.jpg": { + "type": "-", + "size": 26731, + "lmtime": 1741161500499, + "modified": false + }, + "l3_r_6_3.webp": { + "type": "-", + "size": 11426, + "lmtime": 1741161500571, + "modified": false + }, + "l3_r_6_4.jpg": { + "type": "-", + "size": 6881, + "lmtime": 1741161500651, + "modified": false + }, + "l3_r_6_4.webp": { + "type": "-", + "size": 1868, + "lmtime": 1741161500733, + "modified": false + }, + "l3_r_6_5.jpg": { + "type": "-", + "size": 22287, + "lmtime": 1741161500819, + "modified": false + }, + "l3_r_6_5.webp": { + "type": "-", + "size": 8768, + "lmtime": 1741161500897, + "modified": false + }, + "l3_r_6_6.jpg": { + "type": "-", + "size": 27080, + "lmtime": 1741161500986, + "modified": false + }, + "l3_r_6_6.webp": { + "type": "-", + "size": 12456, + "lmtime": 1741161501065, + "modified": false + }, + "l3_r_6_7.jpg": { + "type": "-", + "size": 20776, + "lmtime": 1741161501163, + "modified": false + }, + "l3_r_6_7.webp": { + "type": "-", + "size": 8390, + "lmtime": 1741161501270, + "modified": false + }, + "l3_r_6_8.jpg": { + "type": "-", + "size": 6867, + "lmtime": 1741161501380, + "modified": false + }, + "l3_r_6_8.webp": { + "type": "-", + "size": 1888, + "lmtime": 1741161501481, + "modified": false + } + }, + "7": { + "l3_r_7_1.jpg": { + "type": "-", + "size": 29035, + "lmtime": 1741161501586, + "modified": false + }, + "l3_r_7_1.webp": { + "type": "-", + "size": 14386, + "lmtime": 1741161501684, + "modified": false + }, + "l3_r_7_2.jpg": { + "type": "-", + "size": 28005, + "lmtime": 1741161501794, + "modified": false + }, + "l3_r_7_2.webp": { + "type": "-", + "size": 14138, + "lmtime": 1741161501891, + "modified": false + }, + "l3_r_7_3.jpg": { + "type": "-", + "size": 30148, + "lmtime": 1741161501989, + "modified": false + }, + "l3_r_7_3.webp": { + "type": "-", + "size": 15328, + "lmtime": 1741161502118, + "modified": false + }, + "l3_r_7_4.jpg": { + "type": "-", + "size": 31631, + "lmtime": 1741161502227, + "modified": false + }, + "l3_r_7_4.webp": { + "type": "-", + "size": 17644, + "lmtime": 1741161502311, + "modified": false + }, + "l3_r_7_5.jpg": { + "type": "-", + "size": 36072, + "lmtime": 1741161502409, + "modified": false + }, + "l3_r_7_5.webp": { + "type": "-", + "size": 19676, + "lmtime": 1741161502499, + "modified": false + }, + "l3_r_7_6.jpg": { + "type": "-", + "size": 23492, + "lmtime": 1741161502597, + "modified": false + }, + "l3_r_7_6.webp": { + "type": "-", + "size": 9744, + "lmtime": 1741161502668, + "modified": false + }, + "l3_r_7_7.jpg": { + "type": "-", + "size": 18464, + "lmtime": 1741161502755, + "modified": false + }, + "l3_r_7_7.webp": { + "type": "-", + "size": 6738, + "lmtime": 1741161502838, + "modified": false + }, + "l3_r_7_8.jpg": { + "type": "-", + "size": 8240, + "lmtime": 1741161502919, + "modified": false + }, + "l3_r_7_8.webp": { + "type": "-", + "size": 2760, + "lmtime": 1741161503012, + "modified": false + } + }, + "8": { + "l3_r_8_1.jpg": { + "type": "-", + "size": 10056, + "lmtime": 1741161503092, + "modified": false + }, + "l3_r_8_1.webp": { + "type": "-", + "size": 4394, + "lmtime": 1741161503178, + "modified": false + }, + "l3_r_8_2.jpg": { + "type": "-", + "size": 7953, + "lmtime": 1741161503254, + "modified": false + }, + "l3_r_8_2.webp": { + "type": "-", + "size": 3166, + "lmtime": 1741161503334, + "modified": false + }, + "l3_r_8_3.jpg": { + "type": "-", + "size": 17559, + "lmtime": 1741161503432, + "modified": false + }, + "l3_r_8_3.webp": { + "type": "-", + "size": 10060, + "lmtime": 1741161503541, + "modified": false + }, + "l3_r_8_4.jpg": { + "type": "-", + "size": 22144, + "lmtime": 1741161503645, + "modified": false + }, + "l3_r_8_4.webp": { + "type": "-", + "size": 15518, + "lmtime": 1741161503723, + "modified": false + }, + "l3_r_8_5.jpg": { + "type": "-", + "size": 21216, + "lmtime": 1741161503832, + "modified": false + }, + "l3_r_8_5.webp": { + "type": "-", + "size": 13678, + "lmtime": 1741161503902, + "modified": false + }, + "l3_r_8_6.jpg": { + "type": "-", + "size": 15698, + "lmtime": 1741161503977, + "modified": false + }, + "l3_r_8_6.webp": { + "type": "-", + "size": 7766, + "lmtime": 1741161504048, + "modified": false + }, + "l3_r_8_7.jpg": { + "type": "-", + "size": 9348, + "lmtime": 1741161504125, + "modified": false + }, + "l3_r_8_7.webp": { + "type": "-", + "size": 3602, + "lmtime": 1741161504216, + "modified": false + }, + "l3_r_8_8.jpg": { + "type": "-", + "size": 4251, + "lmtime": 1741161504291, + "modified": false + }, + "l3_r_8_8.webp": { + "type": "-", + "size": 1570, + "lmtime": 1741161504370, + "modified": false + } + } + } + }, + "thumb.jpg": { + "type": "-", + "size": 12150, + "lmtime": 1741161424388, + "modified": false + }, + "thumb.webp": { + "type": "-", + "size": 8088, + "lmtime": 1741161424486, + "modified": false + }, + "u": { + "l1": { + "1": { + "l1_u_1_1.jpg": { + "type": "-", + "size": 14529, + "lmtime": 1741161504463, + "modified": false + }, + "l1_u_1_1.webp": { + "type": "-", + "size": 7336, + "lmtime": 1741161504536, + "modified": false + }, + "l1_u_1_2.jpg": { + "type": "-", + "size": 6232, + "lmtime": 1741161504612, + "modified": false + }, + "l1_u_1_2.webp": { + "type": "-", + "size": 2690, + "lmtime": 1741161504682, + "modified": false + } + }, + "2": { + "l1_u_2_1.jpg": { + "type": "-", + "size": 13257, + "lmtime": 1741161504765, + "modified": false + }, + "l1_u_2_1.webp": { + "type": "-", + "size": 6966, + "lmtime": 1741161504835, + "modified": false + }, + "l1_u_2_2.jpg": { + "type": "-", + "size": 7456, + "lmtime": 1741161504911, + "modified": false + }, + "l1_u_2_2.webp": { + "type": "-", + "size": 3102, + "lmtime": 1741161504989, + "modified": false + } + } + }, + "l2": { + "1": { + "l2_u_1_1.jpg": { + "type": "-", + "size": 7851, + "lmtime": 1741161505079, + "modified": false + }, + "l2_u_1_1.webp": { + "type": "-", + "size": 3154, + "lmtime": 1741161505154, + "modified": false + }, + "l2_u_1_2.jpg": { + "type": "-", + "size": 5992, + "lmtime": 1741161505233, + "modified": false + }, + "l2_u_1_2.webp": { + "type": "-", + "size": 2516, + "lmtime": 1741161505305, + "modified": false + }, + "l2_u_1_3.jpg": { + "type": "-", + "size": 4121, + "lmtime": 1741161505387, + "modified": false + }, + "l2_u_1_3.webp": { + "type": "-", + "size": 1572, + "lmtime": 1741161505456, + "modified": false + }, + "l2_u_1_4.jpg": { + "type": "-", + "size": 1803, + "lmtime": 1741161505531, + "modified": false + }, + "l2_u_1_4.webp": { + "type": "-", + "size": 598, + "lmtime": 1741161505613, + "modified": false + } + }, + "2": { + "l2_u_2_1.jpg": { + "type": "-", + "size": 14595, + "lmtime": 1741161505701, + "modified": false + }, + "l2_u_2_1.webp": { + "type": "-", + "size": 6656, + "lmtime": 1741161505773, + "modified": false + }, + "l2_u_2_2.jpg": { + "type": "-", + "size": 13872, + "lmtime": 1741161505850, + "modified": false + }, + "l2_u_2_2.webp": { + "type": "-", + "size": 6514, + "lmtime": 1741161505920, + "modified": false + }, + "l2_u_2_3.jpg": { + "type": "-", + "size": 3036, + "lmtime": 1741161506005, + "modified": false + }, + "l2_u_2_3.webp": { + "type": "-", + "size": 1102, + "lmtime": 1741161506071, + "modified": false + }, + "l2_u_2_4.jpg": { + "type": "-", + "size": 8648, + "lmtime": 1741161506145, + "modified": false + }, + "l2_u_2_4.webp": { + "type": "-", + "size": 4266, + "lmtime": 1741161506229, + "modified": false + } + }, + "3": { + "l2_u_3_1.jpg": { + "type": "-", + "size": 7201, + "lmtime": 1741161506315, + "modified": false + }, + "l2_u_3_1.webp": { + "type": "-", + "size": 2236, + "lmtime": 1741161506383, + "modified": false + }, + "l2_u_3_2.jpg": { + "type": "-", + "size": 6415, + "lmtime": 1741161506455, + "modified": false + }, + "l2_u_3_2.webp": { + "type": "-", + "size": 2112, + "lmtime": 1741161506524, + "modified": false + }, + "l2_u_3_3.jpg": { + "type": "-", + "size": 6061, + "lmtime": 1741161506600, + "modified": false + }, + "l2_u_3_3.webp": { + "type": "-", + "size": 1866, + "lmtime": 1741161506684, + "modified": false + }, + "l2_u_3_4.jpg": { + "type": "-", + "size": 3437, + "lmtime": 1741161506756, + "modified": false + }, + "l2_u_3_4.webp": { + "type": "-", + "size": 1082, + "lmtime": 1741161506827, + "modified": false + } + }, + "4": { + "l2_u_4_1.jpg": { + "type": "-", + "size": 16607, + "lmtime": 1741161506912, + "modified": false + }, + "l2_u_4_1.webp": { + "type": "-", + "size": 9186, + "lmtime": 1741161506990, + "modified": false + }, + "l2_u_4_2.jpg": { + "type": "-", + "size": 9013, + "lmtime": 1741161507068, + "modified": false + }, + "l2_u_4_2.webp": { + "type": "-", + "size": 3526, + "lmtime": 1741161507146, + "modified": false + }, + "l2_u_4_3.jpg": { + "type": "-", + "size": 4531, + "lmtime": 1741161507231, + "modified": false + }, + "l2_u_4_3.webp": { + "type": "-", + "size": 970, + "lmtime": 1741161507308, + "modified": false + }, + "l2_u_4_4.jpg": { + "type": "-", + "size": 5212, + "lmtime": 1741161507379, + "modified": false + }, + "l2_u_4_4.webp": { + "type": "-", + "size": 2198, + "lmtime": 1741161507454, + "modified": false + } + } + }, + "l3": { + "1": { + "l3_u_1_1.jpg": { + "type": "-", + "size": 9346, + "lmtime": 1741161507536, + "modified": false + }, + "l3_u_1_2.jpg": { + "type": "-", + "size": 9233, + "lmtime": 1741161507607, + "modified": false + }, + "l3_u_1_3.jpg": { + "type": "-", + "size": 8150, + "lmtime": 1741161507683, + "modified": false + }, + "l3_u_1_4.jpg": { + "type": "-", + "size": 8100, + "lmtime": 1741161507753, + "modified": false + }, + "l3_u_1_4.webp": { + "type": "-", + "size": 1426, + "lmtime": 1741161507831, + "modified": false + }, + "l3_u_1_5.jpg": { + "type": "-", + "size": 8764, + "lmtime": 1741161507905, + "modified": false + }, + "l3_u_1_5.webp": { + "type": "-", + "size": 1812, + "lmtime": 1741161507981, + "modified": false + }, + "l3_u_1_6.jpg": { + "type": "-", + "size": 8788, + "lmtime": 1741161508057, + "modified": false + }, + "l3_u_1_6.webp": { + "type": "-", + "size": 2138, + "lmtime": 1741161508127, + "modified": false + }, + "l3_u_1_7.jpg": { + "type": "-", + "size": 8291, + "lmtime": 1741161508210, + "modified": false + }, + "l3_u_1_7.webp": { + "type": "-", + "size": 1676, + "lmtime": 1741161508298, + "modified": false + }, + "l3_u_1_8.jpg": { + "type": "-", + "size": 4456, + "lmtime": 1741161508369, + "modified": false + }, + "l3_u_1_8.webp": { + "type": "-", + "size": 888, + "lmtime": 1741161508450, + "modified": false + } + }, + "2": { + "l3_u_2_1.jpg": { + "type": "-", + "size": 8978, + "lmtime": 1741161508520, + "modified": false + }, + "l3_u_2_2.jpg": { + "type": "-", + "size": 11467, + "lmtime": 1741161508599, + "modified": false + }, + "l3_u_2_3.jpg": { + "type": "-", + "size": 10252, + "lmtime": 1741161508672, + "modified": false + }, + "l3_u_2_4.jpg": { + "type": "-", + "size": 6603, + "lmtime": 1741161508749, + "modified": false + }, + "l3_u_2_4.webp": { + "type": "-", + "size": 1022, + "lmtime": 1741161508825, + "modified": false + }, + "l3_u_2_5.jpg": { + "type": "-", + "size": 5868, + "lmtime": 1741161508905, + "modified": false + }, + "l3_u_2_5.webp": { + "type": "-", + "size": 1162, + "lmtime": 1741161508977, + "modified": false + }, + "l3_u_2_6.jpg": { + "type": "-", + "size": 6407, + "lmtime": 1741161509048, + "modified": false + }, + "l3_u_2_6.webp": { + "type": "-", + "size": 1082, + "lmtime": 1741161509116, + "modified": false + }, + "l3_u_2_7.jpg": { + "type": "-", + "size": 7437, + "lmtime": 1741161509196, + "modified": false + }, + "l3_u_2_8.jpg": { + "type": "-", + "size": 4355, + "lmtime": 1741161509268, + "modified": false + } + }, + "3": { + "l3_u_3_1.jpg": { + "type": "-", + "size": 9022, + "lmtime": 1741161509348, + "modified": false + }, + "l3_u_3_2.jpg": { + "type": "-", + "size": 29842, + "lmtime": 1741161509443, + "modified": false + }, + "l3_u_3_3.jpg": { + "type": "-", + "size": 39569, + "lmtime": 1741161509543, + "modified": false + }, + "l3_u_3_4.jpg": { + "type": "-", + "size": 5214, + "lmtime": 1741161509614, + "modified": false + }, + "l3_u_3_5.jpg": { + "type": "-", + "size": 5027, + "lmtime": 1741161509688, + "modified": false + }, + "l3_u_3_5.webp": { + "type": "-", + "size": 1250, + "lmtime": 1741161509758, + "modified": false + }, + "l3_u_3_6.jpg": { + "type": "-", + "size": 5699, + "lmtime": 1741161509833, + "modified": false + }, + "l3_u_3_7.jpg": { + "type": "-", + "size": 6570, + "lmtime": 1741161509919, + "modified": false + }, + "l3_u_3_8.jpg": { + "type": "-", + "size": 12926, + "lmtime": 1741161509990, + "modified": false + } + }, + "4": { + "l3_u_4_1.jpg": { + "type": "-", + "size": 9356, + "lmtime": 1741161510073, + "modified": false + }, + "l3_u_4_2.jpg": { + "type": "-", + "size": 15742, + "lmtime": 1741161510159, + "modified": false + }, + "l3_u_4_3.jpg": { + "type": "-", + "size": 19228, + "lmtime": 1741161510257, + "modified": false + }, + "l3_u_4_3.webp": { + "type": "-", + "size": 9086, + "lmtime": 1741161510337, + "modified": false + }, + "l3_u_4_4.jpg": { + "type": "-", + "size": 4661, + "lmtime": 1741161510410, + "modified": false + }, + "l3_u_4_4.webp": { + "type": "-", + "size": 1106, + "lmtime": 1741161510493, + "modified": false + }, + "l3_u_4_5.jpg": { + "type": "-", + "size": 4452, + "lmtime": 1741161510568, + "modified": false + }, + "l3_u_4_5.webp": { + "type": "-", + "size": 946, + "lmtime": 1741161510638, + "modified": false + }, + "l3_u_4_6.jpg": { + "type": "-", + "size": 6497, + "lmtime": 1741161510724, + "modified": false + }, + "l3_u_4_6.webp": { + "type": "-", + "size": 1214, + "lmtime": 1741161510805, + "modified": false + }, + "l3_u_4_7.jpg": { + "type": "-", + "size": 6765, + "lmtime": 1741161510874, + "modified": false + }, + "l3_u_4_8.jpg": { + "type": "-", + "size": 14564, + "lmtime": 1741161510951, + "modified": false + } + }, + "5": { + "l3_u_5_1.jpg": { + "type": "-", + "size": 9691, + "lmtime": 1741161511025, + "modified": false + }, + "l3_u_5_1.webp": { + "type": "-", + "size": 2048, + "lmtime": 1741161511101, + "modified": false + }, + "l3_u_5_2.jpg": { + "type": "-", + "size": 6236, + "lmtime": 1741161511181, + "modified": false + }, + "l3_u_5_3.jpg": { + "type": "-", + "size": 5076, + "lmtime": 1741161511251, + "modified": false + }, + "l3_u_5_3.webp": { + "type": "-", + "size": 1430, + "lmtime": 1741161511327, + "modified": false + }, + "l3_u_5_4.jpg": { + "type": "-", + "size": 4700, + "lmtime": 1741161511393, + "modified": false + }, + "l3_u_5_4.webp": { + "type": "-", + "size": 1008, + "lmtime": 1741161511477, + "modified": false + }, + "l3_u_5_5.jpg": { + "type": "-", + "size": 4844, + "lmtime": 1741161511554, + "modified": false + }, + "l3_u_5_5.webp": { + "type": "-", + "size": 944, + "lmtime": 1741161511686, + "modified": false + }, + "l3_u_5_6.jpg": { + "type": "-", + "size": 6031, + "lmtime": 1741161511778, + "modified": false + }, + "l3_u_5_6.webp": { + "type": "-", + "size": 1098, + "lmtime": 1741161511867, + "modified": false + }, + "l3_u_5_7.jpg": { + "type": "-", + "size": 7341, + "lmtime": 1741161511957, + "modified": false + }, + "l3_u_5_7.webp": { + "type": "-", + "size": 1050, + "lmtime": 1741161512046, + "modified": false + }, + "l3_u_5_8.jpg": { + "type": "-", + "size": 4206, + "lmtime": 1741161512139, + "modified": false + } + }, + "6": { + "l3_u_6_1.jpg": { + "type": "-", + "size": 14836, + "lmtime": 1741161512219, + "modified": false + }, + "l3_u_6_1.webp": { + "type": "-", + "size": 3854, + "lmtime": 1741161512283, + "modified": false + }, + "l3_u_6_2.jpg": { + "type": "-", + "size": 16595, + "lmtime": 1741161512371, + "modified": false + }, + "l3_u_6_2.webp": { + "type": "-", + "size": 4780, + "lmtime": 1741161512443, + "modified": false + }, + "l3_u_6_3.jpg": { + "type": "-", + "size": 13739, + "lmtime": 1741161512529, + "modified": false + }, + "l3_u_6_3.webp": { + "type": "-", + "size": 3164, + "lmtime": 1741161512603, + "modified": false + }, + "l3_u_6_4.jpg": { + "type": "-", + "size": 13507, + "lmtime": 1741161512673, + "modified": false + }, + "l3_u_6_4.webp": { + "type": "-", + "size": 3448, + "lmtime": 1741161512750, + "modified": false + }, + "l3_u_6_5.jpg": { + "type": "-", + "size": 13595, + "lmtime": 1741161512826, + "modified": false + }, + "l3_u_6_5.webp": { + "type": "-", + "size": 3374, + "lmtime": 1741161512899, + "modified": false + }, + "l3_u_6_6.jpg": { + "type": "-", + "size": 12236, + "lmtime": 1741161512975, + "modified": false + }, + "l3_u_6_6.webp": { + "type": "-", + "size": 2802, + "lmtime": 1741161513041, + "modified": false + }, + "l3_u_6_7.jpg": { + "type": "-", + "size": 12412, + "lmtime": 1741161513112, + "modified": false + }, + "l3_u_6_7.webp": { + "type": "-", + "size": 2794, + "lmtime": 1741161513190, + "modified": false + }, + "l3_u_6_8.jpg": { + "type": "-", + "size": 6679, + "lmtime": 1741161513259, + "modified": false + }, + "l3_u_6_8.webp": { + "type": "-", + "size": 1662, + "lmtime": 1741161513333, + "modified": false + } + }, + "7": { + "l3_u_7_1.jpg": { + "type": "-", + "size": 15818, + "lmtime": 1741161513403, + "modified": false + }, + "l3_u_7_1.webp": { + "type": "-", + "size": 4184, + "lmtime": 1741161513475, + "modified": false + }, + "l3_u_7_2.jpg": { + "type": "-", + "size": 26232, + "lmtime": 1741161513563, + "modified": false + }, + "l3_u_7_2.webp": { + "type": "-", + "size": 11882, + "lmtime": 1741161513639, + "modified": false + }, + "l3_u_7_3.jpg": { + "type": "-", + "size": 20022, + "lmtime": 1741161513726, + "modified": false + }, + "l3_u_7_3.webp": { + "type": "-", + "size": 7324, + "lmtime": 1741161513832, + "modified": false + }, + "l3_u_7_4.jpg": { + "type": "-", + "size": 17073, + "lmtime": 1741161513916, + "modified": false + }, + "l3_u_7_4.webp": { + "type": "-", + "size": 5358, + "lmtime": 1741161514014, + "modified": false + }, + "l3_u_7_5.jpg": { + "type": "-", + "size": 18375, + "lmtime": 1741161514129, + "modified": false + }, + "l3_u_7_5.webp": { + "type": "-", + "size": 6710, + "lmtime": 1741161514213, + "modified": false + }, + "l3_u_7_6.jpg": { + "type": "-", + "size": 15413, + "lmtime": 1741161514317, + "modified": false + }, + "l3_u_7_6.webp": { + "type": "-", + "size": 3968, + "lmtime": 1741161514402, + "modified": false + }, + "l3_u_7_7.jpg": { + "type": "-", + "size": 15615, + "lmtime": 1741161514500, + "modified": false + }, + "l3_u_7_7.webp": { + "type": "-", + "size": 3966, + "lmtime": 1741161514588, + "modified": false + }, + "l3_u_7_8.jpg": { + "type": "-", + "size": 8385, + "lmtime": 1741161514689, + "modified": false + }, + "l3_u_7_8.webp": { + "type": "-", + "size": 2610, + "lmtime": 1741161514795, + "modified": false + } + }, + "8": { + "l3_u_8_1.jpg": { + "type": "-", + "size": 9163, + "lmtime": 1741161514878, + "modified": false + }, + "l3_u_8_1.webp": { + "type": "-", + "size": 3072, + "lmtime": 1741161514947, + "modified": false + }, + "l3_u_8_2.jpg": { + "type": "-", + "size": 24579, + "lmtime": 1741161515035, + "modified": false + }, + "l3_u_8_2.webp": { + "type": "-", + "size": 16560, + "lmtime": 1741161515125, + "modified": false + }, + "l3_u_8_3.jpg": { + "type": "-", + "size": 16491, + "lmtime": 1741161515209, + "modified": false + }, + "l3_u_8_3.webp": { + "type": "-", + "size": 8640, + "lmtime": 1741161515290, + "modified": false + }, + "l3_u_8_4.jpg": { + "type": "-", + "size": 9102, + "lmtime": 1741161515373, + "modified": false + }, + "l3_u_8_4.webp": { + "type": "-", + "size": 3184, + "lmtime": 1741161515452, + "modified": false + }, + "l3_u_8_5.jpg": { + "type": "-", + "size": 9400, + "lmtime": 1741161515529, + "modified": false + }, + "l3_u_8_5.webp": { + "type": "-", + "size": 3692, + "lmtime": 1741161515622, + "modified": false + }, + "l3_u_8_6.jpg": { + "type": "-", + "size": 8269, + "lmtime": 1741161515713, + "modified": false + }, + "l3_u_8_6.webp": { + "type": "-", + "size": 2568, + "lmtime": 1741161515800, + "modified": false + }, + "l3_u_8_7.jpg": { + "type": "-", + "size": 10156, + "lmtime": 1741161515882, + "modified": false + }, + "l3_u_8_7.webp": { + "type": "-", + "size": 3726, + "lmtime": 1741161515969, + "modified": false + }, + "l3_u_8_8.jpg": { + "type": "-", + "size": 7450, + "lmtime": 1741161516051, + "modified": false + }, + "l3_u_8_8.webp": { + "type": "-", + "size": 3572, + "lmtime": 1741161516138, + "modified": false + } + } + } + } + } + }, + "plugins": { + "bingmaps.js": { + "type": "-", + "size": 24338, + "lmtime": 1741161516248, + "modified": false + }, + "bingmaps.swf": { + "type": "-", + "size": 134656, + "lmtime": 1741161516383, + "modified": false + }, + "combobox.xml": { + "type": "-", + "size": 20360, + "lmtime": 1741161516481, + "modified": false + }, + "googlemaps.js": { + "type": "-", + "size": 21032, + "lmtime": 1741161516565, + "modified": false + }, + "gyro2.js": { + "type": "-", + "size": 13036, + "lmtime": 1741161516661, + "modified": false + }, + "scrollarea.js": { + "type": "-", + "size": 7327, + "lmtime": 1741161516757, + "modified": false + }, + "scrollarea.swf": { + "type": "-", + "size": 5649, + "lmtime": 1741161516849, + "modified": false + }, + "soundinterface.js": { + "type": "-", + "size": 3060, + "lmtime": 1741161516934, + "modified": false + }, + "soundinterface.swf": { + "type": "-", + "size": 5208, + "lmtime": 1741161517027, + "modified": false + }, + "videoplayer.js": { + "type": "-", + "size": 8818, + "lmtime": 1741161517135, + "modified": false + }, + "videoplayer.swf": { + "type": "-", + "size": 7536, + "lmtime": 1741161517244, + "modified": false + }, + "webvr_cursor_80x80_17f.png": { + "type": "-", + "size": 5251, + "lmtime": 1741161517351, + "modified": false + }, + "webvr_cursor_80x80_17f.webp": { + "type": "-", + "size": 1786, + "lmtime": 1741161517448, + "modified": false + }, + "webvr.js": { + "type": "-", + "size": 36276, + "lmtime": 1741161517561, + "modified": false + }, + "webvr.xml": { + "type": "-", + "size": 33474, + "lmtime": 1741161517673, + "modified": false + } + }, + "skin": { + "rotate_device.png": { + "type": "-", + "size": 2694, + "lmtime": 1741161517775, + "modified": false + }, + "rotate_device.webp": { + "type": "-", + "size": 1158, + "lmtime": 1741161517874, + "modified": false + }, + "vtourskin_design_117round.xml": { + "type": "-", + "size": 1257, + "lmtime": 1741161517980, + "modified": false + }, + "vtourskin_design_117.xml": { + "type": "-", + "size": 1087, + "lmtime": 1741161518091, + "modified": false + }, + "vtourskin_design_black.xml": { + "type": "-", + "size": 3081, + "lmtime": 1741161518182, + "modified": false + }, + "vtourskin_design_flat_light.xml": { + "type": "-", + "size": 1949, + "lmtime": 1741161518284, + "modified": false + }, + "vtourskin_design_glass.xml": { + "type": "-", + "size": 1824, + "lmtime": 1741161518383, + "modified": false + }, + "vtourskin_design_ultra_light.xml": { + "type": "-", + "size": 2373, + "lmtime": 1741161518491, + "modified": false + }, + "vtourskin_hotspot.png": { + "type": "-", + "size": 9728, + "lmtime": 1741161518591, + "modified": false + }, + "vtourskin_light.png": { + "type": "-", + "size": 10920, + "lmtime": 1741161518681, + "modified": false + }, + "vtourskin_mapspotactive.png": { + "type": "-", + "size": 7223, + "lmtime": 1741161518763, + "modified": false + }, + "vtourskin_mapspot.png": { + "type": "-", + "size": 7115, + "lmtime": 1741161518846, + "modified": false + }, + "vtourskin.png": { + "type": "-", + "size": 13966, + "lmtime": 1741161518946, + "modified": false + }, + "vtourskin.webp": { + "type": "-", + "size": 8292, + "lmtime": 1741161519033, + "modified": false + }, + "vtourskin.xml": { + "type": "-", + "size": 56805, + "lmtime": 1741161519153, + "modified": false + } + }, + "tour.js": { + "type": "-", + "size": 161032, + "lmtime": 1741161246046, + "modified": false + }, + "tour.swf": { + "type": "-", + "size": 116793, + "lmtime": 1741161246173, + "modified": false + }, + "tour.xml": { + "type": "-", + "size": 5853, + "lmtime": 1741161246263, + "modified": false + } + }, + "drmateraczamosc": { + "build0.xml": { + "type": "-", + "size": 3940, + "lmtime": 1741161519239, + "modified": false + }, + "build1.xml": { + "type": "-", + "size": 5518, + "lmtime": 1741161519339, + "modified": false + }, + "build2.xml": { + "type": "-", + "size": 3933, + "lmtime": 1741161519432, + "modified": false + }, + "builddata": { + "build0": { + "0": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 160323, + "lmtime": 1741161520888, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 139114, + "lmtime": 1741161521018, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 73526, + "lmtime": 1741161521142, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 141862, + "lmtime": 1741161521280, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 77682, + "lmtime": 1741161521392, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 148812, + "lmtime": 1741161521519, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 92404, + "lmtime": 1741161521636, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 140391, + "lmtime": 1741161521769, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 91670, + "lmtime": 1741161521880, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 108408, + "lmtime": 1741161522004, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 115277, + "lmtime": 1741161522121, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 124872, + "lmtime": 1741161522242, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 104848, + "lmtime": 1741161522368, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 127858, + "lmtime": 1741161522485, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 125717, + "lmtime": 1741161522614, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 123199, + "lmtime": 1741161522746, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 101855, + "lmtime": 1741161522876, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 165362, + "lmtime": 1741161523007, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 118424, + "lmtime": 1741161523124, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 112461, + "lmtime": 1741161523242, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 113418, + "lmtime": 1741161523355, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 144767, + "lmtime": 1741161523476, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 86029, + "lmtime": 1741161523588, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 79196, + "lmtime": 1741161523694, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 172863, + "lmtime": 1741161523826, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 68445, + "lmtime": 1741161520492, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 214305, + "lmtime": 1741161520632, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 158942, + "lmtime": 1741161520761, + "modified": false + } + }, + "1": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 172071, + "lmtime": 1741161524337, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 157598, + "lmtime": 1741161524471, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 92272, + "lmtime": 1741161524596, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 148594, + "lmtime": 1741161524718, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 92096, + "lmtime": 1741161524832, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 111182, + "lmtime": 1741161524943, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 56016, + "lmtime": 1741161525053, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 193885, + "lmtime": 1741161525191, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 145186, + "lmtime": 1741161525319, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 117604, + "lmtime": 1741161525479, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 160320, + "lmtime": 1741161525602, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 131811, + "lmtime": 1741161525741, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 90767, + "lmtime": 1741161525873, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 123761, + "lmtime": 1741161525999, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 107299, + "lmtime": 1741161526131, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 129781, + "lmtime": 1741161526246, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 113674, + "lmtime": 1741161526363, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 112361, + "lmtime": 1741161526475, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 115717, + "lmtime": 1741161526590, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 148570, + "lmtime": 1741161526718, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 150091, + "lmtime": 1741161526848, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 106710, + "lmtime": 1741161526967, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 53033, + "lmtime": 1741161527066, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 149879, + "lmtime": 1741161527199, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 186531, + "lmtime": 1741161527380, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 72563, + "lmtime": 1741161523935, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 229497, + "lmtime": 1741161524067, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 174762, + "lmtime": 1741161524203, + "modified": false + } + }, + "2": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 130903, + "lmtime": 1741161527903, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 128711, + "lmtime": 1741161528036, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 66444, + "lmtime": 1741161528168, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 83346, + "lmtime": 1741161528296, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 36712, + "lmtime": 1741161528421, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 122516, + "lmtime": 1741161528551, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 69840, + "lmtime": 1741161528653, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 114924, + "lmtime": 1741161528781, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 58272, + "lmtime": 1741161528895, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 108236, + "lmtime": 1741161529018, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 95084, + "lmtime": 1741161529144, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 55340, + "lmtime": 1741161529250, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 37103, + "lmtime": 1741161529363, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 110719, + "lmtime": 1741161529487, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 90804, + "lmtime": 1741161529625, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 103072, + "lmtime": 1741161529758, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 83356, + "lmtime": 1741161529883, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 131911, + "lmtime": 1741161530010, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 117331, + "lmtime": 1741161530143, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 139908, + "lmtime": 1741161530286, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 116143, + "lmtime": 1741161530424, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 120026, + "lmtime": 1741161530551, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 52285, + "lmtime": 1741161530665, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 55367, + "lmtime": 1741161530784, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 100780, + "lmtime": 1741161530912, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 57012, + "lmtime": 1741161527501, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 166713, + "lmtime": 1741161527625, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 103670, + "lmtime": 1741161527767, + "modified": false + } + }, + "3": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 125709, + "lmtime": 1741161531391, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 81065, + "lmtime": 1741161531507, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 29854, + "lmtime": 1741161531604, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 103620, + "lmtime": 1741161531729, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 46392, + "lmtime": 1741161531840, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 130569, + "lmtime": 1741161531976, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 71554, + "lmtime": 1741161532085, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 136688, + "lmtime": 1741161532218, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 79362, + "lmtime": 1741161532336, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 44310, + "lmtime": 1741161532442, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 76096, + "lmtime": 1741161532579, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 100968, + "lmtime": 1741161532725, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 105414, + "lmtime": 1741161532845, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 73234, + "lmtime": 1741161532954, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 85321, + "lmtime": 1741161533070, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 102879, + "lmtime": 1741161533188, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 109817, + "lmtime": 1741161533318, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 114807, + "lmtime": 1741161533460, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 121573, + "lmtime": 1741161533599, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 97316, + "lmtime": 1741161533735, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 107299, + "lmtime": 1741161533867, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 107440, + "lmtime": 1741161534009, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 102405, + "lmtime": 1741161534141, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 122592, + "lmtime": 1741161534280, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 136742, + "lmtime": 1741161534401, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 54243, + "lmtime": 1741161531018, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 165250, + "lmtime": 1741161531149, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 99460, + "lmtime": 1741161531264, + "modified": false + } + }, + "4": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 162545, + "lmtime": 1741161534985, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 109176, + "lmtime": 1741161535118, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 48208, + "lmtime": 1741161535260, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 159815, + "lmtime": 1741161535408, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 96538, + "lmtime": 1741161535543, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 149972, + "lmtime": 1741161535712, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 81572, + "lmtime": 1741161535832, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 120189, + "lmtime": 1741161535961, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 49418, + "lmtime": 1741161536094, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 54394, + "lmtime": 1741161536208, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 96794, + "lmtime": 1741161536347, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 121264, + "lmtime": 1741161536470, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 135245, + "lmtime": 1741161536597, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 102561, + "lmtime": 1741161536732, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 103847, + "lmtime": 1741161536851, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 128397, + "lmtime": 1741161536994, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 126215, + "lmtime": 1741161537153, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 137035, + "lmtime": 1741161537281, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 124333, + "lmtime": 1741161537410, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 83240, + "lmtime": 1741161537533, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 100349, + "lmtime": 1741161537655, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 122196, + "lmtime": 1741161537797, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 109234, + "lmtime": 1741161537934, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 100155, + "lmtime": 1741161538056, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 102634, + "lmtime": 1741161538218, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 69283, + "lmtime": 1741161534538, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 205614, + "lmtime": 1741161534681, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 133954, + "lmtime": 1741161534833, + "modified": false + } + }, + "5": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 176430, + "lmtime": 1741161538795, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 210948, + "lmtime": 1741161538959, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 182274, + "lmtime": 1741161539107, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 188575, + "lmtime": 1741161539254, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 153600, + "lmtime": 1741161539397, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 161324, + "lmtime": 1741161539550, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 113876, + "lmtime": 1741161539678, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 191959, + "lmtime": 1741161539840, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 155854, + "lmtime": 1741161539993, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 186540, + "lmtime": 1741161540139, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 159104, + "lmtime": 1741161540298, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 187483, + "lmtime": 1741161540511, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 163671, + "lmtime": 1741161540667, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 171111, + "lmtime": 1741161540814, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 211001, + "lmtime": 1741161540952, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 216072, + "lmtime": 1741161541101, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 91211, + "lmtime": 1741161541228, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 110623, + "lmtime": 1741161541378, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 183867, + "lmtime": 1741161541515, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 194166, + "lmtime": 1741161541655, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 193217, + "lmtime": 1741161541813, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 146066, + "lmtime": 1741161541965, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 108742, + "lmtime": 1741161542086, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 95225, + "lmtime": 1741161542225, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 179988, + "lmtime": 1741161542377, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 62862, + "lmtime": 1741161538342, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 262491, + "lmtime": 1741161538502, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 247548, + "lmtime": 1741161538643, + "modified": false + } + }, + "preview.jpg": { + "type": "-", + "size": 65936, + "lmtime": 1741161520224, + "modified": false + }, + "preview.webp": { + "type": "-", + "size": 53818, + "lmtime": 1741161520353, + "modified": false + } + }, + "build1": { + "0": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 147565, + "lmtime": 1741161543300, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 125882, + "lmtime": 1741161543442, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 65584, + "lmtime": 1741161543548, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 127153, + "lmtime": 1741161543681, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 70138, + "lmtime": 1741161543797, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 110181, + "lmtime": 1741161543933, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 55688, + "lmtime": 1741161544054, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 172658, + "lmtime": 1741161544188, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 132284, + "lmtime": 1741161544326, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 105703, + "lmtime": 1741161544460, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 118380, + "lmtime": 1741161544589, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 111030, + "lmtime": 1741161544713, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 113748, + "lmtime": 1741161544842, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 76385, + "lmtime": 1741161544981, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 121369, + "lmtime": 1741161545129, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 112274, + "lmtime": 1741161545246, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 72042, + "lmtime": 1741161545365, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 101981, + "lmtime": 1741161545509, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 110166, + "lmtime": 1741161545642, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 129707, + "lmtime": 1741161545763, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 110041, + "lmtime": 1741161545892, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 79848, + "lmtime": 1741161546011, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 86772, + "lmtime": 1741161546125, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 197582, + "lmtime": 1741161546270, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 155978, + "lmtime": 1741161546424, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 62795, + "lmtime": 1741161542771, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 38010, + "lmtime": 1741161542885, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 197768, + "lmtime": 1741161543026, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 144492, + "lmtime": 1741161543176, + "modified": false + } + }, + "1": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 148709, + "lmtime": 1741161547094, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 131884, + "lmtime": 1741161547234, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 66146, + "lmtime": 1741161547350, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 132227, + "lmtime": 1741161547499, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 75140, + "lmtime": 1741161547635, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 118874, + "lmtime": 1741161547788, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 63404, + "lmtime": 1741161547912, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 144036, + "lmtime": 1741161548049, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 93476, + "lmtime": 1741161548171, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 108955, + "lmtime": 1741161548300, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 118609, + "lmtime": 1741161548425, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 121845, + "lmtime": 1741161548562, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 140397, + "lmtime": 1741161548702, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 85491, + "lmtime": 1741161548841, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 113077, + "lmtime": 1741161549000, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 90979, + "lmtime": 1741161549128, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 93385, + "lmtime": 1741161549267, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 101249, + "lmtime": 1741161549404, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 103138, + "lmtime": 1741161549547, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 110573, + "lmtime": 1741161549686, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 111536, + "lmtime": 1741161549803, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 99104, + "lmtime": 1741161549949, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 105905, + "lmtime": 1741161550088, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 152271, + "lmtime": 1741161550228, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 131137, + "lmtime": 1741161550351, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 63824, + "lmtime": 1741161546546, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 37944, + "lmtime": 1741161546660, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 195081, + "lmtime": 1741161546823, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 135024, + "lmtime": 1741161546959, + "modified": false + } + }, + "2": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 138745, + "lmtime": 1741161550990, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 95798, + "lmtime": 1741161551122, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 40650, + "lmtime": 1741161551219, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 119608, + "lmtime": 1741161551349, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 62274, + "lmtime": 1741161551453, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 107274, + "lmtime": 1741161551577, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 50550, + "lmtime": 1741161551698, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 190150, + "lmtime": 1741161551864, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 152588, + "lmtime": 1741161552011, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 98812, + "lmtime": 1741161552143, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 43082, + "lmtime": 1741161552242, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 90823, + "lmtime": 1741161552371, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 114162, + "lmtime": 1741161552505, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 99721, + "lmtime": 1741161552636, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 96778, + "lmtime": 1741161552774, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 99747, + "lmtime": 1741161552910, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 102231, + "lmtime": 1741161553030, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 105357, + "lmtime": 1741161553151, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 82470, + "lmtime": 1741161553287, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 140144, + "lmtime": 1741161553415, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 128723, + "lmtime": 1741161553538, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 85302, + "lmtime": 1741161553656, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 110730, + "lmtime": 1741161553772, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 201034, + "lmtime": 1741161553901, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 177988, + "lmtime": 1741161554047, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 57654, + "lmtime": 1741161550471, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 33194, + "lmtime": 1741161550580, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 186127, + "lmtime": 1741161550746, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 130936, + "lmtime": 1741161550867, + "modified": false + } + }, + "3": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 154919, + "lmtime": 1741161554713, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 119337, + "lmtime": 1741161554843, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 55828, + "lmtime": 1741161554957, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 143706, + "lmtime": 1741161555090, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 87820, + "lmtime": 1741161555219, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 144246, + "lmtime": 1741161555367, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 89604, + "lmtime": 1741161555544, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 130871, + "lmtime": 1741161555674, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 73196, + "lmtime": 1741161555817, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 87680, + "lmtime": 1741161555940, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 91362, + "lmtime": 1741161556058, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 133823, + "lmtime": 1741161556191, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 122094, + "lmtime": 1741161556323, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 105910, + "lmtime": 1741161556468, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 94482, + "lmtime": 1741161556596, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 104603, + "lmtime": 1741161556730, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 90297, + "lmtime": 1741161556868, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 103164, + "lmtime": 1741161557009, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 110307, + "lmtime": 1741161557151, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 118393, + "lmtime": 1741161557280, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 113814, + "lmtime": 1741161557407, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 157611, + "lmtime": 1741161557533, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 116434, + "lmtime": 1741161557661, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 98151, + "lmtime": 1741161557783, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 121920, + "lmtime": 1741161557923, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 65922, + "lmtime": 1741161554170, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 41040, + "lmtime": 1741161554271, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 200616, + "lmtime": 1741161554419, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 142944, + "lmtime": 1741161554559, + "modified": false + } + }, + "4": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 149218, + "lmtime": 1741161558556, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 119169, + "lmtime": 1741161558702, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 63078, + "lmtime": 1741161558841, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 138542, + "lmtime": 1741161558976, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 87700, + "lmtime": 1741161559101, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 143784, + "lmtime": 1741161559240, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 89358, + "lmtime": 1741161559379, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 109164, + "lmtime": 1741161559527, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 57074, + "lmtime": 1741161559663, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 90450, + "lmtime": 1741161559794, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 88646, + "lmtime": 1741161559918, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 88331, + "lmtime": 1741161560042, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 138447, + "lmtime": 1741161560180, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 83326, + "lmtime": 1741161560313, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 136828, + "lmtime": 1741161560454, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 136527, + "lmtime": 1741161560608, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 102875, + "lmtime": 1741161560749, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 150979, + "lmtime": 1741161560881, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 125089, + "lmtime": 1741161561017, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 82180, + "lmtime": 1741161561141, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 81252, + "lmtime": 1741161561273, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 118450, + "lmtime": 1741161561401, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 82593, + "lmtime": 1741161561530, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 101391, + "lmtime": 1741161561654, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 80698, + "lmtime": 1741161561776, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 63834, + "lmtime": 1741161558052, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 41758, + "lmtime": 1741161558158, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 192482, + "lmtime": 1741161558290, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 142364, + "lmtime": 1741161558416, + "modified": false + } + }, + "5": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 198936, + "lmtime": 1741161562534, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 196346, + "lmtime": 1741161562679, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 167386, + "lmtime": 1741161562832, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 199690, + "lmtime": 1741161562997, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 167432, + "lmtime": 1741161563138, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 252202, + "lmtime": 1741161563300, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 243892, + "lmtime": 1741161563432, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 216221, + "lmtime": 1741161563564, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 192184, + "lmtime": 1741161563694, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 129205, + "lmtime": 1741161563815, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 189234, + "lmtime": 1741161563951, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 213151, + "lmtime": 1741161564088, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 147907, + "lmtime": 1741161564207, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 158106, + "lmtime": 1741161564335, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 218431, + "lmtime": 1741161564472, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 196462, + "lmtime": 1741161564604, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 127647, + "lmtime": 1741161564718, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 204355, + "lmtime": 1741161564847, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 213707, + "lmtime": 1741161564974, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 217636, + "lmtime": 1741161565103, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 204063, + "lmtime": 1741161565232, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 202382, + "lmtime": 1741161565372, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 229786, + "lmtime": 1741161565507, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 201069, + "lmtime": 1741161565670, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 126080, + "lmtime": 1741161565812, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 69143, + "lmtime": 1741161561908, + "modified": false + }, + "mobile_face.webp": { + "type": "-", + "size": 52390, + "lmtime": 1741161562028, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 299428, + "lmtime": 1741161562225, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 297870, + "lmtime": 1741161562374, + "modified": false + } + }, + "preview.jpg": { + "type": "-", + "size": 66144, + "lmtime": 1741161542507, + "modified": false + }, + "preview.webp": { + "type": "-", + "size": 56160, + "lmtime": 1741161542638, + "modified": false + } + }, + "build2": { + "0": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 104912, + "lmtime": 1741161566567, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 68774, + "lmtime": 1741161566699, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 26896, + "lmtime": 1741161566811, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 95048, + "lmtime": 1741161566939, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 45454, + "lmtime": 1741161567054, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 79454, + "lmtime": 1741161567186, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 29508, + "lmtime": 1741161567300, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 136111, + "lmtime": 1741161567453, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 86806, + "lmtime": 1741161567571, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 30768, + "lmtime": 1741161567683, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 53561, + "lmtime": 1741161567808, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 115074, + "lmtime": 1741161567948, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 66671, + "lmtime": 1741161568072, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 74441, + "lmtime": 1741161568202, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 67501, + "lmtime": 1741161568346, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 82027, + "lmtime": 1741161568471, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 72031, + "lmtime": 1741161568605, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 68691, + "lmtime": 1741161568741, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 78477, + "lmtime": 1741161568873, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 113921, + "lmtime": 1741161569003, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 105307, + "lmtime": 1741161569150, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 73511, + "lmtime": 1741161569291, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 93605, + "lmtime": 1741161569497, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 93283, + "lmtime": 1741161569650, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 180209, + "lmtime": 1741161569785, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 46162, + "lmtime": 1741161566168, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 139681, + "lmtime": 1741161566299, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 84018, + "lmtime": 1741161566420, + "modified": false + } + }, + "1": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 112752, + "lmtime": 1741161570331, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 84329, + "lmtime": 1741161570482, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 32972, + "lmtime": 1741161570600, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 117258, + "lmtime": 1741161570743, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 60714, + "lmtime": 1741161570875, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 95036, + "lmtime": 1741161570990, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 43126, + "lmtime": 1741161571103, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 98666, + "lmtime": 1741161571238, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 44194, + "lmtime": 1741161571351, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 62154, + "lmtime": 1741161571478, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 88282, + "lmtime": 1741161571611, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 109707, + "lmtime": 1741161571748, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 87424, + "lmtime": 1741161571877, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 71834, + "lmtime": 1741161572016, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 79990, + "lmtime": 1741161572146, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 102704, + "lmtime": 1741161572289, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 79337, + "lmtime": 1741161572422, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 84208, + "lmtime": 1741161572563, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 98963, + "lmtime": 1741161572708, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 104327, + "lmtime": 1741161572856, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 63235, + "lmtime": 1741161572972, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 109173, + "lmtime": 1741161573106, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 52060, + "lmtime": 1741161573229, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 103898, + "lmtime": 1741161573348, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 89677, + "lmtime": 1741161573489, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 49797, + "lmtime": 1741161569901, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 145505, + "lmtime": 1741161570053, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 83470, + "lmtime": 1741161570189, + "modified": false + } + }, + "2": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 130947, + "lmtime": 1741161574026, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 159318, + "lmtime": 1741161574163, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 92860, + "lmtime": 1741161574301, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 104791, + "lmtime": 1741161574441, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 49114, + "lmtime": 1741161574578, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 79253, + "lmtime": 1741161574702, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 32114, + "lmtime": 1741161574821, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 134789, + "lmtime": 1741161574952, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 86982, + "lmtime": 1741161575099, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 112160, + "lmtime": 1741161575246, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 116085, + "lmtime": 1741161575382, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 91927, + "lmtime": 1741161575511, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 73849, + "lmtime": 1741161575636, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 137462, + "lmtime": 1741161575799, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 120978, + "lmtime": 1741161575929, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 110224, + "lmtime": 1741161576060, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 94750, + "lmtime": 1741161576191, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 79786, + "lmtime": 1741161576313, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 110376, + "lmtime": 1741161576452, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 111194, + "lmtime": 1741161576586, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 85907, + "lmtime": 1741161576732, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 43121, + "lmtime": 1741161576858, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 54334, + "lmtime": 1741161577215, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 110487, + "lmtime": 1741161577354, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 163092, + "lmtime": 1741161577494, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 55198, + "lmtime": 1741161573622, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 174816, + "lmtime": 1741161573772, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 117606, + "lmtime": 1741161573902, + "modified": false + } + }, + "3": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 81098, + "lmtime": 1741161578020, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 68700, + "lmtime": 1741161578167, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 23672, + "lmtime": 1741161578289, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 53009, + "lmtime": 1741161578413, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 17726, + "lmtime": 1741161578505, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 96417, + "lmtime": 1741161578638, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 42130, + "lmtime": 1741161578763, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 73882, + "lmtime": 1741161578890, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 25530, + "lmtime": 1741161578997, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 68384, + "lmtime": 1741161579128, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 93061, + "lmtime": 1741161579249, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 37285, + "lmtime": 1741161579364, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 32502, + "lmtime": 1741161579496, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 75881, + "lmtime": 1741161579610, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 49047, + "lmtime": 1741161579726, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 59994, + "lmtime": 1741161579843, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 62849, + "lmtime": 1741161579964, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 79711, + "lmtime": 1741161580099, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 77413, + "lmtime": 1741161580236, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 73967, + "lmtime": 1741161580364, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 70057, + "lmtime": 1741161580506, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 103135, + "lmtime": 1741161580651, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 87292, + "lmtime": 1741161580778, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 67961, + "lmtime": 1741161580922, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 64343, + "lmtime": 1741161581066, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 36406, + "lmtime": 1741161577614, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 102867, + "lmtime": 1741161577754, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 45446, + "lmtime": 1741161577877, + "modified": false + } + }, + "4": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 117524, + "lmtime": 1741161581635, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 116154, + "lmtime": 1741161581768, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 54112, + "lmtime": 1741161581898, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 79219, + "lmtime": 1741161582025, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 27160, + "lmtime": 1741161582153, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 90011, + "lmtime": 1741161582298, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 34636, + "lmtime": 1741161582411, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 120826, + "lmtime": 1741161582561, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 58138, + "lmtime": 1741161582693, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 88089, + "lmtime": 1741161582835, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 95918, + "lmtime": 1741161582976, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 71024, + "lmtime": 1741161583116, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 60477, + "lmtime": 1741161583249, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 102216, + "lmtime": 1741161583393, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 82607, + "lmtime": 1741161583515, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 67584, + "lmtime": 1741161583640, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 56253, + "lmtime": 1741161583767, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 92417, + "lmtime": 1741161583895, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 77992, + "lmtime": 1741161584025, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 79820, + "lmtime": 1741161584149, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 85838, + "lmtime": 1741161584271, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 41726, + "lmtime": 1741161584375, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 100442, + "lmtime": 1741161584519, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 117445, + "lmtime": 1741161584665, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 109116, + "lmtime": 1741161584812, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 51013, + "lmtime": 1741161581199, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 147677, + "lmtime": 1741161581355, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 80968, + "lmtime": 1741161581497, + "modified": false + } + }, + "5": { + "0": { + "0_0.jpg": { + "type": "-", + "size": 136413, + "lmtime": 1741161585468, + "modified": false + } + }, + "1": { + "0_0.jpg": { + "type": "-", + "size": 74185, + "lmtime": 1741161585612, + "modified": false + }, + "0_0.webp": { + "type": "-", + "size": 28738, + "lmtime": 1741161585725, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 188978, + "lmtime": 1741161585878, + "modified": false + }, + "0_1.webp": { + "type": "-", + "size": 154136, + "lmtime": 1741161586027, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 184446, + "lmtime": 1741161586184, + "modified": false + }, + "1_0.webp": { + "type": "-", + "size": 148678, + "lmtime": 1741161586344, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 120196, + "lmtime": 1741161586493, + "modified": false + }, + "1_1.webp": { + "type": "-", + "size": 72656, + "lmtime": 1741161586619, + "modified": false + } + }, + "2": { + "0_0.jpg": { + "type": "-", + "size": 49589, + "lmtime": 1741161586749, + "modified": false + }, + "0_1.jpg": { + "type": "-", + "size": 61528, + "lmtime": 1741161586893, + "modified": false + }, + "0_2.jpg": { + "type": "-", + "size": 151256, + "lmtime": 1741161587049, + "modified": false + }, + "0_3.jpg": { + "type": "-", + "size": 172825, + "lmtime": 1741161587201, + "modified": false + }, + "1_0.jpg": { + "type": "-", + "size": 59025, + "lmtime": 1741161587335, + "modified": false + }, + "1_1.jpg": { + "type": "-", + "size": 117780, + "lmtime": 1741161587489, + "modified": false + }, + "1_2.jpg": { + "type": "-", + "size": 199382, + "lmtime": 1741161587654, + "modified": false + }, + "1_3.jpg": { + "type": "-", + "size": 134689, + "lmtime": 1741161587807, + "modified": false + }, + "2_0.jpg": { + "type": "-", + "size": 118707, + "lmtime": 1741161587956, + "modified": false + }, + "2_1.jpg": { + "type": "-", + "size": 195950, + "lmtime": 1741161588111, + "modified": false + }, + "2_2.jpg": { + "type": "-", + "size": 147942, + "lmtime": 1741161588253, + "modified": false + }, + "2_3.jpg": { + "type": "-", + "size": 166548, + "lmtime": 1741161588413, + "modified": false + }, + "3_0.jpg": { + "type": "-", + "size": 172406, + "lmtime": 1741161588578, + "modified": false + }, + "3_1.jpg": { + "type": "-", + "size": 146326, + "lmtime": 1741161588709, + "modified": false + }, + "3_2.jpg": { + "type": "-", + "size": 44365, + "lmtime": 1741161588824, + "modified": false + }, + "3_3.jpg": { + "type": "-", + "size": 71751, + "lmtime": 1741161588932, + "modified": false + } + }, + "mobile_face.jpg": { + "type": "-", + "size": 51249, + "lmtime": 1741161584936, + "modified": false + }, + "tablet_face.jpg": { + "type": "-", + "size": 196514, + "lmtime": 1741161585099, + "modified": false + }, + "tablet_face.webp": { + "type": "-", + "size": 159270, + "lmtime": 1741161585250, + "modified": false + } + }, + "preview.jpg": { + "type": "-", + "size": 51955, + "lmtime": 1741161565932, + "modified": false + }, + "preview.webp": { + "type": "-", + "size": 38496, + "lmtime": 1741161566054, + "modified": false + } + }, + "graphics": { + "arrow.png": { + "type": "-", + "size": 310, + "lmtime": 1741161589009, + "modified": false + }, + "arrow.webp": { + "type": "-", + "size": 110, + "lmtime": 1741161589078, + "modified": false + }, + "gyro.js": { + "type": "-", + "size": 3786, + "lmtime": 1741161589153, + "modified": false + }, + "introImage.png": { + "type": "-", + "size": 598222, + "lmtime": 1741161589305, + "modified": false + }, + "introImage.webp": { + "type": "-", + "size": 59862, + "lmtime": 1741161589407, + "modified": false + }, + "menu": { + "help.png": { + "type": "-", + "size": 257932, + "lmtime": 1741161590114, + "modified": false + }, + "help.webp": { + "type": "-", + "size": 73678, + "lmtime": 1741161590250, + "modified": false + }, + "menu0.png": { + "type": "-", + "size": 5083, + "lmtime": 1741161590338, + "modified": false + }, + "menu0.webp": { + "type": "-", + "size": 940, + "lmtime": 1741161590422, + "modified": false + }, + "menu10.png": { + "type": "-", + "size": 11039, + "lmtime": 1741161590517, + "modified": false + }, + "menu10.webp": { + "type": "-", + "size": 1758, + "lmtime": 1741161590612, + "modified": false + }, + "menu11.png": { + "type": "-", + "size": 4910, + "lmtime": 1741161590706, + "modified": false + }, + "menu12.png": { + "type": "-", + "size": 4952, + "lmtime": 1741161590807, + "modified": false + }, + "menu13.png": { + "type": "-", + "size": 2937, + "lmtime": 1741161590902, + "modified": false + }, + "menu1.png": { + "type": "-", + "size": 4327, + "lmtime": 1741161590999, + "modified": false + }, + "menu1.webp": { + "type": "-", + "size": 596, + "lmtime": 1741161591104, + "modified": false + }, + "menu2.png": { + "type": "-", + "size": 6124, + "lmtime": 1741161591190, + "modified": false + }, + "menu2.webp": { + "type": "-", + "size": 1144, + "lmtime": 1741161591270, + "modified": false + }, + "menu3.png": { + "type": "-", + "size": 6335, + "lmtime": 1741161591372, + "modified": false + }, + "menu3.webp": { + "type": "-", + "size": 1184, + "lmtime": 1741161591458, + "modified": false + }, + "menu4.png": { + "type": "-", + "size": 6194, + "lmtime": 1741161591539, + "modified": false + }, + "menu4.webp": { + "type": "-", + "size": 1228, + "lmtime": 1741161591625, + "modified": false + }, + "menu5.png": { + "type": "-", + "size": 6296, + "lmtime": 1741161591720, + "modified": false + }, + "menu5.webp": { + "type": "-", + "size": 1264, + "lmtime": 1741161591832, + "modified": false + }, + "menu6.png": { + "type": "-", + "size": 7160, + "lmtime": 1741161591923, + "modified": false + }, + "menu6.webp": { + "type": "-", + "size": 1800, + "lmtime": 1741161592022, + "modified": false + }, + "menu7.png": { + "type": "-", + "size": 6794, + "lmtime": 1741161592111, + "modified": false + }, + "menu7.webp": { + "type": "-", + "size": 1698, + "lmtime": 1741161592210, + "modified": false + }, + "menu8.png": { + "type": "-", + "size": 7058, + "lmtime": 1741161592303, + "modified": false + }, + "menu8.webp": { + "type": "-", + "size": 1376, + "lmtime": 1741161592390, + "modified": false + }, + "menu9.png": { + "type": "-", + "size": 13096, + "lmtime": 1741161592501, + "modified": false + }, + "menu9.webp": { + "type": "-", + "size": 3550, + "lmtime": 1741161592599, + "modified": false + } + }, + "slide.png": { + "type": "-", + "size": 2819, + "lmtime": 1741161589482, + "modified": false + }, + "slide.webp": { + "type": "-", + "size": 34, + "lmtime": 1741161589558, + "modified": false + }, + "spots": { + "object0.jpg": { + "type": "-", + "size": 73903, + "lmtime": 1741161592730, + "modified": false + }, + "object0.webp": { + "type": "-", + "size": 45306, + "lmtime": 1741161592859, + "modified": false + }, + "spot0.png": { + "type": "-", + "size": 1079, + "lmtime": 1741161592943, + "modified": false + }, + "spot0.webp": { + "type": "-", + "size": 470, + "lmtime": 1741161593039, + "modified": false + }, + "spot1.png": { + "type": "-", + "size": 1439, + "lmtime": 1741161593139, + "modified": false + }, + "spot1.webp": { + "type": "-", + "size": 902, + "lmtime": 1741161593212, + "modified": false + } + }, + "strap.png": { + "type": "-", + "size": 3452, + "lmtime": 1741161589632, + "modified": false + }, + "strap.webp": { + "type": "-", + "size": 326, + "lmtime": 1741161589708, + "modified": false + }, + "thumb_scroll.png": { + "type": "-", + "size": 182, + "lmtime": 1741161589787, + "modified": false + }, + "transparent.png": { + "type": "-", + "size": 234, + "lmtime": 1741161589872, + "modified": false + }, + "transparent.webp": { + "type": "-", + "size": 34, + "lmtime": 1741161589947, + "modified": false + } + } + }, + "build.html": { + "type": "-", + "size": 1210, + "lmtime": 1741161519517, + "modified": false + }, + "build.swf": { + "type": "-", + "size": 119131, + "lmtime": 1741161519650, + "modified": false + }, + "build.xml": { + "type": "-", + "size": 10991, + "lmtime": 1741161519739, + "modified": false + }, + "index.html": { + "type": "-", + "size": 1213, + "lmtime": 1741161519813, + "modified": false + }, + "krpanoiphone.js": { + "type": "-", + "size": 84072, + "lmtime": 1741161519935, + "modified": false + }, + "krpanoiphone.license.js": { + "type": "-", + "size": 2843, + "lmtime": 1741161520008, + "modified": false + }, + "sp.xml": { + "type": "-", + "size": 8187, + "lmtime": 1741161520096, + "modified": false + }, + "swfobject": { + "swfkrpano.js": { + "type": "-", + "size": 7691, + "lmtime": 1741161593298, + "modified": false + } + } + } + } + }, + "cache": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161593401, + "modified": false + } + }, + "changelog": { + "changelog_1741.txt": { + "type": "-", + "size": 1308, + "lmtime": 1741161593487, + "modified": false + }, + "changelog_1750.txt": { + "type": "-", + "size": 965, + "lmtime": 1741161593583, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161593694, + "modified": false + } + }, + "config": { + ".htaccess": { + "type": "-", + "size": 181, + "lmtime": 1741161593774, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161593874, + "modified": false + }, + "theme.yml": { + "type": "-", + "size": 4199, + "lmtime": 1741161593971, + "modified": false + } + }, + "config.rb": { + "type": "-", + "size": 876, + "lmtime": 1741161143788, + "modified": false + }, + "config.xml": { + "type": "-", + "size": 8850, + "lmtime": 1741161143875, + "modified": false + }, + "dependencies": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161594060, + "modified": false + }, + "modules": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161594158, + "modified": false + } + } + }, + "development": { + "customize": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161594361, + "modified": false + }, + "layout.xml": { + "type": "-", + "size": 432, + "lmtime": 1741161594454, + "modified": false + }, + "module.xml": { + "type": "-", + "size": 486, + "lmtime": 1741161594543, + "modified": false + }, + "themeeditor.xml": { + "type": "-", + "size": 12873, + "lmtime": 1741161594626, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161594263, + "modified": false + } + }, + "export": { + "appagebuilder.xml": { + "type": "-", + "size": 277115, + "lmtime": 1741161594788, + "modified": false + }, + "group.xml": { + "type": "-", + "size": 10944, + "lmtime": 1741161594884, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1740602820000, + "modified": false + }, + "modules": { + "appagebuilder": { + "img": { + "ajax-loader.gif": { + "type": "-", + "size": 4178, + "lmtime": 1741161595272, + "modified": false + }, + "AjaxLoader.gif": { + "type": "-", + "size": 1517, + "lmtime": 1741161595376, + "modified": false + }, + "grabbing.png": { + "type": "-", + "size": 116, + "lmtime": 1741161595470, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1267, + "lmtime": 1741161595549, + "modified": false + }, + "label-new.png": { + "type": "-", + "size": 6279, + "lmtime": 1741161595649, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161595166, + "modified": false + }, + "views": { + "css": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161595817, + "modified": false + }, + "non-responsive.css": { + "type": "-", + "size": 1343, + "lmtime": 1741161595901, + "modified": false + }, + "owl.carousel.css": { + "type": "-", + "size": 3692, + "lmtime": 1741161595984, + "modified": false + }, + "owl.theme.css": { + "type": "-", + "size": 4299, + "lmtime": 1741161596068, + "modified": false + }, + "paneltool.css": { + "type": "-", + "size": 13867, + "lmtime": 1741161596164, + "modified": false + }, + "patterns": { + "index.php": { + "type": "-", + "size": 1277, + "lmtime": 1741161596557, + "modified": false + }, + "test.css": { + "type": "-", + "size": 465, + "lmtime": 1741161596644, + "modified": false + }, + "test.json": { + "type": "-", + "size": 7346, + "lmtime": 1741161596731, + "modified": false + } + }, + "positions": { + "footerposition1488482722.css": { + "type": "-", + "size": 1334, + "lmtime": 1741161596821, + "modified": false + }, + "headerposition1487279078.css": { + "type": "-", + "size": 743, + "lmtime": 1741161596915, + "modified": false + }, + "headerposition1488216530.css": { + "type": "-", + "size": 1078, + "lmtime": 1741161596997, + "modified": false + }, + "headerposition1488483140.css": { + "type": "-", + "size": 2615, + "lmtime": 1741161597080, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161597172, + "modified": false + } + }, + "profiles": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161597252, + "modified": false + }, + "profile1487249757.css": { + "type": "-", + "size": 734, + "lmtime": 1741161597332, + "modified": false + }, + "profile1488232255.css": { + "type": "-", + "size": 792, + "lmtime": 1741161597404, + "modified": false + }, + "profile1488392287.css": { + "type": "-", + "size": 788, + "lmtime": 1741161597502, + "modified": false + }, + "profile1488486320.css": { + "type": "-", + "size": 1050, + "lmtime": 1741161597599, + "modified": false + }, + "profile1488572554.css": { + "type": "-", + "size": 1236, + "lmtime": 1741161597701, + "modified": false + } + }, + "skins": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161597784, + "modified": false + } + }, + "slick.css": { + "type": "-", + "size": 2575, + "lmtime": 1741161596251, + "modified": false + }, + "slick-theme.css": { + "type": "-", + "size": 4080, + "lmtime": 1741161596343, + "modified": false + }, + "styles.css": { + "type": "-", + "size": 57647, + "lmtime": 1741161596464, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161595733, + "modified": false + }, + "js": { + "index.php": { + "type": "-", + "size": 1267, + "lmtime": 1741161597879, + "modified": false + }, + "positions": { + "index.php": { + "type": "-", + "size": 1267, + "lmtime": 1741161597978, + "modified": false + } + }, + "profiles": { + "index.php": { + "type": "-", + "size": 1267, + "lmtime": 1741161598072, + "modified": false + } + } + }, + "templates": { + "front": { + "details": { + "detail1526383012.tpl": { + "type": "-", + "size": 11323, + "lmtime": 1741161598381, + "modified": false + }, + "detail1526395446 kopia.tpl": { + "type": "-", + "size": 18621, + "lmtime": 1741161598493, + "modified": false + }, + "detail1526395446 — kopia.tpl": { + "type": "-", + "size": 18302, + "lmtime": 1741161598602, + "modified": false + }, + "detail1526395446.tpl": { + "type": "-", + "size": 29782, + "lmtime": 1741161598708, + "modified": false + }, + "detail1526396195.tpl": { + "type": "-", + "size": 11225, + "lmtime": 1741161598794, + "modified": false + }, + "detail1526396867.tpl": { + "type": "-", + "size": 11189, + "lmtime": 1741161598882, + "modified": false + }, + "detail1526404070.tpl": { + "type": "-", + "size": 11208, + "lmtime": 1741161598960, + "modified": false + }, + "detail1526404543.tpl": { + "type": "-", + "size": 10976, + "lmtime": 1741161599051, + "modified": false + }, + "detail1526406009.tpl": { + "type": "-", + "size": 11206, + "lmtime": 1741161599126, + "modified": false + }, + "detail1526475521.tpl": { + "type": "-", + "size": 5661, + "lmtime": 1741161599222, + "modified": false + }, + "detail1526484542.tpl": { + "type": "-", + "size": 10988, + "lmtime": 1741161599326, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161599424, + "modified": false + }, + "kopia_3lis2020.tpl": { + "type": "-", + "size": 23093, + "lmtime": 1741161599506, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1307, + "lmtime": 1741161598280, + "modified": false + }, + "info": { + "assets": { + "form.css": { + "type": "-", + "size": 1774, + "lmtime": 1741161599764, + "modified": false + }, + "form.js": { + "type": "-", + "size": 639, + "lmtime": 1741161599851, + "modified": false + }, + "icon.png": { + "type": "-", + "size": 1209, + "lmtime": 1741161599931, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161600001, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1314, + "lmtime": 1741161599585, + "modified": false + }, + "paneltool.tpl": { + "type": "-", + "size": 0, + "lmtime": 1741161599667, + "modified": false + } + }, + "products": { + "index.php": { + "type": "-", + "size": 1287, + "lmtime": 1741161600083, + "modified": false + }, + "leo_countdown.tpl": { + "type": "-", + "size": 330, + "lmtime": 1741161600154, + "modified": false + } + }, + "profiles": { + "id_gencode_58a59cac30b87_1487248556.tpl": { + "type": "-", + "size": 282, + "lmtime": 1741161600231, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1314, + "lmtime": 1741161600303, + "modified": false + }, + "plist1487280701.tpl": { + "type": "-", + "size": 6959, + "lmtime": 1741161600400, + "modified": false + }, + "plist1488206015.tpl": { + "type": "-", + "size": 4304, + "lmtime": 1741161600499, + "modified": false + }, + "plist1488671538.tpl": { + "type": "-", + "size": 2785, + "lmtime": 1741161600588, + "modified": false + }, + "profile1487249757": { + "id_gencode_58a59cac30b87_1487248556.tpl": { + "type": "-", + "size": 447, + "lmtime": 1741161600663, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1277, + "lmtime": 1741161600753, + "modified": false + } + } + } + }, + "index.php": { + "type": "-", + "size": 1310, + "lmtime": 1741161598175, + "modified": false + } + } + } + }, + "blockgrouptop": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161600838, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161600934, + "modified": false + }, + "templates": { + "hook": { + "blockgrouptop.tpl": { + "type": "-", + "size": 4303, + "lmtime": 1741161601100, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161601176, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161601014, + "modified": false + } + } + } + }, + "blockreassurance": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161601344, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161601447, + "modified": false + }, + "templates": { + "hook": { + "blockreassurance.sync-conflict-20231027-195751-EDGUH2C.tpl": { + "type": "-", + "size": 9313, + "lmtime": 1741161601605, + "modified": false + }, + "blockreassurance.tpl": { + "type": "-", + "size": 9296, + "lmtime": 1741161601696, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161601771, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161601529, + "modified": false + } + } + } + }, + "contactform": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161601906, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161601998, + "modified": false + }, + "templates": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161602101, + "modified": false + }, + "widget": { + "contactform.tpl": { + "type": "-", + "size": 5011, + "lmtime": 1741161602218, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161602318, + "modified": false + } + } + } + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161595007, + "modified": false + }, + "leoblog": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161602397, + "modified": false + }, + "views": { + "css": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161602602, + "modified": false + }, + "leoblog.css": { + "type": "-", + "size": 11174, + "lmtime": 1741161602696, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161602503, + "modified": false + }, + "templates": { + "front": { + "default": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161603001, + "modified": false + }, + "_listing_blog.tpl": { + "type": "-", + "size": 4420, + "lmtime": 1741161603090, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161602909, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161602816, + "modified": false + } + } + } + }, + "leobootstrapmenu": { + "img": { + "index.php": { + "type": "-", + "size": 1287, + "lmtime": 1741161603255, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161603168, + "modified": false + }, + "views": { + "css": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161603434, + "modified": false + }, + "leomenusidebar.css": { + "type": "-", + "size": 52, + "lmtime": 1741161603511, + "modified": false + }, + "megamenu.css": { + "type": "-", + "size": 46, + "lmtime": 1741161603586, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161603349, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1277, + "lmtime": 1741161603803, + "modified": false + }, + "megamenu.tpl": { + "type": "-", + "size": 2780, + "lmtime": 1741161603889, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1342, + "lmtime": 1741161603687, + "modified": false + } + } + } + }, + "leofeature": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161603988, + "modified": false + }, + "views": { + "css": { + "front.css": { + "type": "-", + "size": 61319, + "lmtime": 1741161604167, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604254, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604055, + "modified": false + } + } + }, + "leoproductsearch": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604330, + "modified": false + }, + "views": { + "css": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604477, + "modified": false + }, + "jquery.autocomplete_productsearch.css": { + "type": "-", + "size": 1239, + "lmtime": 1741161604561, + "modified": false + }, + "leosearch.css": { + "type": "-", + "size": 5180, + "lmtime": 1741161604639, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604411, + "modified": false + } + } + }, + "leoslideshow": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604718, + "modified": false + }, + "views": { + "css": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604898, + "modified": false + }, + "iView": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604971, + "modified": false + }, + "iview.css": { + "type": "-", + "size": 8804, + "lmtime": 1741161605057, + "modified": false + }, + "skin_4_responsive": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161605133, + "modified": false + }, + "style.css": { + "type": "-", + "size": 2094, + "lmtime": 1741161605214, + "modified": false + } + } + }, + "typo": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161605297, + "modified": false + }, + "typo.css": { + "type": "-", + "size": 27965, + "lmtime": 1741161605394, + "modified": false + } + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161604806, + "modified": false + } + } + }, + "ps_advertising": { + "index.php": { + "type": "-", + "size": 1267, + "lmtime": 1741161605474, + "modified": false + }, + "ps_advertising.tpl": { + "type": "-", + "size": 1141, + "lmtime": 1741161605553, + "modified": false + } + }, + "ps_banner": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161605656, + "modified": false + }, + "ps_banner.tpl": { + "type": "-", + "size": 1217, + "lmtime": 1741161605751, + "modified": false + } + }, + "ps_bestsellers": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161605860, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161605946, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161606109, + "modified": false + }, + "ps_bestsellers.tpl": { + "type": "-", + "size": 1513, + "lmtime": 1741161606191, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161606038, + "modified": false + } + } + } + }, + "ps_brandlist": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161606294, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161606389, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161606575, + "modified": false + }, + "ps_brandlist.tpl": { + "type": "-", + "size": 1587, + "lmtime": 1741161606724, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161606487, + "modified": false + }, + "_partials": { + "brand_form.tpl": { + "type": "-", + "size": 1334, + "lmtime": 1741161606813, + "modified": false + }, + "brand_text.tpl": { + "type": "-", + "size": 1310, + "lmtime": 1741161606911, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161606983, + "modified": false + } + } + } + } + }, + "ps_categoryproducts": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607065, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607144, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607326, + "modified": false + }, + "ps_categoryproducts.tpl": { + "type": "-", + "size": 6028, + "lmtime": 1741161607410, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607249, + "modified": false + } + } + } + }, + "ps_categorytree": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607496, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607580, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607745, + "modified": false + }, + "ps_categorytree.tpl": { + "type": "-", + "size": 2715, + "lmtime": 1741161607828, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607663, + "modified": false + } + } + } + }, + "ps_contactinfo": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161607904, + "modified": false + }, + "nav.tpl": { + "type": "-", + "size": 1444, + "lmtime": 1741161608040, + "modified": false + }, + "ps_contactinfo-rich.tpl": { + "type": "-", + "size": 2143, + "lmtime": 1741161608144, + "modified": false + }, + "ps_contactinfo.tpl": { + "type": "-", + "size": 2962, + "lmtime": 1741161608241, + "modified": false + } + }, + "ps_crossselling": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161608324, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161608406, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161608619, + "modified": false + }, + "ps_crossselling.tpl": { + "type": "-", + "size": 1854, + "lmtime": 1741161608706, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161608498, + "modified": false + } + } + } + }, + "ps_currencyselector": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161608796, + "modified": false + }, + "ps_currencyselector.tpl": { + "type": "-", + "size": 1687, + "lmtime": 1741161608877, + "modified": false + } + }, + "ps_customeraccountlinks": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161608966, + "modified": false + }, + "ps_customeraccountlinks.tpl": { + "type": "-", + "size": 2038, + "lmtime": 1741161609068, + "modified": false + } + }, + "ps_customersignin": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161609142, + "modified": false + }, + "ps_customersignin.tpl": { + "type": "-", + "size": 1982, + "lmtime": 1741161609225, + "modified": false + } + }, + "ps_emailalerts": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161609318, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161609407, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161609593, + "modified": false + }, + "my-account-footer.tpl": { + "type": "-", + "size": 1172, + "lmtime": 1741161609690, + "modified": false + }, + "my-account.tpl": { + "type": "-", + "size": 1301, + "lmtime": 1741161609791, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161609502, + "modified": false + } + } + } + }, + "ps_emailsubscription": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161609890, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161609988, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161610151, + "modified": false + }, + "ps_emailsubscription.tpl": { + "type": "-", + "size": 2854, + "lmtime": 1741161610223, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161610072, + "modified": false + } + } + } + }, + "ps_facetedsearch": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161610300, + "modified": false + }, + "ps_facetedsearch.tpl": { + "type": "-", + "size": 1411, + "lmtime": 1741161610373, + "modified": false + } + }, + "ps_featuredproducts": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161610469, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161610566, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161610728, + "modified": false + }, + "ps_featuredproducts.tpl": { + "type": "-", + "size": 1541, + "lmtime": 1741161610838, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161610653, + "modified": false + } + } + } + }, + "ps_imageslider": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161611027, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161611121, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161611309, + "modified": false + }, + "slider.tpl": { + "type": "-", + "size": 2713, + "lmtime": 1741161611419, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161611211, + "modified": false + } + } + } + }, + "ps_languageselector": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161611507, + "modified": false + }, + "ps_languageselector.tpl": { + "type": "-", + "size": 2124, + "lmtime": 1741161611588, + "modified": false + } + }, + "ps_legalcompliance": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161611677, + "modified": false + }, + "views": { + "css": { + "aeuc_front.css": { + "type": "-", + "size": 2967, + "lmtime": 1741161611873, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161611964, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161611781, + "modified": false + }, + "templates": { + "hook": { + "hookDisplayFooter.tpl": { + "type": "-", + "size": 1822, + "lmtime": 1741161612149, + "modified": false + }, + "hookDisplayOverrideTemplateFooter.tpl": { + "type": "-", + "size": 297, + "lmtime": 1741161612233, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161612321, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161612059, + "modified": false + } + } + } + }, + "ps_linklist": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161612397, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161612469, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161614012, + "modified": false + }, + "linkblock.tpl": { + "type": "-", + "size": 2164, + "lmtime": 1741161614108, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161613926, + "modified": false + } + } + } + }, + "ps_mainmenu": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161615542, + "modified": false + }, + "ps_mainmenu.tpl": { + "type": "-", + "size": 2073, + "lmtime": 1741161615636, + "modified": false + } + }, + "ps_newproducts": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161615717, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161615921, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616121, + "modified": false + }, + "ps_newproducts.tpl": { + "type": "-", + "size": 1511, + "lmtime": 1741161616213, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616033, + "modified": false + } + } + } + }, + "ps_productinfo": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616291, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616375, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616576, + "modified": false + }, + "ps_productinfo.tpl": { + "type": "-", + "size": 1698, + "lmtime": 1741161616677, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616486, + "modified": false + } + } + } + }, + "ps_rssfeed": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616756, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616843, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161617007, + "modified": false + }, + "ps_rssfeed.tpl": { + "type": "-", + "size": 1417, + "lmtime": 1741161617096, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161616928, + "modified": false + } + } + } + }, + "ps_searchbar": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161617183, + "modified": false + }, + "ps_searchbar.tpl": { + "type": "-", + "size": 1756, + "lmtime": 1741161617282, + "modified": false + } + }, + "ps_sharebuttons": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161617371, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161617486, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161617823, + "modified": false + }, + "ps_sharebuttons.tpl": { + "type": "-", + "size": 1465, + "lmtime": 1741161617901, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161617605, + "modified": false + } + } + } + }, + "ps_shoppingcart": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161617984, + "modified": false + }, + "modal.tpl": { + "type": "-", + "size": 4190, + "lmtime": 1741161618059, + "modified": false + }, + "ps_shoppingcart-product-line.tpl": { + "type": "-", + "size": 2515, + "lmtime": 1741161618144, + "modified": false + }, + "ps_shoppingcart.tpl": { + "type": "-", + "size": 1856, + "lmtime": 1741161618209, + "modified": false + }, + "ps_shoppingcart.tpl_old": { + "type": "-", + "size": 1765, + "lmtime": 1741161618281, + "modified": false + } + }, + "ps_socialfollow": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161618359, + "modified": false + }, + "ps_socialfollow.tpl": { + "type": "-", + "size": 1305, + "lmtime": 1741161618437, + "modified": false + } + }, + "ps_specials": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161618512, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161618586, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161618754, + "modified": false + }, + "ps_specials.tpl": { + "type": "-", + "size": 1505, + "lmtime": 1741161618855, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161618675, + "modified": false + } + } + } + }, + "ps_supplierlist": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161618924, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161619004, + "modified": false + }, + "templates": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161619176, + "modified": false + }, + "ps_supplierlist.tpl": { + "type": "-", + "size": 1620, + "lmtime": 1741161619249, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161619090, + "modified": false + }, + "_partials": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161619319, + "modified": false + }, + "supplier_form.tpl": { + "type": "-", + "size": 1349, + "lmtime": 1741161619391, + "modified": false + }, + "supplier_text.tpl": { + "type": "-", + "size": 1331, + "lmtime": 1741161619476, + "modified": false + } + } + } + } + }, + "ps_viewedproduct": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161619557, + "modified": false + }, + "views": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161619646, + "modified": false + }, + "template": { + "hook": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161619809, + "modified": false + }, + "ps_viewedproduct.tpl": { + "type": "-", + "size": 3059, + "lmtime": 1741161619893, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161619732, + "modified": false + } + } + } + } + }, + "preview.png": { + "type": "-", + "size": 100970, + "lmtime": 1741161143999, + "modified": false + }, + "preview.webp": { + "type": "-", + "size": 15682, + "lmtime": 1741161144084, + "modified": false + }, + "samples": { + "appagebuilder.xml": { + "type": "-", + "size": 936215, + "lmtime": 1741161620053, + "modified": false + }, + "blockgrouptop.xml": { + "type": "-", + "size": 328, + "lmtime": 1741161620155, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161620231, + "modified": false + }, + "leoblog.xml": { + "type": "-", + "size": 103265, + "lmtime": 1741161620353, + "modified": false + }, + "leobootstrapmenu.xml": { + "type": "-", + "size": 74780, + "lmtime": 1741161620474, + "modified": false + }, + "leofeature.xml": { + "type": "-", + "size": 16934, + "lmtime": 1741161620570, + "modified": false + }, + "leoproductsearch.xml": { + "type": "-", + "size": 283, + "lmtime": 1741161620648, + "modified": false + }, + "leoquicklogin.xml": { + "type": "-", + "size": 8721, + "lmtime": 1741161620733, + "modified": false + }, + "leoslideshow.xml": { + "type": "-", + "size": 91625, + "lmtime": 1741161620854, + "modified": false + } + }, + "templates": { + "catalog": { + "brands.tpl": { + "type": "-", + "size": 1405, + "lmtime": 1741161621333, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161621401, + "modified": false + }, + "listing": { + "best-sales.tpl": { + "type": "-", + "size": 471, + "lmtime": 1741161621742, + "modified": false + }, + "category.tpl": { + "type": "-", + "size": 2424, + "lmtime": 1741161621831, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161621919, + "modified": false + }, + "manufacturer.tpl": { + "type": "-", + "size": 1376, + "lmtime": 1741161622013, + "modified": false + }, + "new-products.tpl": { + "type": "-", + "size": 472, + "lmtime": 1741161622113, + "modified": false + }, + "prices-drop.tpl": { + "type": "-", + "size": 476, + "lmtime": 1741161622192, + "modified": false + }, + "product-list.tpl": { + "type": "-", + "size": 45181, + "lmtime": 1741161622311, + "modified": false + }, + "search.tpl": { + "type": "-", + "size": 467, + "lmtime": 1741161622398, + "modified": false + }, + "supplier.tpl": { + "type": "-", + "size": 1247, + "lmtime": 1741161622486, + "modified": false + } + }, + "manufacturers.tpl": { + "type": "-", + "size": 1017, + "lmtime": 1741161621495, + "modified": false + }, + "_partials": { + "active_filters.tpl": { + "type": "-", + "size": 1748, + "lmtime": 1741161622600, + "modified": false + }, + "buy-by-phone-popup.tpl": { + "type": "-", + "size": 5145, + "lmtime": 1741161622691, + "modified": false + }, + "category-header.tpl": { + "type": "-", + "size": 1493, + "lmtime": 1741161622786, + "modified": false + }, + "facets.tpl": { + "type": "-", + "size": 7342, + "lmtime": 1741161622876, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161622958, + "modified": false + }, + "miniatures": { + "brand.tpl": { + "type": "-", + "size": 1600, + "lmtime": 1741161624369, + "modified": false + }, + "category.tpl": { + "type": "-", + "size": 1361, + "lmtime": 1741161624450, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161624530, + "modified": false + }, + "leo_col_products.tpl": { + "type": "-", + "size": 6153, + "lmtime": 1741161624611, + "modified": false + }, + "pack-product.tpl": { + "type": "-", + "size": 1881, + "lmtime": 1741161624692, + "modified": false + }, + "product.tpl": { + "type": "-", + "size": 7240, + "lmtime": 1741161624767, + "modified": false + } + }, + "product-activation.tpl": { + "type": "-", + "size": 1400, + "lmtime": 1741161623051, + "modified": false + }, + "product-additional-info.tpl": { + "type": "-", + "size": 1086, + "lmtime": 1741161623150, + "modified": false + }, + "product-add-to-cart.tpl": { + "type": "-", + "size": 4246, + "lmtime": 1741161623241, + "modified": false + }, + "product-cover-thumbnails.tpl": { + "type": "-", + "size": 3167, + "lmtime": 1741161623336, + "modified": false + }, + "product-customization.tpl": { + "type": "-", + "size": 3406, + "lmtime": 1741161623424, + "modified": false + }, + "product-details.tpl": { + "type": "-", + "size": 3310, + "lmtime": 1741161623512, + "modified": false + }, + "product-discounts.tpl": { + "type": "-", + "size": 2085, + "lmtime": 1741161623587, + "modified": false + }, + "product-images-modal.tpl": { + "type": "-", + "size": 2583, + "lmtime": 1741161623655, + "modified": false + }, + "product-prices.tpl": { + "type": "-", + "size": 3583, + "lmtime": 1741161623734, + "modified": false + }, + "products-bottom.tpl": { + "type": "-", + "size": 511, + "lmtime": 1741161623813, + "modified": false + }, + "products-top.tpl": { + "type": "-", + "size": 3417, + "lmtime": 1741161623899, + "modified": false + }, + "products.tpl": { + "type": "-", + "size": 2161, + "lmtime": 1741161623964, + "modified": false + }, + "product-variants.tpl": { + "type": "-", + "size": 3323, + "lmtime": 1741161624047, + "modified": false + }, + "quickview.tpl": { + "type": "-", + "size": 3276, + "lmtime": 1741161624138, + "modified": false + }, + "sort-orders.tpl": { + "type": "-", + "size": 2345, + "lmtime": 1741161624210, + "modified": false + }, + "variant-links.tpl": { + "type": "-", + "size": 906, + "lmtime": 1741161624291, + "modified": false + } + }, + "product.tpl": { + "type": "-", + "size": 8357, + "lmtime": 1741161621580, + "modified": false + }, + "suppliers.tpl": { + "type": "-", + "size": 1107, + "lmtime": 1741161621671, + "modified": false + } + }, + "checkout": { + "cart-empty.tpl": { + "type": "-", + "size": 1636, + "lmtime": 1741161624862, + "modified": false + }, + "cart.tpl": { + "type": "-", + "size": 2775, + "lmtime": 1741161624948, + "modified": false + }, + "checkout-process.tpl": { + "type": "-", + "size": 1156, + "lmtime": 1741161625028, + "modified": false + }, + "checkout.tpl": { + "type": "-", + "size": 2806, + "lmtime": 1741161625110, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161625195, + "modified": false + }, + "order-confirmation.tpl": { + "type": "-", + "size": 3860, + "lmtime": 1741161625274, + "modified": false + }, + "_partials": { + "address-form.tpl": { + "type": "-", + "size": 2019, + "lmtime": 1741161625341, + "modified": false + }, + "address-selector-block.tpl": { + "type": "-", + "size": 2689, + "lmtime": 1741161625426, + "modified": false + }, + "cart-detailed-actions.tpl": { + "type": "-", + "size": 1863, + "lmtime": 1741161625503, + "modified": false + }, + "cart-detailed-product-line.tpl": { + "type": "-", + "size": 7128, + "lmtime": 1741161625579, + "modified": false + }, + "cart-detailed-totals.tpl": { + "type": "-", + "size": 2377, + "lmtime": 1741161625665, + "modified": false + }, + "cart-detailed.tpl": { + "type": "-", + "size": 1718, + "lmtime": 1741161625756, + "modified": false + }, + "cart-summary-items-subtotal.tpl": { + "type": "-", + "size": 1263, + "lmtime": 1741161625849, + "modified": false + }, + "cart-summary-product-line.tpl": { + "type": "-", + "size": 1841, + "lmtime": 1741161625955, + "modified": false + }, + "cart-summary-totals.tpl": { + "type": "-", + "size": 1508, + "lmtime": 1741161626061, + "modified": false + }, + "cart-summary.tpl": { + "type": "-", + "size": 2690, + "lmtime": 1741161626154, + "modified": false + }, + "cart-voucher.tpl": { + "type": "-", + "size": 3483, + "lmtime": 1741161626244, + "modified": false + }, + "customer-form.tpl": { + "type": "-", + "size": 1771, + "lmtime": 1741161626320, + "modified": false + }, + "footer.tpl": { + "type": "-", + "size": 2010, + "lmtime": 1741161626399, + "modified": false + }, + "header.tpl": { + "type": "-", + "size": 2187, + "lmtime": 1741161626485, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161626574, + "modified": false + }, + "login-form.tpl": { + "type": "-", + "size": 1275, + "lmtime": 1741161626658, + "modified": false + }, + "order-confirmation-table.tpl": { + "type": "-", + "size": 5111, + "lmtime": 1741161626761, + "modified": false + }, + "order-final-summary-table.tpl": { + "type": "-", + "size": 1647, + "lmtime": 1741161626861, + "modified": false + }, + "order-final-summary.tpl": { + "type": "-", + "size": 3714, + "lmtime": 1741161626953, + "modified": false + }, + "steps": { + "addresses.tpl": { + "type": "-", + "size": 7047, + "lmtime": 1741161627049, + "modified": false + }, + "checkout-step.tpl": { + "type": "-", + "size": 1822, + "lmtime": 1741161627158, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161627245, + "modified": false + }, + "payment.tpl": { + "type": "-", + "size": 6735, + "lmtime": 1741161627332, + "modified": false + }, + "personal-information.tpl": { + "type": "-", + "size": 2927, + "lmtime": 1741161627417, + "modified": false + }, + "shipping.tpl": { + "type": "-", + "size": 5719, + "lmtime": 1741161627534, + "modified": false + }, + "unreachable.tpl": { + "type": "-", + "size": 1190, + "lmtime": 1741161627631, + "modified": false + } + } + } + }, + "cms": { + "category.tpl": { + "type": "-", + "size": 1841, + "lmtime": 1741161627741, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161627826, + "modified": false + }, + "page.tpl": { + "type": "-", + "size": 2274, + "lmtime": 1741161627889, + "modified": false + }, + "_partials": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161628101, + "modified": false + }, + "sitemap-nested-list.tpl": { + "type": "-", + "size": 1428, + "lmtime": 1741161628171, + "modified": false + } + }, + "sitemap.tpl": { + "type": "-", + "size": 1920, + "lmtime": 1741161627962, + "modified": false + }, + "stores.tpl": { + "type": "-", + "size": 3372, + "lmtime": 1741161628036, + "modified": false + } + }, + "contact.tpl": { + "type": "-", + "size": 1298, + "lmtime": 1741161620964, + "modified": false + }, + "customer": { + "addresses.tpl": { + "type": "-", + "size": 1692, + "lmtime": 1741161628256, + "modified": false + }, + "address.tpl": { + "type": "-", + "size": 1356, + "lmtime": 1741161628345, + "modified": false + }, + "authentication.tpl": { + "type": "-", + "size": 1662, + "lmtime": 1741161628423, + "modified": false + }, + "discount.tpl": { + "type": "-", + "size": 3458, + "lmtime": 1741161628513, + "modified": false + }, + "guest-login.tpl": { + "type": "-", + "size": 2656, + "lmtime": 1741161628595, + "modified": false + }, + "guest-tracking.tpl": { + "type": "-", + "size": 2578, + "lmtime": 1741161628670, + "modified": false + }, + "history.tpl": { + "type": "-", + "size": 4705, + "lmtime": 1741161628743, + "modified": false + }, + "identity.tpl": { + "type": "-", + "size": 1225, + "lmtime": 1741161628812, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161628901, + "modified": false + }, + "my-account.tpl": { + "type": "-", + "size": 3887, + "lmtime": 1741161628989, + "modified": false + }, + "order-detail.tpl": { + "type": "-", + "size": 7845, + "lmtime": 1741161629060, + "modified": false + }, + "order-follow.tpl": { + "type": "-", + "size": 3569, + "lmtime": 1741161629145, + "modified": false + }, + "order-return.tpl": { + "type": "-", + "size": 6469, + "lmtime": 1741161629212, + "modified": false + }, + "order-slip.tpl": { + "type": "-", + "size": 3078, + "lmtime": 1741161629288, + "modified": false + }, + "page.tpl": { + "type": "-", + "size": 1529, + "lmtime": 1741161629367, + "modified": false + }, + "_partials": { + "address-form.tpl": { + "type": "-", + "size": 2182, + "lmtime": 1741161629769, + "modified": false + }, + "block-address.tpl": { + "type": "-", + "size": 1825, + "lmtime": 1741161629845, + "modified": false + }, + "customer-form.tpl": { + "type": "-", + "size": 1975, + "lmtime": 1741161629930, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161629997, + "modified": false + }, + "login-form.tpl": { + "type": "-", + "size": 2115, + "lmtime": 1741161630092, + "modified": false + }, + "my-account-links.tpl": { + "type": "-", + "size": 1364, + "lmtime": 1741161630163, + "modified": false + }, + "order-detail-no-return.tpl": { + "type": "-", + "size": 7318, + "lmtime": 1741161630243, + "modified": false + }, + "order-detail-return.tpl": { + "type": "-", + "size": 12217, + "lmtime": 1741161630336, + "modified": false + }, + "order-messages.tpl": { + "type": "-", + "size": 3063, + "lmtime": 1741161630408, + "modified": false + } + }, + "password-email.tpl": { + "type": "-", + "size": 2928, + "lmtime": 1741161629458, + "modified": false + }, + "password-infos.tpl": { + "type": "-", + "size": 1622, + "lmtime": 1741161629533, + "modified": false + }, + "password-new.tpl": { + "type": "-", + "size": 3378, + "lmtime": 1741161629617, + "modified": false + }, + "registration.tpl": { + "type": "-", + "size": 1549, + "lmtime": 1741161629696, + "modified": false + } + }, + "errors": { + "404.tpl": { + "type": "-", + "size": 1147, + "lmtime": 1741161630480, + "modified": false + }, + "forbidden.tpl": { + "type": "-", + "size": 1072, + "lmtime": 1741161630557, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161630630, + "modified": false + }, + "maintenance.tpl": { + "type": "-", + "size": 1866, + "lmtime": 1741161630710, + "modified": false + }, + "not-found.tpl": { + "type": "-", + "size": 1397, + "lmtime": 1741161630818, + "modified": false + }, + "restricted-country.tpl": { + "type": "-", + "size": 1827, + "lmtime": 1741161630899, + "modified": false + }, + "static": { + "500.html": { + "type": "-", + "size": 0, + "lmtime": 1741161630984, + "modified": false + }, + "503.html": { + "type": "-", + "size": 0, + "lmtime": 1741161631054, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161631151, + "modified": false + } + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161621054, + "modified": false + }, + "index.tpl": { + "type": "-", + "size": 1317, + "lmtime": 1741161621147, + "modified": false + }, + "layouts": { + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161631247, + "modified": false + }, + "layout-both-columns.tpl": { + "type": "-", + "size": 5330, + "lmtime": 1741161631327, + "modified": false + }, + "layout-content-only.tpl": { + "type": "-", + "size": 1439, + "lmtime": 1741161631397, + "modified": false + }, + "layout-error.tpl": { + "type": "-", + "size": 2065, + "lmtime": 1741161631471, + "modified": false + }, + "layout-full-width.tpl": { + "type": "-", + "size": 1731, + "lmtime": 1741161631548, + "modified": false + }, + "layout-left-column.tpl": { + "type": "-", + "size": 1380, + "lmtime": 1741161631625, + "modified": false + }, + "layout-right-column.tpl": { + "type": "-", + "size": 1380, + "lmtime": 1741161631701, + "modified": false + }, + "setting.tpl": { + "type": "-", + "size": 587, + "lmtime": 1741161631780, + "modified": false + } + }, + "page.tpl": { + "type": "-", + "size": 1742, + "lmtime": 1741161621236, + "modified": false + }, + "_partials": { + "breadcrumb.tpl": { + "type": "-", + "size": 1630, + "lmtime": 1741161631855, + "modified": false + }, + "footer.tpl": { + "type": "-", + "size": 2350, + "lmtime": 1741161631928, + "modified": false + }, + "form-errors.tpl": { + "type": "-", + "size": 1219, + "lmtime": 1741161632011, + "modified": false + }, + "form-fields.tpl": { + "type": "-", + "size": 7260, + "lmtime": 1741161632084, + "modified": false + }, + "header.tpl": { + "type": "-", + "size": 2550, + "lmtime": 1741161632153, + "modified": false + }, + "head.tpl": { + "type": "-", + "size": 6382, + "lmtime": 1741161632241, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161632333, + "modified": false + }, + "javascript.tpl": { + "type": "-", + "size": 2137, + "lmtime": 1741161632407, + "modified": false + }, + "notifications.tpl": { + "type": "-", + "size": 2462, + "lmtime": 1741161632491, + "modified": false + }, + "pagination.tpl": { + "type": "-", + "size": 2769, + "lmtime": 1741161632595, + "modified": false + }, + "stylesheets.tpl": { + "type": "-", + "size": 1238, + "lmtime": 1741161632674, + "modified": false + } + }, + "sub": { + "index.php": { + "type": "-", + "size": 1277, + "lmtime": 1741161632755, + "modified": false + }, + "product_info": { + "accordions.tpl": { + "type": "-", + "size": 8678, + "lmtime": 1741161632845, + "modified": false + }, + "default.tpl": { + "type": "-", + "size": 8994, + "lmtime": 1741161632924, + "modified": false + }, + "index.php": { + "type": "-", + "size": 1277, + "lmtime": 1741161633016, + "modified": false + }, + "tab.tpl": { + "type": "-", + "size": 3453, + "lmtime": 1741161633111, + "modified": false + } + } + } + }, + "themes": { + "leo_gstoremails": { + "themes": { + "leo_gstoreen": { + "themes": { + "leo_gstoreaccount.html": { + "type": "-", + "size": 6830, + "lmtime": 1741161633215, + "modified": false + }, + "leo_gstoreaccount.txt": { + "type": "-", + "size": 554, + "lmtime": 1741161633300, + "modified": false + }, + "leo_gstorebackoffice_order.html": { + "type": "-", + "size": 5287, + "lmtime": 1741161633390, + "modified": false + }, + "leo_gstorebackoffice_order.txt": { + "type": "-", + "size": 247, + "lmtime": 1741161633457, + "modified": false + }, + "leo_gstorebankwire.html": { + "type": "-", + "size": 7624, + "lmtime": 1741161633605, + "modified": false + }, + "leo_gstorebankwire.txt": { + "type": "-", + "size": 897, + "lmtime": 1741161633698, + "modified": false + }, + "leo_gstorecheque.html": { + "type": "-", + "size": 7441, + "lmtime": 1741161633785, + "modified": false + }, + "leo_gstorecheque.txt": { + "type": "-", + "size": 799, + "lmtime": 1741161633883, + "modified": false + }, + "leo_gstorecontact_form.html": { + "type": "-", + "size": 5583, + "lmtime": 1741161633986, + "modified": false + }, + "leo_gstorecontact_form.txt": { + "type": "-", + "size": 310, + "lmtime": 1741161634072, + "modified": false + }, + "leo_gstorecontact.html": { + "type": "-", + "size": 5402, + "lmtime": 1741161634175, + "modified": false + }, + "leo_gstorecontact.txt": { + "type": "-", + "size": 259, + "lmtime": 1741161634270, + "modified": false + }, + "leo_gstorecredit_slip.html": { + "type": "-", + "size": 5834, + "lmtime": 1741161634370, + "modified": false + }, + "leo_gstorecredit_slip.txt": { + "type": "-", + "size": 401, + "lmtime": 1741161634466, + "modified": false + }, + "leo_gstoredownload_product.html": { + "type": "-", + "size": 6410, + "lmtime": 1741161634572, + "modified": false + }, + "leo_gstoredownload_product.txt": { + "type": "-", + "size": 648, + "lmtime": 1741161634672, + "modified": false + }, + "leo_gstoreemployee_password.html": { + "type": "-", + "size": 5560, + "lmtime": 1741161634773, + "modified": false + }, + "leo_gstoreemployee_password.txt": { + "type": "-", + "size": 262, + "lmtime": 1741161634877, + "modified": false + }, + "leo_gstoreforward_msg.html": { + "type": "-", + "size": 5548, + "lmtime": 1741161634971, + "modified": false + }, + "leo_gstoreforward_msg.txt": { + "type": "-", + "size": 248, + "lmtime": 1741161635081, + "modified": false + }, + "leo_gstoreguest_to_customer.html": { + "type": "-", + "size": 5946, + "lmtime": 1741161635184, + "modified": false + }, + "leo_gstoreguest_to_customer.txt": { + "type": "-", + "size": 359, + "lmtime": 1741161635275, + "modified": false + }, + "leo_gstoreimport.html": { + "type": "-", + "size": 5321, + "lmtime": 1741161635382, + "modified": false + }, + "leo_gstoreimport.txt": { + "type": "-", + "size": 194, + "lmtime": 1741161635470, + "modified": false + }, + "leo_gstorein_transit.html": { + "type": "-", + "size": 7370, + "lmtime": 1741161635562, + "modified": false + }, + "leo_gstorein_transit.txt": { + "type": "-", + "size": 635, + "lmtime": 1741161635662, + "modified": false + }, + "leo_gstorelog_alert.html": { + "type": "-", + "size": 5450, + "lmtime": 1741161635761, + "modified": false + }, + "leo_gstorelog_alert.txt": { + "type": "-", + "size": 271, + "lmtime": 1741161635870, + "modified": false + }, + "leo_gstorenewsletter.html": { + "type": "-", + "size": 4447, + "lmtime": 1741161635972, + "modified": false + }, + "leo_gstorenewsletter.txt": { + "type": "-", + "size": 139, + "lmtime": 1741161636072, + "modified": false + }, + "leo_gstoreorder_canceled.html": { + "type": "-", + "size": 6211, + "lmtime": 1741161636161, + "modified": false + }, + "leo_gstoreorder_canceled.txt": { + "type": "-", + "size": 541, + "lmtime": 1741161636250, + "modified": false + }, + "leo_gstoreorder_changed.html": { + "type": "-", + "size": 6225, + "lmtime": 1741161636347, + "modified": false + }, + "leo_gstoreorder_changed.txt": { + "type": "-", + "size": 556, + "lmtime": 1741161636443, + "modified": false + }, + "leo_gstoreorder_conf.html": { + "type": "-", + "size": 17027, + "lmtime": 1741161636566, + "modified": false + }, + "leo_gstoreorder_conf.txt": { + "type": "-", + "size": 930, + "lmtime": 1741161636650, + "modified": false + }, + "leo_gstoreorder_customer_comment.html": { + "type": "-", + "size": 5430, + "lmtime": 1741161636741, + "modified": false + }, + "leo_gstoreorder_customer_comment.txt": { + "type": "-", + "size": 245, + "lmtime": 1741161636834, + "modified": false + }, + "leo_gstoreorder_merchant_comment.html": { + "type": "-", + "size": 5468, + "lmtime": 1741161636933, + "modified": false + }, + "leo_gstoreorder_merchant_comment.txt": { + "type": "-", + "size": 250, + "lmtime": 1741161637041, + "modified": false + }, + "leo_gstoreorder_return_state.html": { + "type": "-", + "size": 5844, + "lmtime": 1741161637136, + "modified": false + }, + "leo_gstoreorder_return_state.txt": { + "type": "-", + "size": 419, + "lmtime": 1741161637231, + "modified": false + }, + "leo_gstoreoutofstock.html": { + "type": "-", + "size": 6427, + "lmtime": 1741161637349, + "modified": false + }, + "leo_gstoreoutofstock.txt": { + "type": "-", + "size": 731, + "lmtime": 1741161637455, + "modified": false + }, + "leo_gstorepassword.html": { + "type": "-", + "size": 6097, + "lmtime": 1741161637551, + "modified": false + }, + "leo_gstorepassword_query.html": { + "type": "-", + "size": 5508, + "lmtime": 1741161637663, + "modified": false + }, + "leo_gstorepassword_query.txt": { + "type": "-", + "size": 322, + "lmtime": 1741161637751, + "modified": false + }, + "leo_gstorepassword.txt": { + "type": "-", + "size": 486, + "lmtime": 1741161637850, + "modified": false + }, + "leo_gstorepayment_error.html": { + "type": "-", + "size": 6401, + "lmtime": 1741161637950, + "modified": false + }, + "leo_gstorepayment_error.txt": { + "type": "-", + "size": 663, + "lmtime": 1741161638044, + "modified": false + }, + "leo_gstorepayment.html": { + "type": "-", + "size": 6330, + "lmtime": 1741161638133, + "modified": false + }, + "leo_gstorepayment.txt": { + "type": "-", + "size": 587, + "lmtime": 1741161638232, + "modified": false + }, + "leo_gstorepreparation.html": { + "type": "-", + "size": 6212, + "lmtime": 1741161638347, + "modified": false + }, + "leo_gstorepreparation.txt": { + "type": "-", + "size": 546, + "lmtime": 1741161638424, + "modified": false + }, + "leo_gstoreproductoutofstock.html": { + "type": "-", + "size": 5501, + "lmtime": 1741161638525, + "modified": false + }, + "leo_gstoreproductoutofstock.txt": { + "type": "-", + "size": 303, + "lmtime": 1741161638606, + "modified": false + }, + "leo_gstorerefund.html": { + "type": "-", + "size": 6220, + "lmtime": 1741161638695, + "modified": false + }, + "leo_gstorerefund.txt": { + "type": "-", + "size": 547, + "lmtime": 1741161638791, + "modified": false + }, + "leo_gstorereply_msg.html": { + "type": "-", + "size": 4875, + "lmtime": 1741161638881, + "modified": false + }, + "leo_gstorereply_msg.txt": { + "type": "-", + "size": 275, + "lmtime": 1741161638983, + "modified": false + }, + "leo_gstoreshipped.html": { + "type": "-", + "size": 5845, + "lmtime": 1741161639065, + "modified": false + }, + "leo_gstoreshipped.txt": { + "type": "-", + "size": 449, + "lmtime": 1741161639169, + "modified": false + }, + "leo_gstoretest.html": { + "type": "-", + "size": 4628, + "lmtime": 1741161639265, + "modified": false + }, + "leo_gstoretest.txt": { + "type": "-", + "size": 195, + "lmtime": 1741161639362, + "modified": false + }, + "leo_gstorevoucher.html": { + "type": "-", + "size": 5625, + "lmtime": 1741161639460, + "modified": false + }, + "leo_gstorevoucher_new.html": { + "type": "-", + "size": 5403, + "lmtime": 1741161639559, + "modified": false + }, + "leo_gstorevoucher_new.txt": { + "type": "-", + "size": 257, + "lmtime": 1741161639649, + "modified": false + }, + "leo_gstorevoucher.txt": { + "type": "-", + "size": 371, + "lmtime": 1741161639756, + "modified": false + } + } + }, + "leo_gstorepl": { + "themes": { + "leo_gstoreaccount.html": { + "type": "-", + "size": 6933, + "lmtime": 1741161639861, + "modified": false + }, + "leo_gstoreaccount.txt": { + "type": "-", + "size": 650, + "lmtime": 1741161639959, + "modified": false + }, + "leo_gstorebackoffice_order.html": { + "type": "-", + "size": 5305, + "lmtime": 1741161640047, + "modified": false + }, + "leo_gstorebackoffice_order.txt": { + "type": "-", + "size": 263, + "lmtime": 1741161640121, + "modified": false + }, + "leo_gstorebankwire.html": { + "type": "-", + "size": 10302, + "lmtime": 1741161640200, + "modified": false + }, + "leo_gstorebankwire.txt": { + "type": "-", + "size": 927, + "lmtime": 1741161640277, + "modified": false + }, + "leo_gstorecheque.html": { + "type": "-", + "size": 10125, + "lmtime": 1741161640364, + "modified": false + }, + "leo_gstorecheque.txt": { + "type": "-", + "size": 815, + "lmtime": 1741161640446, + "modified": false + }, + "leo_gstorecontact_form.html": { + "type": "-", + "size": 5626, + "lmtime": 1741161640523, + "modified": false + }, + "leo_gstorecontact_form.txt": { + "type": "-", + "size": 351, + "lmtime": 1741161640607, + "modified": false + }, + "leo_gstorecontact.html": { + "type": "-", + "size": 5412, + "lmtime": 1741161640715, + "modified": false + }, + "leo_gstorecontact.txt": { + "type": "-", + "size": 267, + "lmtime": 1741161640808, + "modified": false + }, + "leo_gstorecredit_slip.html": { + "type": "-", + "size": 5857, + "lmtime": 1741161640913, + "modified": false + }, + "leo_gstorecredit_slip.txt": { + "type": "-", + "size": 405, + "lmtime": 1741161641021, + "modified": false + }, + "leo_gstoredownload_product.html": { + "type": "-", + "size": 8731, + "lmtime": 1741161641102, + "modified": false + }, + "leo_gstoredownload_product.txt": { + "type": "-", + "size": 682, + "lmtime": 1741161641182, + "modified": false + }, + "leo_gstoreemployee_password.html": { + "type": "-", + "size": 5560, + "lmtime": 1741161641292, + "modified": false + }, + "leo_gstoreemployee_password.txt": { + "type": "-", + "size": 257, + "lmtime": 1741161641387, + "modified": false + }, + "leo_gstoreforward_msg.html": { + "type": "-", + "size": 5554, + "lmtime": 1741161641499, + "modified": false + }, + "leo_gstoreforward_msg.txt": { + "type": "-", + "size": 253, + "lmtime": 1741161641603, + "modified": false + }, + "leo_gstoreguest_to_customer.html": { + "type": "-", + "size": 5985, + "lmtime": 1741161641702, + "modified": false + }, + "leo_gstoreguest_to_customer.txt": { + "type": "-", + "size": 397, + "lmtime": 1741161641786, + "modified": false + }, + "leo_gstoreimport.html": { + "type": "-", + "size": 5337, + "lmtime": 1741161641880, + "modified": false + }, + "leo_gstoreimport.txt": { + "type": "-", + "size": 205, + "lmtime": 1741161641976, + "modified": false + }, + "leo_gstorein_transit.html": { + "type": "-", + "size": 10112, + "lmtime": 1741161642075, + "modified": false + }, + "leo_gstorein_transit.txt": { + "type": "-", + "size": 677, + "lmtime": 1741161642174, + "modified": false + }, + "leo_gstorelog_alert.html": { + "type": "-", + "size": 5462, + "lmtime": 1741161642278, + "modified": false + }, + "leo_gstorelog_alert.txt": { + "type": "-", + "size": 281, + "lmtime": 1741161642367, + "modified": false + }, + "leo_gstorenewsletter.html": { + "type": "-", + "size": 4452, + "lmtime": 1741161642456, + "modified": false + }, + "leo_gstorenewsletter.txt": { + "type": "-", + "size": 142, + "lmtime": 1741161642553, + "modified": false + }, + "leo_gstoreorder_canceled.html": { + "type": "-", + "size": 8472, + "lmtime": 1741161642641, + "modified": false + }, + "leo_gstoreorder_canceled.txt": { + "type": "-", + "size": 563, + "lmtime": 1741161642740, + "modified": false + }, + "leo_gstoreorder_changed.html": { + "type": "-", + "size": 8490, + "lmtime": 1741161642857, + "modified": false + }, + "leo_gstoreorder_changed.txt": { + "type": "-", + "size": 581, + "lmtime": 1741161642948, + "modified": false + }, + "leo_gstoreorder_conf.html": { + "type": "-", + "size": 17503, + "lmtime": 1741161643033, + "modified": false + }, + "leo_gstoreorder_conf.txt": { + "type": "-", + "size": 1038, + "lmtime": 1741161643109, + "modified": false + }, + "leo_gstoreorder_customer_comment.html": { + "type": "-", + "size": 5426, + "lmtime": 1741161643193, + "modified": false + }, + "leo_gstoreorder_customer_comment.txt": { + "type": "-", + "size": 240, + "lmtime": 1741161643287, + "modified": false + }, + "leo_gstoreorder_merchant_comment.html": { + "type": "-", + "size": 5471, + "lmtime": 1741161643387, + "modified": false + }, + "leo_gstoreorder_merchant_comment.txt": { + "type": "-", + "size": 249, + "lmtime": 1741161643478, + "modified": false + }, + "leo_gstoreorder_return_state.html": { + "type": "-", + "size": 8013, + "lmtime": 1741161643572, + "modified": false + }, + "leo_gstoreorder_return_state.txt": { + "type": "-", + "size": 446, + "lmtime": 1741161643672, + "modified": false + }, + "leo_gstoreoutofstock.html": { + "type": "-", + "size": 8710, + "lmtime": 1741161643773, + "modified": false + }, + "leo_gstoreoutofstock.txt": { + "type": "-", + "size": 776, + "lmtime": 1741161643874, + "modified": false + }, + "leo_gstorepassword.html": { + "type": "-", + "size": 8383, + "lmtime": 1741161643967, + "modified": false + }, + "leo_gstorepassword_query.html": { + "type": "-", + "size": 5538, + "lmtime": 1741161644073, + "modified": false + }, + "leo_gstorepassword_query.txt": { + "type": "-", + "size": 350, + "lmtime": 1741161644184, + "modified": false + }, + "leo_gstorepassword.txt": { + "type": "-", + "size": 509, + "lmtime": 1741161644288, + "modified": false + }, + "leo_gstorepayment_error.html": { + "type": "-", + "size": 8726, + "lmtime": 1741161644395, + "modified": false + }, + "leo_gstorepayment_error.txt": { + "type": "-", + "size": 706, + "lmtime": 1741161644500, + "modified": false + }, + "leo_gstorepayment.html": { + "type": "-", + "size": 8621, + "lmtime": 1741161644616, + "modified": false + }, + "leo_gstorepayment.txt": { + "type": "-", + "size": 627, + "lmtime": 1741161644724, + "modified": false + }, + "leo_gstorepreparation.html": { + "type": "-", + "size": 8468, + "lmtime": 1741161644825, + "modified": false + }, + "leo_gstorepreparation.txt": { + "type": "-", + "size": 570, + "lmtime": 1741161644923, + "modified": false + }, + "leo_gstoreproductoutofstock.html": { + "type": "-", + "size": 5522, + "lmtime": 1741161645016, + "modified": false + }, + "leo_gstoreproductoutofstock.txt": { + "type": "-", + "size": 325, + "lmtime": 1741161645113, + "modified": false + }, + "leo_gstorerefund.html": { + "type": "-", + "size": 8497, + "lmtime": 1741161645215, + "modified": false + }, + "leo_gstorerefund.txt": { + "type": "-", + "size": 592, + "lmtime": 1741161645308, + "modified": false + }, + "leo_gstorereply_msg.html": { + "type": "-", + "size": 4880, + "lmtime": 1741161645404, + "modified": false + }, + "leo_gstorereply_msg.txt": { + "type": "-", + "size": 278, + "lmtime": 1741161645490, + "modified": false + }, + "leo_gstoreshipped.html": { + "type": "-", + "size": 7967, + "lmtime": 1741161645564, + "modified": false + }, + "leo_gstoreshipped.txt": { + "type": "-", + "size": 482, + "lmtime": 1741161645665, + "modified": false + }, + "leo_gstoretest.html": { + "type": "-", + "size": 4668, + "lmtime": 1741161645741, + "modified": false + }, + "leo_gstoretest.txt": { + "type": "-", + "size": 232, + "lmtime": 1741161645823, + "modified": false + }, + "leo_gstorevoucher.html": { + "type": "-", + "size": 5638, + "lmtime": 1741161645914, + "modified": false + }, + "leo_gstorevoucher_new.html": { + "type": "-", + "size": 5406, + "lmtime": 1741161646003, + "modified": false + }, + "leo_gstorevoucher_new.txt": { + "type": "-", + "size": 261, + "lmtime": 1741161646086, + "modified": false + }, + "leo_gstorevoucher.txt": { + "type": "-", + "size": 376, + "lmtime": 1741161646155, + "modified": false + } + } + }, + "leo_gstoresk": { + "themes": { + "leo_gstoreaccount.html": { + "type": "-", + "size": 6912, + "lmtime": 1741161646241, + "modified": false + }, + "leo_gstoreaccount.txt": { + "type": "-", + "size": 613, + "lmtime": 1741161646348, + "modified": false + }, + "leo_gstorebackoffice_order.html": { + "type": "-", + "size": 5308, + "lmtime": 1741161646440, + "modified": false + }, + "leo_gstorebackoffice_order.txt": { + "type": "-", + "size": 263, + "lmtime": 1741161646538, + "modified": false + }, + "leo_gstorebankwire.html": { + "type": "-", + "size": 7696, + "lmtime": 1741161646635, + "modified": false + }, + "leo_gstorebankwire.txt": { + "type": "-", + "size": 941, + "lmtime": 1741161646729, + "modified": false + }, + "leo_gstorecheque.html": { + "type": "-", + "size": 7494, + "lmtime": 1741161646827, + "modified": false + }, + "leo_gstorecheque.txt": { + "type": "-", + "size": 837, + "lmtime": 1741161646933, + "modified": false + }, + "leo_gstorecontact_form.html": { + "type": "-", + "size": 5599, + "lmtime": 1741161647033, + "modified": false + }, + "leo_gstorecontact_form.txt": { + "type": "-", + "size": 321, + "lmtime": 1741161647121, + "modified": false + }, + "leo_gstorecontact.html": { + "type": "-", + "size": 5434, + "lmtime": 1741161647212, + "modified": false + }, + "leo_gstorecontact.txt": { + "type": "-", + "size": 286, + "lmtime": 1741161647300, + "modified": false + }, + "leo_gstorecredit_slip.html": { + "type": "-", + "size": 5818, + "lmtime": 1741161647390, + "modified": false + }, + "leo_gstorecredit_slip.txt": { + "type": "-", + "size": 373, + "lmtime": 1741161647482, + "modified": false + }, + "leo_gstoredownload_product.html": { + "type": "-", + "size": 6472, + "lmtime": 1741161647588, + "modified": false + }, + "leo_gstoredownload_product.txt": { + "type": "-", + "size": 707, + "lmtime": 1741161647677, + "modified": false + }, + "leo_gstoreemployee_password.html": { + "type": "-", + "size": 5585, + "lmtime": 1741161647777, + "modified": false + }, + "leo_gstoreemployee_password.txt": { + "type": "-", + "size": 267, + "lmtime": 1741161647865, + "modified": false + }, + "leo_gstoreforward_msg.html": { + "type": "-", + "size": 5572, + "lmtime": 1741161647989, + "modified": false + }, + "leo_gstoreforward_msg.txt": { + "type": "-", + "size": 264, + "lmtime": 1741161648072, + "modified": false + }, + "leo_gstoreguest_to_customer.html": { + "type": "-", + "size": 6022, + "lmtime": 1741161648176, + "modified": false + }, + "leo_gstoreguest_to_customer.txt": { + "type": "-", + "size": 420, + "lmtime": 1741161648277, + "modified": false + }, + "leo_gstoreimport.html": { + "type": "-", + "size": 5342, + "lmtime": 1741161648368, + "modified": false + }, + "leo_gstoreimport.txt": { + "type": "-", + "size": 203, + "lmtime": 1741161648469, + "modified": false + }, + "leo_gstorein_transit.html": { + "type": "-", + "size": 7453, + "lmtime": 1741161648569, + "modified": false + }, + "leo_gstorein_transit.txt": { + "type": "-", + "size": 689, + "lmtime": 1741161648663, + "modified": false + }, + "leo_gstorelog_alert.html": { + "type": "-", + "size": 5477, + "lmtime": 1741161648778, + "modified": false + }, + "leo_gstorelog_alert.txt": { + "type": "-", + "size": 282, + "lmtime": 1741161648881, + "modified": false + }, + "leo_gstorenewsletter.html": { + "type": "-", + "size": 4461, + "lmtime": 1741161648987, + "modified": false + }, + "leo_gstorenewsletter.txt": { + "type": "-", + "size": 148, + "lmtime": 1741161649089, + "modified": false + }, + "leo_gstoreorder_canceled.html": { + "type": "-", + "size": 6283, + "lmtime": 1741161649170, + "modified": false + }, + "leo_gstoreorder_canceled.txt": { + "type": "-", + "size": 589, + "lmtime": 1741161649282, + "modified": false + }, + "leo_gstoreorder_changed.html": { + "type": "-", + "size": 6300, + "lmtime": 1741161649365, + "modified": false + }, + "leo_gstoreorder_changed.txt": { + "type": "-", + "size": 601, + "lmtime": 1741161649449, + "modified": false + }, + "leo_gstoreorder_conf.html": { + "type": "-", + "size": 17119, + "lmtime": 1741161649547, + "modified": false + }, + "leo_gstoreorder_conf.txt": { + "type": "-", + "size": 1003, + "lmtime": 1741161649631, + "modified": false + }, + "leo_gstoreorder_customer_comment.html": { + "type": "-", + "size": 5423, + "lmtime": 1741161649709, + "modified": false + }, + "leo_gstoreorder_customer_comment.txt": { + "type": "-", + "size": 234, + "lmtime": 1741161649799, + "modified": false + }, + "leo_gstoreorder_merchant_comment.html": { + "type": "-", + "size": 5485, + "lmtime": 1741161649889, + "modified": false + }, + "leo_gstoreorder_merchant_comment.txt": { + "type": "-", + "size": 257, + "lmtime": 1741161649989, + "modified": false + }, + "leo_gstoreorder_return_state.html": { + "type": "-", + "size": 5864, + "lmtime": 1741161650087, + "modified": false + }, + "leo_gstoreorder_return_state.txt": { + "type": "-", + "size": 417, + "lmtime": 1741161650180, + "modified": false + }, + "leo_gstoreoutofstock.html": { + "type": "-", + "size": 6570, + "lmtime": 1741161650277, + "modified": false + }, + "leo_gstoreoutofstock.txt": { + "type": "-", + "size": 848, + "lmtime": 1741161650371, + "modified": false + }, + "leo_gstorepassword.html": { + "type": "-", + "size": 6163, + "lmtime": 1741161650469, + "modified": false + }, + "leo_gstorepassword_query.html": { + "type": "-", + "size": 5536, + "lmtime": 1741161650577, + "modified": false + }, + "leo_gstorepassword_query.txt": { + "type": "-", + "size": 337, + "lmtime": 1741161650682, + "modified": false + }, + "leo_gstorepassword.txt": { + "type": "-", + "size": 525, + "lmtime": 1741161650779, + "modified": false + }, + "leo_gstorepayment_error.html": { + "type": "-", + "size": 6487, + "lmtime": 1741161650880, + "modified": false + }, + "leo_gstorepayment_error.txt": { + "type": "-", + "size": 729, + "lmtime": 1741161650964, + "modified": false + }, + "leo_gstorepayment.html": { + "type": "-", + "size": 6400, + "lmtime": 1741161651057, + "modified": false + }, + "leo_gstorepayment.txt": { + "type": "-", + "size": 630, + "lmtime": 1741161651165, + "modified": false + }, + "leo_gstorepreparation.html": { + "type": "-", + "size": 6277, + "lmtime": 1741161651275, + "modified": false + }, + "leo_gstorepreparation.txt": { + "type": "-", + "size": 595, + "lmtime": 1741161651385, + "modified": false + }, + "leo_gstoreproductoutofstock.html": { + "type": "-", + "size": 5504, + "lmtime": 1741161651484, + "modified": false + }, + "leo_gstoreproductoutofstock.txt": { + "type": "-", + "size": 308, + "lmtime": 1741161651587, + "modified": false + }, + "leo_gstorerefund.html": { + "type": "-", + "size": 6313, + "lmtime": 1741161651687, + "modified": false + }, + "leo_gstorerefund.txt": { + "type": "-", + "size": 611, + "lmtime": 1741161651787, + "modified": false + }, + "leo_gstorereply_msg.html": { + "type": "-", + "size": 4886, + "lmtime": 1741161651876, + "modified": false + }, + "leo_gstorereply_msg.txt": { + "type": "-", + "size": 281, + "lmtime": 1741161651980, + "modified": false + }, + "leo_gstoreshipped.html": { + "type": "-", + "size": 5857, + "lmtime": 1741161652073, + "modified": false + }, + "leo_gstoreshipped.txt": { + "type": "-", + "size": 448, + "lmtime": 1741161652179, + "modified": false + }, + "leo_gstoretest.html": { + "type": "-", + "size": 4637, + "lmtime": 1741161652281, + "modified": false + }, + "leo_gstoretest.txt": { + "type": "-", + "size": 200, + "lmtime": 1741161652369, + "modified": false + }, + "leo_gstorevoucher.html": { + "type": "-", + "size": 5659, + "lmtime": 1741161652476, + "modified": false + }, + "leo_gstorevoucher_new.html": { + "type": "-", + "size": 5445, + "lmtime": 1741161652564, + "modified": false + }, + "leo_gstorevoucher_new.txt": { + "type": "-", + "size": 286, + "lmtime": 1741161652649, + "modified": false + }, + "leo_gstorevoucher.txt": { + "type": "-", + "size": 382, + "lmtime": 1741161652739, + "modified": false + } + } + } + } + } + }, + "translations": { + "ar-SA": { + "ShopFormsHelp.ar-SA.xlf": { + "type": "-", + "size": 3617, + "lmtime": 1741161652922, + "modified": false + }, + "ShopFormsLabels.ar-SA.xlf": { + "type": "-", + "size": 7785, + "lmtime": 1741161652996, + "modified": false + }, + "ShopThemeActions.ar-SA.xlf": { + "type": "-", + "size": 23317, + "lmtime": 1741161653092, + "modified": false + }, + "ShopThemeCatalog.ar-SA.xlf": { + "type": "-", + "size": 28716, + "lmtime": 1741161653196, + "modified": false + }, + "ShopThemeCheckout.ar-SA.xlf": { + "type": "-", + "size": 30526, + "lmtime": 1741161653300, + "modified": false + }, + "ShopThemeCustomeraccount.ar-SA.xlf": { + "type": "-", + "size": 32688, + "lmtime": 1741161653409, + "modified": false + }, + "ShopThemeGlobal.ar-SA.xlf": { + "type": "-", + "size": 39394, + "lmtime": 1741161653526, + "modified": false + } + }, + "de-DE": { + "ShopFormsHelp.de-DE.xlf": { + "type": "-", + "size": 3548, + "lmtime": 1741161653623, + "modified": false + }, + "ShopFormsLabels.de-DE.xlf": { + "type": "-", + "size": 6949, + "lmtime": 1741161653713, + "modified": false + }, + "ShopThemeActions.de-DE.xlf": { + "type": "-", + "size": 23072, + "lmtime": 1741161653799, + "modified": false + }, + "ShopThemeCatalog.de-DE.xlf": { + "type": "-", + "size": 27000, + "lmtime": 1741161653908, + "modified": false + }, + "ShopThemeCheckout.de-DE.xlf": { + "type": "-", + "size": 28241, + "lmtime": 1741161654024, + "modified": false + }, + "ShopThemeCustomeraccount.de-DE.xlf": { + "type": "-", + "size": 31540, + "lmtime": 1741161654136, + "modified": false + }, + "ShopThemeGlobal.de-DE.xlf": { + "type": "-", + "size": 38781, + "lmtime": 1741161654246, + "modified": false + } + }, + "en-US": { + "ShopFormsHelp.en-US.xlf": { + "type": "-", + "size": 3508, + "lmtime": 1741161654330, + "modified": false + }, + "ShopFormsLabels.en-US.xlf": { + "type": "-", + "size": 6953, + "lmtime": 1741161654416, + "modified": false + }, + "ShopThemeActions.en-US.xlf": { + "type": "-", + "size": 22947, + "lmtime": 1741161654526, + "modified": false + }, + "ShopThemeCatalog.en-US.xlf": { + "type": "-", + "size": 26961, + "lmtime": 1741161654621, + "modified": false + }, + "ShopThemeCheckout.en-US.xlf": { + "type": "-", + "size": 28011, + "lmtime": 1741161654745, + "modified": false + }, + "ShopThemeCustomeraccount.en-US.xlf": { + "type": "-", + "size": 31059, + "lmtime": 1741161654865, + "modified": false + }, + "ShopThemeGlobal.en-US.xlf": { + "type": "-", + "size": 39119, + "lmtime": 1741161654983, + "modified": false + } + }, + "es-ES": { + "ShopFormsHelp.es-ES.xlf": { + "type": "-", + "size": 3535, + "lmtime": 1741161655086, + "modified": false + }, + "ShopFormsLabels.es-ES.xlf": { + "type": "-", + "size": 6986, + "lmtime": 1741161655178, + "modified": false + }, + "ShopThemeActions.es-ES.xlf": { + "type": "-", + "size": 23020, + "lmtime": 1741161655290, + "modified": false + }, + "ShopThemeCatalog.es-ES.xlf": { + "type": "-", + "size": 27148, + "lmtime": 1741161655405, + "modified": false + }, + "ShopThemeCheckout.es-ES.xlf": { + "type": "-", + "size": 28158, + "lmtime": 1741161655526, + "modified": false + }, + "ShopThemeCustomeraccount.es-ES.xlf": { + "type": "-", + "size": 31478, + "lmtime": 1741161655642, + "modified": false + }, + "ShopThemeGlobal.es-ES.xlf": { + "type": "-", + "size": 38766, + "lmtime": 1741161655758, + "modified": false + } + }, + "fr-FR": { + "ShopFormsHelp.fr-FR.xlf": { + "type": "-", + "size": 3552, + "lmtime": 1741161655852, + "modified": false + }, + "ShopFormsLabels.fr-FR.xlf": { + "type": "-", + "size": 6997, + "lmtime": 1741161655950, + "modified": false + }, + "ShopThemeActions.fr-FR.xlf": { + "type": "-", + "size": 23095, + "lmtime": 1741161656063, + "modified": false + }, + "ShopThemeCatalog.fr-FR.xlf": { + "type": "-", + "size": 27145, + "lmtime": 1741161656150, + "modified": false + }, + "ShopThemeCheckout.fr-FR.xlf": { + "type": "-", + "size": 28311, + "lmtime": 1741161656265, + "modified": false + }, + "ShopThemeCustomeraccount.fr-FR.xlf": { + "type": "-", + "size": 31482, + "lmtime": 1741161656370, + "modified": false + }, + "ShopThemeGlobal.fr-FR.xlf": { + "type": "-", + "size": 38800, + "lmtime": 1741161656482, + "modified": false + } + }, + "index.php": { + "type": "-", + "size": 1279, + "lmtime": 1741161652826, + "modified": false + }, + "it-IT": { + "ShopFormsHelp.it-IT.xlf": { + "type": "-", + "size": 3543, + "lmtime": 1741161656583, + "modified": false + }, + "ShopFormsLabels.it-IT.xlf": { + "type": "-", + "size": 6999, + "lmtime": 1741161656689, + "modified": false + }, + "ShopThemeActions.it-IT.xlf": { + "type": "-", + "size": 22905, + "lmtime": 1741161656805, + "modified": false + }, + "ShopThemeCatalog.it-IT.xlf": { + "type": "-", + "size": 27132, + "lmtime": 1741161656922, + "modified": false + }, + "ShopThemeCheckout.it-IT.xlf": { + "type": "-", + "size": 28232, + "lmtime": 1741161657012, + "modified": false + }, + "ShopThemeCustomeraccount.it-IT.xlf": { + "type": "-", + "size": 31409, + "lmtime": 1741161657124, + "modified": false + }, + "ShopThemeGlobal.it-IT.xlf": { + "type": "-", + "size": 38835, + "lmtime": 1741161657248, + "modified": false + } + }, + "pl-PL": {}, + "sk-SK": {} + } + }, + "_libraries": {}, + "package-lock.json": { + "type": "-", + "size": 231792, + "lmtime": 0, + "modified": false + } + }, "tools": {}, "translations": {}, "upload": {}, diff --git a/config/defines.inc.php b/config/defines.inc.php new file mode 100644 index 00000000..14c9509f --- /dev/null +++ b/config/defines.inc.php @@ -0,0 +1,203 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/* Debug only */ +if (!defined('_PS_MODE_DEV_')) { + if ( $_SERVER['REMOTE_ADDR'] == '91.189.216.43' ) + define('_PS_MODE_DEV_', false); + else + define('_PS_MODE_DEV_', false); +} +/* Compatibility warning */ +define('_PS_DISPLAY_COMPATIBILITY_WARNING_', false); +if (_PS_MODE_DEV_ === true) { + @ini_set('display_errors', 'on'); + @error_reporting(E_ALL | E_STRICT); + define('_PS_DEBUG_SQL_', true); +} else { + @ini_set('display_errors', 'off'); + define('_PS_DEBUG_SQL_', false); +} + +define('_PS_DEBUG_PROFILING_', false); +define('_PS_MODE_DEMO_', false); + +$currentDir = dirname(__FILE__); + +if (!defined('_PS_HOST_MODE_') && (getenv('_PS_HOST_MODE_') || getenv('REDIRECT__PS_HOST_MODE_'))) { + define('_PS_HOST_MODE_', getenv('_PS_HOST_MODE_') ? getenv('_PS_HOST_MODE_') : getenv('REDIRECT__PS_HOST_MODE_')); +} + +if (!defined('_PS_ROOT_DIR_') && (getenv('_PS_ROOT_DIR_') || getenv('REDIRECT__PS_ROOT_DIR_'))) { + define('_PS_ROOT_DIR_', getenv('_PS_ROOT_DIR_') ? getenv('_PS_ROOT_DIR_') : getenv('REDIRECT__PS_ROOT_DIR_')); +} + +/* Directories */ +if (!defined('_PS_ROOT_DIR_')) { + define('_PS_ROOT_DIR_', realpath($currentDir.'/..')); +} + +if (!defined('_PS_CORE_DIR_')) { + define('_PS_CORE_DIR_', realpath($currentDir.'/..')); +} + +define('_PS_ALL_THEMES_DIR_', _PS_ROOT_DIR_.'/themes/'); +/* BO THEMES */ +if (defined('_PS_ADMIN_DIR_')) { + define('_PS_BO_ALL_THEMES_DIR_', _PS_ADMIN_DIR_.'/themes/'); +} +if (!defined('_PS_CACHE_DIR_')) { + $prestashopCacheDir = _PS_ROOT_DIR_.'/var/cache/'.(_PS_MODE_DEV_ ? 'dev': 'prod'). DIRECTORY_SEPARATOR; + define('_PS_CACHE_DIR_',$prestashopCacheDir); +} +define('_PS_CONFIG_DIR_', _PS_CORE_DIR_.'/config/'); +define('_PS_CUSTOM_CONFIG_FILE_', _PS_CONFIG_DIR_.'settings_custom.inc.php'); +define('_PS_CLASS_DIR_', _PS_CORE_DIR_.'/classes/'); +if (!defined('_PS_DOWNLOAD_DIR_')) { + define('_PS_DOWNLOAD_DIR_', _PS_ROOT_DIR_.'/download/'); +} +define('_PS_MAIL_DIR_', _PS_CORE_DIR_.'/mails/'); +if (!defined('_PS_MODULE_DIR_')) { + define('_PS_MODULE_DIR_', _PS_ROOT_DIR_.'/modules/'); +} +if (!defined('_PS_OVERRIDE_DIR_')) { + define('_PS_OVERRIDE_DIR_', _PS_ROOT_DIR_.'/override/'); +} +define('_PS_PDF_DIR_', _PS_CORE_DIR_.'/pdf/'); +define('_PS_TRANSLATIONS_DIR_', _PS_ROOT_DIR_.'/translations/'); +if (!defined('_PS_UPLOAD_DIR_')) { + define('_PS_UPLOAD_DIR_', _PS_ROOT_DIR_.'/upload/'); +} +define('_PS_CONTROLLER_DIR_', _PS_CORE_DIR_.'/controllers/'); +define('_PS_ADMIN_CONTROLLER_DIR_', _PS_CORE_DIR_.'/controllers/admin/'); +define('_PS_FRONT_CONTROLLER_DIR_', _PS_CORE_DIR_.'/controllers/front/'); + +define('_PS_TOOL_DIR_', _PS_CORE_DIR_.'/tools/'); +if (!defined('_PS_GEOIP_DIR_')) { + define('_PS_GEOIP_DIR_', _PS_CORE_DIR_.'/app/Resources/geoip/'); +} +if (!defined('_PS_GEOIP_CITY_FILE_')) { + define('_PS_GEOIP_CITY_FILE_', 'GeoLite2-City.mmdb'); +} + +define('_PS_VENDOR_DIR_', _PS_CORE_DIR_.'/vendor/'); +define('_PS_PEAR_XML_PARSER_PATH_', _PS_TOOL_DIR_.'pear_xml_parser/'); +define('_PS_SWIFT_DIR_', _PS_TOOL_DIR_.'swift/'); +define('_PS_TAASC_PATH_', _PS_TOOL_DIR_.'taasc/'); +define('_PS_TCPDF_PATH_', _PS_TOOL_DIR_.'tcpdf/'); + +if (!defined('_PS_IMG_DIR_')) { + define('_PS_IMG_DIR_', _PS_ROOT_DIR_.'/img/'); +} +if (!defined('_PS_HOST_MODE_')) { + define('_PS_CORE_IMG_DIR_', _PS_CORE_DIR_.'/img/'); +} else { + define('_PS_CORE_IMG_DIR_', _PS_ROOT_DIR_.'/img/'); +} + +define('_PS_CAT_IMG_DIR_', _PS_IMG_DIR_.'c/'); +define('_PS_COL_IMG_DIR_', _PS_IMG_DIR_.'co/'); +define('_PS_EMPLOYEE_IMG_DIR_', _PS_IMG_DIR_.'e/'); +define('_PS_GENDERS_DIR_', _PS_IMG_DIR_.'genders/'); +define('_PS_LANG_IMG_DIR_', _PS_IMG_DIR_.'l/'); +define('_PS_MANU_IMG_DIR_', _PS_IMG_DIR_.'m/'); +define('_PS_ORDER_STATE_IMG_DIR_', _PS_IMG_DIR_.'os/'); +define('_PS_PROD_IMG_DIR_', _PS_IMG_DIR_.'p/'); +define('_PS_SHIP_IMG_DIR_', _PS_IMG_DIR_.'s/'); +define('_PS_STORE_IMG_DIR_', _PS_IMG_DIR_.'st/'); +define('_PS_SUPP_IMG_DIR_', _PS_IMG_DIR_.'su/'); +define('_PS_TMP_IMG_DIR_', _PS_IMG_DIR_.'tmp/'); + +/* settings php */ +define('_PS_TRANS_PATTERN_', '(.*[^\\\\])'); +define('_PS_MIN_TIME_GENERATE_PASSWD_', '360'); + +if (!defined('_PS_MAGIC_QUOTES_GPC_')) { + define('_PS_MAGIC_QUOTES_GPC_', false); +} + +define('_CAN_LOAD_FILES_', 1); + +/* Order statuses +Order statuses have been moved into config.inc.php file for backward compatibility reasons */ + +/* Tax behavior */ +define('PS_PRODUCT_TAX', 0); +define('PS_STATE_TAX', 1); +define('PS_BOTH_TAX', 2); + +define('PS_TAX_EXC', 1); +define('PS_TAX_INC', 0); + +define('PS_ROUND_UP', 0); +define('PS_ROUND_DOWN', 1); +define('PS_ROUND_HALF_UP', 2); +define('PS_ROUND_HALF_DOWN', 3); +define('PS_ROUND_HALF_EVEN', 4); +define('PS_ROUND_HALF_ODD', 5); + +/* Backward compatibility */ +define('PS_ROUND_HALF', PS_ROUND_HALF_UP); + +/* Carrier::getCarriers() filter */ +// these defines are DEPRECATED since 1.4.5 version +define('PS_CARRIERS_ONLY', 1); +define('CARRIERS_MODULE', 2); +define('CARRIERS_MODULE_NEED_RANGE', 3); +define('PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE', 4); +define('ALL_CARRIERS', 5); + +/* SQL Replication management */ +define('_PS_USE_SQL_SLAVE_', 0); + +/* PS Technical configuration */ +define('_PS_ADMIN_PROFILE_', 1); + +/* Stock Movement */ +define('_STOCK_MOVEMENT_ORDER_REASON_', 3); +define('_STOCK_MOVEMENT_MISSING_REASON_', 4); + +define('_PS_CACHEFS_DIRECTORY_', _PS_ROOT_DIR_.'/cache/cachefs/'); + +/* Geolocation */ +define('_PS_GEOLOCATION_NO_CATALOG_', 0); +define('_PS_GEOLOCATION_NO_ORDER_', 1); + +define('MIN_PASSWD_LENGTH', 8); + +define('_PS_SMARTY_NO_COMPILE_', 0); +define('_PS_SMARTY_CHECK_COMPILE_', 1); +define('_PS_SMARTY_FORCE_COMPILE_', 2); + +define('_PS_SMARTY_CONSOLE_CLOSE_', 0); +define('_PS_SMARTY_CONSOLE_OPEN_BY_URL_', 1); +define('_PS_SMARTY_CONSOLE_OPEN_', 2); + +if (!defined('_PS_JQUERY_VERSION_')) { + define('_PS_JQUERY_VERSION_', '1.11.0'); +} + +define('_PS_CACHE_CA_CERT_FILE_', _PS_CACHE_DIR_.'cacert.pem'); diff --git a/controllers/admin/AdminAccessController.php b/controllers/admin/AdminAccessController.php new file mode 100644 index 00000000..be6977bf --- /dev/null +++ b/controllers/admin/AdminAccessController.php @@ -0,0 +1,226 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Profile $object + */ +class AdminAccessControllerCore extends AdminController +{ + /* @var array : Black list of id_tab that do not have access */ + public $accesses_black_list = array(); + + public function __construct() + { + $this->bootstrap = true; + $this->show_toolbar = false; + $this->table = 'access'; + $this->className = 'Profile'; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->lang = false; + $this->context = Context::getContext(); + + // Blacklist AdminLogin + $this->accesses_black_list[] = Tab::getIdFromClassName('AdminLogin'); + + parent::__construct(); + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $current_profile = (int) $this->getCurrentProfileId(); + $profiles = Profile::getProfiles($this->context->language->id); + $tabs = Tab::getTabs($this->context->language->id); + + $accesses = array(); + foreach ($profiles as $profile) { + $accesses[$profile['id_profile']] = Profile::getProfileAccesses($profile['id_profile']); + } + + // Deleted id_tab that do not have access + foreach ($tabs as $key => $tab) { + // Don't allow permissions for unnamed tabs (ie. AdminLogin) + if (empty($tab['name'])) { + unset($tabs[$key]); + } + + foreach ($this->accesses_black_list as $id_tab) { + if ($tab['id_tab'] == (int) $id_tab) { + unset($tabs[$key]); + } + } + } + + $modules = array(); + foreach ($profiles as $profile) { + $modules[$profile['id_profile']] = Module::getModulesAccessesByIdProfile($profile['id_profile']); + uasort($modules[$profile['id_profile']], array($this, 'sortModuleByName')); + } + + $this->fields_form = array(''); + $this->tpl_form_vars = array( + 'profiles' => $profiles, + 'accesses' => $accesses, + 'id_tab_parentmodule' => (int) Tab::getIdFromClassName('AdminParentModules'), + 'id_tab_module' => (int) Tab::getIdFromClassName('AdminModules'), + 'tabs' => $this->displayTabs($tabs), + 'current_profile' => (int) $current_profile, + 'admin_profile' => (int) _PS_ADMIN_PROFILE_, + 'access_edit' => $this->access('edit'), + 'perms' => array('view', 'add', 'edit', 'delete'), + 'id_perms' => array('view' => 0, 'add' => 1, 'edit' => 2, 'delete' => 3, 'all' => 4), + 'modules' => $modules, + 'link' => $this->context->link, + 'employee_profile_id' => (int) $this->context->employee->id_profile, + ); + + return parent::renderForm(); + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + $this->display = 'edit'; + + if (!$this->loadObject(true)) { + return; + } + + $this->content .= $this->renderForm(); + + $this->context->smarty->assign(array( + 'content' => $this->content, + )); + } + + public function initToolbarTitle() + { + $this->toolbar_title = array_unique($this->breadcrumbs); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + unset($this->page_header_toolbar_btn['cancel']); + } + + public function ajaxProcessUpdateAccess() + { + if (_PS_MODE_DEMO_) { + throw new PrestaShopException($this->trans('This functionality has been disabled.', array(), 'Admin.Notifications.Error')); + } + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error')); + } + + if (Tools::isSubmit('submitAddAccess')) { + $access = new Access(); + $perm = Tools::getValue('perm'); + if (!in_array($perm, array('view', 'add', 'edit', 'delete', 'all'))) { + throw new PrestaShopException('permission does not exist'); + } + + $enabled = (int) Tools::getValue('enabled'); + $id_tab = (int) Tools::getValue('id_tab'); + $id_profile = (int) Tools::getValue('id_profile'); + $addFromParent = (int) Tools::getValue('addFromParent'); + + die($access->updateLgcAccess((int) $id_profile, $id_tab, $perm, $enabled, $addFromParent)); + } + } + + public function ajaxProcessUpdateModuleAccess() + { + if (_PS_MODE_DEMO_) { + throw new PrestaShopException($this->trans('This functionality has been disabled.', array(), 'Admin.Notifications.Error')); + } + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error')); + } + + if (Tools::isSubmit('changeModuleAccess')) { + $access = new Access(); + $perm = Tools::getValue('perm'); + $enabled = (int) Tools::getValue('enabled'); + $id_module = (int) Tools::getValue('id_module'); + $id_profile = (int) Tools::getValue('id_profile'); + + if (!in_array($perm, array('view', 'configure', 'uninstall'))) { + throw new PrestaShopException('permission does not exist'); + } + + die($access->updateLgcModuleAccess((int) $id_profile, $id_module, $perm, $enabled)); + } + } + + /** + * Get the current profile id. + * + * @return int the $_GET['profile'] if valid, else 1 (the first profile id) + */ + public function getCurrentProfileId() + { + return (isset($_GET['id_profile']) && !empty($_GET['id_profile']) && is_numeric($_GET['id_profile'])) ? (int) $_GET['id_profile'] : 1; + } + + private function sortModuleByName($a, $b) + { + return strnatcmp($a['name'], $b['name']); + } + + /** + * return human readable Tabs hierarchy for display. + */ + private function displayTabs(array $tabs) + { + $tabsTree = $this->getChildrenTab($tabs); + + return $tabsTree; + } + + private function getChildrenTab(array &$tabs, $id_parent = 0) + { + $children = []; + foreach ($tabs as &$tab) { + $id = $tab['id_tab']; + + if ($tab['id_parent'] == $id_parent) { + $children[$id] = $tab; + $children[$id]['children'] = $this->getChildrenTab($tabs, $id); + } + } + + return $children; + } +} diff --git a/controllers/admin/AdminAddressesController.php b/controllers/admin/AdminAddressesController.php new file mode 100644 index 00000000..7bc97bc9 --- /dev/null +++ b/controllers/admin/AdminAddressesController.php @@ -0,0 +1,551 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Address $object + */ +class AdminAddressesControllerCore extends AdminController +{ + /** @var array countries list */ + protected $countries_array = array(); + + public function __construct() + { + $this->bootstrap = true; + $this->required_database = true; + $this->required_fields = array('company', 'address2', 'postcode', 'other', 'phone', 'phone_mobile', 'vat_number', 'dni'); + $this->table = 'address'; + $this->className = 'CustomerAddress'; + $this->lang = false; + $this->addressType = 'customer'; + $this->explicitSelect = true; + + parent::__construct(); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + ), + ); + + $this->allow_export = true; + + if (!Tools::getValue('realedit')) { + $this->deleted = true; + } + + $countries = Country::getCountries($this->context->language->id); + foreach ($countries as $country) { + $this->countries_array[$country['id_country']] = $country['name']; + } + + $this->fields_list = array( + 'id_address' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'firstname' => array( + 'title' => $this->trans('First Name', array(), 'Admin.Global'), + 'filter_key' => 'a!firstname', + 'maxlength' => 30, + ), + 'lastname' => array( + 'title' => $this->trans('Last Name', array(), 'Admin.Global'), + 'filter_key' => 'a!lastname', + 'maxlength' => 30, + ), + 'address1' => array( + 'title' => $this->trans('Address', array(), 'Admin.Global'), + ), + 'postcode' => array( + 'title' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'align' => 'right', + ), + 'city' => array( + 'title' => $this->trans('City', array(), 'Admin.Global'), + ), + 'country' => array( + 'title' => $this->trans('Country', array(), 'Admin.Global'), + 'type' => 'select', + 'list' => $this->countries_array, + 'filter_key' => 'cl!id_country', + ), + ); + + $this->_select = 'cl.`name` as country'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (cl.`id_country` = a.`id_country` AND cl.`id_lang` = ' . (int) $this->context->language->id . ') + LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON a.id_customer = c.id_customer + '; + $this->_where = 'AND a.id_customer != 0 ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER, 'c'); + $this->_use_found_rows = false; + } + + public function initToolbar() + { + parent::initToolbar(); + + if (!$this->display && $this->can_import) { + $this->toolbar_btn['import'] = array( + 'href' => $this->context->link->getAdminLink('AdminImport', true, array(), array('import_type' => 'addresses')), + 'desc' => $this->trans('Import', array(), 'Admin.Actions'), + ); + } + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_address'] = array( + 'href' => $this->context->link->getAdminLink('AdminAddresses', true, array(), array('addaddress' => 1)), + 'desc' => $this->trans('Add new address', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Addresses', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'icon-envelope-alt', + ), + 'input' => array( + array( + 'type' => 'text_customer', + 'label' => $this->trans('Customer', array(), 'Admin.Global'), + 'name' => 'id_customer', + 'required' => false, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Identification number', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'dni', + 'required' => false, + 'col' => '4', + 'hint' => $this->trans('The national ID card number of this person, or a unique tax identification number.', array(), 'Admin.Orderscustomers.Feature'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Address alias', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'alias', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Other', array(), 'Admin.Global'), + 'name' => 'other', + 'required' => false, + 'cols' => 15, + 'rows' => 3, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'hidden', + 'name' => 'id_order', + ), + array( + 'type' => 'hidden', + 'name' => 'address_type', + ), + array( + 'type' => 'hidden', + 'name' => 'back', + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + $this->fields_value['address_type'] = (int) Tools::getValue('address_type', 1); + + $id_customer = (int) Tools::getValue('id_customer'); + if (!$id_customer && Validate::isLoadedObject($this->object)) { + $id_customer = $this->object->id_customer; + } + if ($id_customer) { + $customer = new Customer((int) $id_customer); + $token_customer = Tools::getAdminToken('AdminCustomers' . (int) (Tab::getIdFromClassName('AdminCustomers')) . (int) $this->context->employee->id); + } + + $this->tpl_form_vars = array( + 'customer' => isset($customer) ? $customer : null, + 'tokenCustomer' => isset($token_customer) ? $token_customer : null, + 'back_url' => urldecode(Tools::getValue('back')), + ); + + // Order address fields depending on country format + $addresses_fields = $this->processAddressFormat(); + // we use delivery address + $addresses_fields = $addresses_fields['dlv_all_fields']; + + // get required field + $required_fields = AddressFormat::getFieldsRequired(); + + // Merge with field required + $addresses_fields = array_unique(array_merge($addresses_fields, $required_fields)); + + $temp_fields = array(); + + foreach ($addresses_fields as $addr_field_item) { + if ($addr_field_item == 'company') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Company', array(), 'Admin.Global'), + 'name' => 'company', + 'required' => in_array('company', $required_fields), + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ); + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('VAT number', array(), 'Admin.Orderscustomers.Feature'), + 'col' => '2', + 'name' => 'vat_number', + 'required' => in_array('vat_number', $required_fields), + ); + } elseif ($addr_field_item == 'lastname') { + if (isset($customer) && + !Tools::isSubmit('submit' . strtoupper($this->table)) && + Validate::isLoadedObject($customer) && + !Validate::isLoadedObject($this->object)) { + $default_value = $customer->lastname; + } else { + $default_value = ''; + } + + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Last Name', array(), 'Admin.Global'), + 'name' => 'lastname', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' 0-9!&lt;&gt;,;?=+()@#"�{}_$%:', + 'default_value' => $default_value, + ); + } elseif ($addr_field_item == 'firstname') { + if (isset($customer) && + !Tools::isSubmit('submit' . strtoupper($this->table)) && + Validate::isLoadedObject($customer) && + !Validate::isLoadedObject($this->object)) { + $default_value = $customer->firstname; + } else { + $default_value = ''; + } + + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('First Name', array(), 'Admin.Global'), + 'name' => 'firstname', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' 0-9!&lt;&gt;,;?=+()@#"�{}_$%:', + 'default_value' => $default_value, + ); + } elseif ($addr_field_item == 'address1') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Address', array(), 'Admin.Global'), + 'name' => 'address1', + 'col' => '6', + 'required' => true, + ); + } elseif ($addr_field_item == 'address2') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Address', array(), 'Admin.Global') . ' (2)', + 'name' => 'address2', + 'col' => '6', + 'required' => in_array('address2', $required_fields), + ); + } elseif ($addr_field_item == 'postcode') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'name' => 'postcode', + 'col' => '2', + 'required' => true, + ); + } elseif ($addr_field_item == 'city') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('City', array(), 'Admin.Global'), + 'name' => 'city', + 'col' => '4', + 'required' => true, + ); + } elseif ($addr_field_item == 'country' || $addr_field_item == 'Country:name') { + $temp_fields[] = array( + 'type' => 'select', + 'label' => $this->trans('Country', array(), 'Admin.Global'), + 'name' => 'id_country', + 'required' => in_array('Country:name', $required_fields) || in_array('country', $required_fields), + 'col' => '4', + 'default_value' => (int) $this->context->country->id, + 'options' => array( + 'query' => Country::getCountries($this->context->language->id), + 'id' => 'id_country', + 'name' => 'name', + ), + ); + $temp_fields[] = array( + 'type' => 'select', + 'label' => $this->trans('State', array(), 'Admin.Global'), + 'name' => 'id_state', + 'required' => false, + 'col' => '4', + 'options' => array( + 'query' => array(), + 'id' => 'id_state', + 'name' => 'name', + ), + ); + } elseif ($addr_field_item == 'phone') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Home phone', array(), 'Admin.Global'), + 'name' => 'phone', + 'required' => in_array('phone', $required_fields), + 'col' => '4', + ); + } elseif ($addr_field_item == 'phone_mobile') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Mobile phone', array(), 'Admin.Global'), + 'name' => 'phone_mobile', + 'required' => in_array('phone_mobile', $required_fields), + 'col' => '4', + ); + } + } + + // merge address format with the rest of the form + array_splice($this->fields_form['input'], 3, 0, $temp_fields); + + return parent::renderForm(); + } + + public function processSave() + { + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + } + + // Transform e-mail in id_customer for parent processing + if (Validate::isEmail(Tools::getValue('email'))) { + $customer = new Customer(); + $customer->getByEmail(Tools::getValue('email'), null, false); + if (Validate::isLoadedObject($customer)) { + $_POST['id_customer'] = $customer->id; + } else { + $this->errors[] = $this->trans('This email address is not registered.', array(), 'Admin.Orderscustomers.Notification'); + } + } elseif ($id_customer = Tools::getValue('id_customer')) { + $customer = new Customer((int) $id_customer); + if (Validate::isLoadedObject($customer)) { + $_POST['id_customer'] = $customer->id; + } else { + $this->errors[] = $this->trans('This customer ID is not recognized.', array(), 'Admin.Orderscustomers.Notification'); + } + } else { + $this->errors[] = $this->trans('This email address is not valid. Please use an address like bob@example.com.', array(), 'Admin.Orderscustomers.Notification'); + } + if (Country::isNeedDniByCountryId(Tools::getValue('id_country')) && !Tools::getValue('dni')) { + $this->errors[] = $this->trans('The identification number is incorrect or has already been used.', array(), 'Admin.Orderscustomers.Notification'); + } + + /* If the selected country does not contain states */ + $id_state = (int) Tools::getValue('id_state'); + $id_country = (int) Tools::getValue('id_country'); + $country = new Country((int) $id_country); + if ($country && !(int) $country->contains_states && $id_state) { + $this->errors[] = $this->trans('You have selected a state for a country that does not contain states.', array(), 'Admin.Orderscustomers.Notification'); + } + + /* If the selected country contains states, then a state have to be selected */ + if ((int) $country->contains_states && !$id_state) { + $this->errors[] = $this->trans('An address located in a country containing states must have a state selected.', array(), 'Admin.Orderscustomers.Notification'); + } + + $postcode = Tools::getValue('postcode'); + /* Check zip code format */ + if ($country->zip_code_format && !$country->checkZipCode($postcode)) { + $this->errors[] = $this->trans('Your Zip/postal code is incorrect.', array(), 'Admin.Notifications.Error') . '
' . $this->trans('It must be entered as follows:', array(), 'Admin.Notifications.Error') . ' ' . str_replace('C', $country->iso_code, str_replace('N', '0', str_replace('L', 'A', $country->zip_code_format))); + } elseif (empty($postcode) && $country->need_zip_code) { + $this->errors[] = $this->trans('A Zip/postal code is required.', array(), 'Admin.Notifications.Error'); + } elseif ($postcode && !Validate::isPostCode($postcode)) { + $this->errors[] = $this->trans('The Zip/postal code is invalid.', array(), 'Admin.Notifications.Error'); + } + + /* If this address come from order's edition and is the same as the other one (invoice or delivery one) + ** we delete its id_address to force the creation of a new one */ + if ((int) Tools::getValue('id_order')) { + $this->_redirect = false; + if (isset($_POST['address_type'])) { + $_POST['id_address'] = ''; + $this->id_object = null; + } + } + + // Check the requires fields which are settings in the BO + $address = new Address(); + $this->errors = array_merge($this->errors, $address->validateFieldsRequiredDatabase()); + + $return = false; + if (empty($this->errors)) { + $return = parent::processSave(); + } else { + // if we have errors, we stay on the form instead of going back to the list + $this->display = 'edit'; + } + + /* Reassignation of the order's new (invoice or delivery) address */ + $address_type = (int) Tools::getValue('address_type') == 2 ? 'invoice' : 'delivery'; + + if ($this->action == 'save' && ($id_order = (int) Tools::getValue('id_order')) && !count($this->errors) && !empty($address_type)) { + if (!Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'orders SET `id_address_' . bqSQL($address_type) . '` = ' . (int) $this->object->id . ' WHERE `id_order` = ' . (int) $id_order)) { + $this->errors[] = $this->trans('An error occurred while linking this address to its order.', array(), 'Admin.Orderscustomers.Notification'); + } else { + //update order shipping cost + $order = new Order($id_order); + $order->refreshShippingCost(); + Tools::redirectAdmin(urldecode(Tools::getValue('back')) . '&conf=4'); + } + } + + return $return; + } + + public function processAdd() + { + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + } + + return parent::processAdd(); + } + + /** + * Get Address formats used by the country where the address id retrieved from POST/GET is. + * + * @return array address formats + */ + protected function processAddressFormat() + { + $tmp_addr = new CustomerAddress((int) Tools::getValue('id_address')); + + $selected_country = ($tmp_addr && $tmp_addr->id_country) ? $tmp_addr->id_country : (int) Configuration::get('PS_COUNTRY_DEFAULT'); + $adr_fields = AddressFormat::getOrderedAddressFields($selected_country, false, true); + + $all_fields = array(); + $out = array(); + + foreach ($adr_fields as $fields_line) { + foreach (explode(' ', $fields_line) as $field_item) { + $all_fields[] = trim($field_item); + } + } + + foreach (array('inv', 'dlv') as $adr_type) { + $out[$adr_type . '_adr_fields'] = $adr_fields; + $out[$adr_type . '_all_fields'] = $all_fields; + } + + return $out; + } + + /** + * Method called when an ajax request is made. + * + * @see AdminController::postProcess() + */ + public function ajaxProcess() + { + if (Tools::isSubmit('email')) { + $email = pSQL(Tools::getValue('email')); + $customer = Customer::searchByName($email); + if (!empty($customer)) { + $customer = $customer['0']; + echo json_encode(array('infos' => pSQL($customer['firstname']) . '_' . pSQL($customer['lastname']) . '_' . pSQL($customer['company']))); + } + } + die; + } + + /** + * Object Delete. + */ + public function processDelete() + { + if (Validate::isLoadedObject($object = $this->loadObject())) { + /** @var Address $object */ + if (!$object->isUsed()) { + $this->deleted = false; + } + } + + $res = parent::processDelete(); + + if ($back = Tools::getValue('back')) { + $this->redirect_after = urldecode($back) . '&conf=1'; + } + + return $res; + } + + /** + * Delete multiple items. + * + * @return bool true if succcess + */ + protected function processBulkDelete() + { + if (is_array($this->boxes) && !empty($this->boxes)) { + $deleted = false; + foreach ($this->boxes as $id) { + $to_delete = new Address((int) $id); + if ($to_delete->isUsed()) { + $deleted = true; + break; + } + } + $this->deleted = $deleted; + } + + return parent::processBulkDelete(); + } +} diff --git a/controllers/admin/AdminAttachmentsController.php b/controllers/admin/AdminAttachmentsController.php new file mode 100644 index 00000000..5971e636 --- /dev/null +++ b/controllers/admin/AdminAttachmentsController.php @@ -0,0 +1,262 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Attachment $object + */ +class AdminAttachmentsControllerCore extends AdminController +{ + public $bootstrap = true; + + protected $product_attachements = array(); + + public function __construct() + { + $this->table = 'attachment'; + $this->className = 'Attachment'; + $this->lang = true; + + $this->addRowAction('edit'); + $this->addRowAction('view'); + $this->addRowAction('delete'); + + $this->_select = 'IFNULL(virtual_product_attachment.products, 0) as products'; + $this->_join = 'LEFT JOIN (SELECT id_attachment, COUNT(*) as products FROM ' . _DB_PREFIX_ . 'product_attachment GROUP BY id_attachment) virtual_product_attachment ON a.id_attachment = virtual_product_attachment.id_attachment'; + $this->_use_found_rows = false; + + parent::__construct(); + + $this->fields_list = array( + 'id_attachment' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + ), + 'file' => array( + 'title' => $this->trans('File', array(), 'Admin.Global'), + 'orderby' => false, + 'search' => false, + ), + 'file_size' => array( + 'title' => $this->trans('Size', array(), 'Admin.Global'), + 'callback' => 'displayHumanReadableSize', + ), + 'products' => array( + 'title' => $this->trans('Associated with', array(), 'Admin.Catalog.Feature'), + 'suffix' => $this->trans('product(s)', array(), 'Admin.Catalog.Feature'), + 'filter_key' => 'virtual_product_attachment!products', + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + ), + ); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + + $this->addJs(_PS_JS_DIR_ . '/admin/attachments.js'); + Media::addJsDefL('confirm_text', $this->trans('This file is associated with the following products, do you really want to delete it?', array(), 'Admin.Catalog.Notification')); + } + + public static function displayHumanReadableSize($size) + { + return Tools::formatBytes($size); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_attachment'] = array( + 'href' => self::$currentIndex . '&addattachment&token=' . $this->token, + 'desc' => $this->trans('Add new file', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderView() + { + if (($obj = $this->loadObject(true)) && Validate::isLoadedObject($obj)) { + $link = $this->context->link->getPageLink('attachment', true, null, 'id_attachment=' . $obj->id); + Tools::redirectLink($link); + } + + return $this->displayWarning($this->trans('File not found', array(), 'Admin.Catalog.Notification')); + } + + public function renderForm() + { + if (($obj = $this->loadObject(true)) && Validate::isLoadedObject($obj)) { + /** @var Attachment $obj */ + $link = $this->context->link->getPageLink('attachment', true, null, 'id_attachment=' . $obj->id); + + if (file_exists(_PS_DOWNLOAD_DIR_ . $obj->file)) { + $size = round(filesize(_PS_DOWNLOAD_DIR_ . $obj->file) / 1024); + } + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Add new file', array(), 'Admin.Catalog.Feature'), + 'icon' => 'icon-paper-clip', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Filename', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'lang' => true, + 'col' => 4, + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + 'lang' => true, + 'col' => 6, + ), + array( + 'type' => 'file', + 'file' => isset($link) ? $link : null, + 'size' => isset($size) ? $size : null, + 'label' => $this->trans('File', array(), 'Admin.Global'), + 'name' => 'file', + 'required' => true, + 'col' => 6, + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + return parent::renderForm(); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList((int) $id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + if (count($this->_list)) { + $this->product_attachements = Attachment::getProductAttached((int) $id_lang, $this->_list); + + $list_product_list = array(); + foreach ($this->_list as $list) { + $product_list = ''; + + if (isset($this->product_attachements[$list['id_attachment']])) { + foreach ($this->product_attachements[$list['id_attachment']] as $product) { + $product_list .= $product . ', '; + } + + $product_list = rtrim($product_list, ', '); + } + + $list_product_list[$list['id_attachment']] = $product_list; + } + + // Assign array in list_action_delete.tpl + $this->tpl_delete_link_vars = array( + 'product_list' => $list_product_list, + 'product_attachements' => $this->product_attachements, + ); + } + } + + public function postProcess() + { + if (_PS_MODE_DEMO_) { + $this->errors[] = $this->trans('This functionality has been disabled.', array(), 'Admin.Notifications.Error'); + + return; + } + + if (Tools::isSubmit('submitAdd' . $this->table)) { + $id = (int) Tools::getValue('id_attachment'); + if ($id && $a = new Attachment($id)) { + $_POST['file'] = $a->file; + $_POST['mime'] = $a->mime; + } + if (!count($this->errors)) { + if (isset($_FILES['file']) && is_uploaded_file($_FILES['file']['tmp_name'])) { + if ($_FILES['file']['size'] > (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024 * 1024)) { + $this->errors[] = $this->trans( + 'The file is too large. Maximum size allowed is: %1$d kB. The file you are trying to upload is %2$d kB.', + array( + '%1$d' => (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024), + '%2$d' => number_format(($_FILES['file']['size'] / 1024), 2, '.', ''), + ), + 'Admin.Notifications.Error' + ); + } else { + do { + $uniqid = sha1(microtime()); + } while (file_exists(_PS_DOWNLOAD_DIR_ . $uniqid)); + if (!move_uploaded_file($_FILES['file']['tmp_name'], _PS_DOWNLOAD_DIR_ . $uniqid)) { + $this->errors[] = $this->trans('Failed to copy the file.', array(), 'Admin.Catalog.Notification'); + } + $_POST['file_name'] = $_FILES['file']['name']; + @unlink($_FILES['file']['tmp_name']); + if (!sizeof($this->errors) && isset($a) && file_exists(_PS_DOWNLOAD_DIR_ . $a->file)) { + unlink(_PS_DOWNLOAD_DIR_ . $a->file); + } + $_POST['file'] = $uniqid; + $_POST['mime'] = $_FILES['file']['type']; + } + } elseif (array_key_exists('file', $_FILES) && (int) $_FILES['file']['error'] === 1) { + $max_upload = (int) ini_get('upload_max_filesize'); + $max_post = (int) ini_get('post_max_size'); + $upload_mb = min($max_upload, $max_post); + $this->errors[] = $this->trans('The file %file% exceeds the size allowed by the server. The limit is set to %size% MB.', + array('%file%' => '' . $_FILES['file']['name'] . ' ', '%size%' => '' . $upload_mb . ''), + 'Admin.Catalog.Notification'); + } elseif (!isset($a) || (isset($a) && !file_exists(_PS_DOWNLOAD_DIR_ . $a->file))) { + $this->errors[] = $this->trans('Upload error. Please check your server configurations for the maximum upload size allowed.', array(), 'Admin.Catalog.Notification'); + } + } + $this->validateRules(); + } + $return = parent::postProcess(); + if (!$return && isset($uniqid) && file_exists(_PS_DOWNLOAD_DIR_ . $uniqid)) { + unlink(_PS_DOWNLOAD_DIR_ . $uniqid); + } + + return $return; + } +} diff --git a/controllers/admin/AdminAttributeGeneratorController.php b/controllers/admin/AdminAttributeGeneratorController.php new file mode 100644 index 00000000..ecc81d8a --- /dev/null +++ b/controllers/admin/AdminAttributeGeneratorController.php @@ -0,0 +1,273 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +@ini_set('max_execution_time', 3600); + +/** + * @property Product $object + */ +class AdminAttributeGeneratorControllerCore extends AdminController +{ + protected $combinations = array(); + + /** @var Product */ + protected $product; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'product_attribute'; + $this->className = 'Product'; + $this->multishop_context_group = false; + + parent::__construct(); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJS(_PS_JS_DIR_ . 'admin/attributes.js'); + } + + protected function addAttribute($attributes, $price = 0, $weight = 0) + { + foreach ($attributes as $attribute) { + $price += (float) preg_replace('/[^0-9.-]/', '', str_replace(',', '.', Tools::getValue('price_impact_' . (int) $attribute))); + $weight += (float) preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('weight_impact_' . (int) $attribute))); + } + if ($this->product->id) { + return array( + 'id_product' => (int) $this->product->id, + 'price' => (float) $price, + 'weight' => (float) $weight, + 'ecotax' => 0, + 'quantity' => (int) Tools::getValue('quantity'), + 'reference' => pSQL($_POST['reference']), + 'default_on' => 0, + 'available_date' => '0000-00-00', + ); + } + + return array(); + } + + public static function createCombinations($list) + { + if (count($list) <= 1) { + return count($list) ? array_map(function ($v) { return array($v); }, $list[0]) : $list; + } + $res = array(); + $first = array_pop($list); + foreach ($first as $attribute) { + $tab = AdminAttributeGeneratorController::createCombinations($list); + foreach ($tab as $to_add) { + $res[] = is_array($to_add) ? array_merge($to_add, array($attribute)) : array($to_add, $attribute); + } + } + + return $res; + } + + public function initProcess() + { + if (!defined('PS_MASS_PRODUCT_CREATION')) { + define('PS_MASS_PRODUCT_CREATION', true); + } + + if (Tools::isSubmit('generate')) { + if ($this->access('edit')) { + $this->action = 'generate'; + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', array(), 'Admin.Notifications.Error'); + } + } + parent::initProcess(); + } + + public function postProcess() + { + $this->product = new Product((int) Tools::getValue('id_product')); + $this->product->loadStockData(); + parent::postProcess(); + } + + public function processGenerate() + { + if (!is_array(Tools::getValue('options'))) { + $this->errors[] = $this->trans('Please select at least one attribute.', array(), 'Admin.Catalog.Notification'); + } else { + $tab = array_values(Tools::getValue('options')); + if (count($tab) && Validate::isLoadedObject($this->product)) { + AdminAttributeGeneratorController::setAttributesImpacts($this->product->id, $tab); + $this->combinations = array_values(AdminAttributeGeneratorController::createCombinations($tab)); + $values = array_values(array_map(array($this, 'addAttribute'), $this->combinations)); + + // @since 1.5.0 + if ($this->product->depends_on_stock == 0) { + $attributes = Product::getProductAttributesIds($this->product->id, true); + foreach ($attributes as $attribute) { + StockAvailable::removeProductFromStockAvailable($this->product->id, $attribute['id_product_attribute'], Context::getContext()->shop); + } + } + + SpecificPriceRule::disableAnyApplication(); + + $this->product->deleteProductAttributes(); + $this->product->generateMultipleCombinations($values, $this->combinations); + + // Reset cached default attribute for the product and get a new one + Product::getDefaultAttribute($this->product->id, 0, true); + Product::updateDefaultAttribute($this->product->id); + + // @since 1.5.0 + if ($this->product->depends_on_stock == 0) { + $attributes = Product::getProductAttributesIds($this->product->id, true); + $quantity = (int) Tools::getValue('quantity'); + foreach ($attributes as $attribute) { + if (Shop::getContext() == Shop::CONTEXT_ALL) { + $shops_list = Shop::getShops(); + if (is_array($shops_list)) { + foreach ($shops_list as $current_shop) { + if (isset($current_shop['id_shop']) && (int) $current_shop['id_shop'] > 0) { + StockAvailable::setQuantity($this->product->id, (int) $attribute['id_product_attribute'], $quantity, (int) $current_shop['id_shop']); + } + } + } + } else { + StockAvailable::setQuantity($this->product->id, (int) $attribute['id_product_attribute'], $quantity); + } + } + } else { + StockAvailable::synchronize($this->product->id); + } + + SpecificPriceRule::enableAnyApplication(); + SpecificPriceRule::applyAllRules(array((int) $this->product->id)); + + Tools::redirectAdmin($this->context->link->getAdminLink('AdminProducts') . '&id_product=' . (int) Tools::getValue('id_product') . '&updateproduct&key_tab=Combinations&conf=4'); + } else { + $this->errors[] = $this->trans('Unable to initialize these parameters. A combination is missing or an object cannot be loaded.', array(), 'Admin.Catalog.Notification'); + } + } + } + + protected static function setAttributesImpacts($id_product, $tab) + { + $attributes = array(); + foreach ($tab as $group) { + foreach ($group as $attribute) { + $price = preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('price_impact_' . (int) $attribute))); + $weight = preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('weight_impact_' . (int) $attribute))); + $attributes[] = '(' . (int) $id_product . ', ' . (int) $attribute . ', ' . (float) $price . ', ' . (float) $weight . ')'; + } + } + + return Db::getInstance()->execute(' + INSERT INTO `' . _DB_PREFIX_ . 'attribute_impact` (`id_product`, `id_attribute`, `price`, `weight`) + VALUES ' . implode(',', $attributes) . ' + ON DUPLICATE KEY UPDATE `price` = VALUES(price), `weight` = VALUES(weight)'); + } + + public function initGroupTable() + { + $combinations_groups = $this->product->getAttributesGroups($this->context->language->id); + $attributes = array(); + $impacts = Product::getAttributesImpacts($this->product->id); + foreach ($combinations_groups as &$combination) { + $target = &$attributes[$combination['id_attribute_group']][$combination['id_attribute']]; + $target = $combination; + if (isset($impacts[$combination['id_attribute']])) { + $target['price'] = $impacts[$combination['id_attribute']]['price']; + $target['weight'] = $impacts[$combination['id_attribute']]['weight']; + } + } + $this->context->smarty->assign(array( + 'currency_sign' => $this->context->currency->sign, + 'weight_unit' => Configuration::get('PS_WEIGHT_UNIT'), + 'attributes' => $attributes, + )); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + $this->page_header_toolbar_title = $this->trans('Attributes generator', array(), 'Admin.Catalog.Feature'); + $this->page_header_toolbar_btn['back'] = array( + 'href' => $this->context->link->getAdminLink('AdminProducts') . '&id_product=' . (int) Tools::getValue('id_product') . '&updateproduct&key_tab=Combinations', + 'desc' => $this->trans('Back to the product', array(), 'Admin.Catalog.Feature'), + ); + } + + public function initBreadcrumbs($tab_id = null, $tabs = null) + { + $this->display = 'generator'; + + return parent::initBreadcrumbs(); + } + + public function initContent() + { + if (!Combination::isFeatureActive()) { + $adminPerformanceUrl = $this->context->link->getAdminLink('AdminPerformance'); + + $url = '' . + $this->trans('Performance', array(), 'Admin.Global') . ''; + $this->displayWarning($this->trans('This feature has been disabled. You can activate it here: %link%.', array('%link%' => $url), 'Admin.Catalog.Notification')); + + return; + } + + // Init toolbar + $this->initPageHeaderToolbar(); + $this->initGroupTable(); + + $attributes = Attribute::getAttributes(Context::getContext()->language->id, true); + $attribute_js = array(); + + foreach ($attributes as $k => $attribute) { + $attribute_js[$attribute['id_attribute_group']][$attribute['id_attribute']] = $attribute['name']; + } + + $attribute_groups = AttributeGroup::getAttributesGroups($this->context->language->id); + $this->product = new Product((int) Tools::getValue('id_product')); + + $this->context->smarty->assign(array( + 'tax_rates' => $this->product->getTaxesRate(), + 'generate' => isset($_POST['generate']) && !count($this->errors), + 'combinations_size' => count($this->combinations), + 'product_name' => $this->product->name[$this->context->language->id], + 'product_reference' => $this->product->reference, + 'url_generator' => self::$currentIndex . '&id_product=' . (int) Tools::getValue('id_product') . '&attributegenerator&token=' . Tools::getValue('token'), + 'attribute_groups' => $attribute_groups, + 'attribute_js' => $attribute_js, + 'toolbar_btn' => $this->toolbar_btn, + 'toolbar_scroll' => true, + 'show_page_header_toolbar' => $this->show_page_header_toolbar, + 'page_header_toolbar_title' => $this->page_header_toolbar_title, + 'page_header_toolbar_btn' => $this->page_header_toolbar_btn, + )); + } +} diff --git a/controllers/admin/AdminAttributesGroupsController.php b/controllers/admin/AdminAttributesGroupsController.php new file mode 100644 index 00000000..6ae9f63e --- /dev/null +++ b/controllers/admin/AdminAttributesGroupsController.php @@ -0,0 +1,967 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property AttributeGroup $object + */ +class AdminAttributesGroupsControllerCore extends AdminController +{ + public $bootstrap = true; + protected $id_attribute; + protected $position_identifier = 'id_attribute_group'; + protected $attribute_name; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'attribute_group'; + $this->list_id = 'attribute_group'; + $this->identifier = 'id_attribute_group'; + $this->className = 'AttributeGroup'; + $this->lang = true; + $this->_defaultOrderBy = 'position'; + + parent::__construct(); + + $this->fields_list = array( + 'id_attribute_group' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + 'filter_key' => 'b!name', + 'align' => 'left', + ), + 'count_values' => array( + 'title' => $this->trans('Values', array(), 'Admin.Catalog.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + 'orderby' => false, + 'search' => false, + ), + 'position' => array( + 'title' => $this->trans('Position', array(), 'Admin.Global'), + 'filter_key' => 'a!position', + 'position' => 'position', + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + ), + ); + $this->fieldImageSettings = array('name' => 'texture', 'dir' => 'co'); + } + + /** + * AdminController::renderList() override. + * + * @see AdminController::renderList() + */ + public function renderList() + { + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + return parent::renderList(); + } + + public function renderView() + { + if (($id = Tools::getValue('id_attribute_group'))) { + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + $this->position_identifier = 'id_attribute'; + $this->position_group_identifier = 'id_attribute_group'; + $this->list_id = 'attribute_values'; + $this->lang = true; + + $this->context->smarty->assign(array( + 'current' => self::$currentIndex . '&id_attribute_group=' . (int) $id . '&viewattribute_group', + )); + + if (!Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Catalog.Notification') . + ' ' . $this->table . ' ' . + $this->trans('(cannot load object)', array(), 'Admin.Catalog.Notification'); + + return; + } + + $this->attribute_name = $obj->name; + $this->fields_list = array( + 'id_attribute' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Value', array(), 'Admin.Catalog.Feature'), + 'width' => 'auto', + 'filter_key' => 'b!name', + ), + ); + + if ($obj->group_type == 'color') { + $this->fields_list['color'] = array( + 'title' => $this->trans('Color', array(), 'Admin.Catalog.Feature'), + 'filter_key' => 'a!color', + ); + } + + $this->fields_list['position'] = array( + 'title' => $this->trans('Position', array(), 'Admin.Global'), + 'filter_key' => 'a!position', + 'position' => 'position', + 'class' => 'fixed-width-md', + ); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_where = 'AND a.`id_attribute_group` = ' . (int) $id; + $this->_orderBy = 'position'; + + self::$currentIndex = self::$currentIndex . '&id_attribute_group=' . (int) $id . '&viewattribute_group'; + $this->processFilter(); + + return parent::renderList(); + } + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $this->table = 'attribute_group'; + $this->identifier = 'id_attribute_group'; + + $group_type = array( + array( + 'id' => 'select', + 'name' => $this->trans('Drop-down list', array(), 'Admin.Global'), + ), + array( + 'id' => 'radio', + 'name' => $this->trans('Radio buttons', array(), 'Admin.Global'), + ), + array( + 'id' => 'color', + 'name' => $this->trans('Color or texture', array(), 'Admin.Catalog.Feature'), + ), + ); + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Attributes', array(), 'Admin.Catalog.Feature'), + 'icon' => 'icon-info-sign', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Your internal name for this attribute.', array(), 'Admin.Catalog.Help') . ' ' . $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Public name', array(), 'Admin.Catalog.Feature'), + 'name' => 'public_name', + 'lang' => true, + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('The public name for this attribute, displayed to the customers.', array(), 'Admin.Catalog.Help') . ' ' . $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'select', + 'label' => $this->trans('Attribute type', array(), 'Admin.Catalog.Feature'), + 'name' => 'group_type', + 'required' => true, + 'options' => array( + 'query' => $group_type, + 'id' => 'id', + 'name' => 'name', + ), + 'col' => '2', + 'hint' => $this->trans('The way the attribute\'s values will be presented to the customers in the product\'s page.', array(), 'Admin.Catalog.Help'), + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + if (!($obj = $this->loadObject(true))) { + return; + } + + return parent::renderForm(); + } + + public function renderFormAttributes() + { + $attributes_groups = AttributeGroup::getAttributesGroups($this->context->language->id); + + $this->table = 'attribute'; + $this->identifier = 'id_attribute'; + + $this->show_form_cancel_button = true; + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Values', array(), 'Admin.Global'), + 'icon' => 'icon-info-sign', + ), + 'input' => array( + array( + 'type' => 'select', + 'label' => $this->trans('Attribute group', array(), 'Admin.Catalog.Feature'), + 'name' => 'id_attribute_group', + 'required' => true, + 'options' => array( + 'query' => $attributes_groups, + 'id' => 'id_attribute_group', + 'name' => 'name', + ), + 'hint' => $this->trans('Choose the attribute group for this value.', array(), 'Admin.Catalog.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Value', array(), 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + ), + ); + + if (Shop::isFeatureActive()) { + // We get all associated shops for all attribute groups, because we will disable group shops + // for attributes that the selected attribute group don't support + $sql = 'SELECT id_attribute_group, id_shop FROM ' . _DB_PREFIX_ . 'attribute_group_shop'; + $associations = array(); + foreach (Db::getInstance()->executeS($sql) as $row) { + $associations[$row['id_attribute_group']][] = $row['id_shop']; + } + + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + 'values' => Shop::getTree(), + ); + } else { + $associations = array(); + } + + $this->fields_form['shop_associations'] = json_encode($associations); + + $this->fields_form['input'][] = array( + 'type' => 'color', + 'label' => $this->trans('Color', array(), 'Admin.Catalog.Feature'), + 'name' => 'color', + 'hint' => $this->trans('Choose a color with the color picker, or enter an HTML color (e.g. "lightblue", "#CC6600").', array(), 'Admin.Catalog.Help'), + ); + + $this->fields_form['input'][] = array( + 'type' => 'file', + 'label' => $this->trans('Texture', array(), 'Admin.Catalog.Feature'), + 'name' => 'texture', + 'hint' => array( + $this->trans('Upload an image file containing the color texture from your computer.', array(), 'Admin.Catalog.Help'), + $this->trans('This will override the HTML color!', array(), 'Admin.Catalog.Help'), + ), + ); + + $this->fields_form['input'][] = array( + 'type' => 'current_texture', + 'label' => $this->trans('Current texture', array(), 'Admin.Catalog.Feature'), + 'name' => 'current_texture', + ); + + $this->fields_form['input'][] = array( + 'type' => 'closediv', + 'name' => '', + ); + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + $this->fields_form['buttons'] = array( + 'save-and-stay' => array( + 'title' => $this->trans('Save then add another value', array(), 'Admin.Catalog.Feature'), + 'name' => 'submitAdd' . $this->table . 'AndStay', + 'type' => 'submit', + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-save', + ), + ); + + $this->fields_value['id_attribute_group'] = (int) Tools::getValue('id_attribute_group'); + + // Override var of Controller + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + $this->lang = true; + $this->tpl_folder = 'attributes/'; + + // Create object Attribute + if (!$obj = new Attribute((int) Tools::getValue($this->identifier))) { + return; + } + + $str_attributes_groups = ''; + foreach ($attributes_groups as $attribute_group) { + $str_attributes_groups .= '"' . $attribute_group['id_attribute_group'] . '" : ' . ($attribute_group['group_type'] == 'color' ? '1' : '0') . ', '; + } + + $image = '../img/' . $this->fieldImageSettings['dir'] . '/' . (int) $obj->id . '.jpg'; + + $this->tpl_form_vars = array( + 'strAttributesGroups' => $str_attributes_groups, + 'colorAttributeProperties' => Validate::isLoadedObject($obj) && $obj->isColorAttribute(), + 'imageTextureExists' => file_exists(_PS_IMG_DIR_ . $this->fieldImageSettings['dir'] . '/' . (int) $obj->id . '.jpg'), + 'imageTexture' => $image, + 'imageTextureUrl' => Tools::safeOutput($_SERVER['REQUEST_URI']) . '&deleteImage=1', + ); + + return parent::renderForm(); + } + + /** + * AdminController::init() override. + * + * @see AdminController::init() + */ + public function init() + { + if (Tools::isSubmit('updateattribute')) { + $this->display = 'editAttributes'; + } elseif (Tools::isSubmit('submitAddattribute')) { + $this->display = 'editAttributes'; + } elseif (Tools::isSubmit('submitAddattribute_group')) { + $this->display = 'add'; + } + + parent::init(); + } + + /** + * Override processAdd to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processUpdate() + */ + public function processAdd() + { + if ($this->table == 'attribute') { + /** @var AttributeGroup $object */ + $object = new $this->className(); + foreach (Language::getLanguages(false) as $language) { + if ($object->isAttribute((int) Tools::getValue('id_attribute_group'), + Tools::getValue('name_' . $language['id_lang']), $language['id_lang'])) { + $this->errors['name_' . $language['id_lang']] = $this->trans('The attribute value "%1$s" already exist for %2$s language', + array( + Tools::getValue('name_' . $language['id_lang']), + $language['name'], + ), + 'Admin.Catalog.Notification'); + } + } + + if (!empty($this->errors)) { + return $object; + } + } + + $object = parent::processAdd(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + if ($this->display == 'add') { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } else { + $this->redirect_after = self::$currentIndex . '&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&conf=3&update' . $this->table . '&token=' . $this->token; + } + } + + if (count($this->errors)) { + $this->setTypeAttribute(); + } + + return $object; + } + + /** + * Override processUpdate to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processUpdate() + */ + public function processUpdate() + { + $object = parent::processUpdate(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + if ($this->display == 'add') { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } else { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&conf=3&update' . $this->table . '&token=' . $this->token; + } + } + + if (count($this->errors)) { + $this->setTypeAttribute(); + } + + if (Tools::isSubmit('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute') || Tools::isSubmit('submitBulkdeleteattribute')) { + Tools::clearColorListCache(); + } + + return $object; + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + if (Combination::isFeatureActive()) { + if ($this->display == 'edit' || $this->display == 'add') { + if (!($this->object = $this->loadObject(true))) { + return; + } + $this->content .= $this->renderForm(); + } elseif ($this->display == 'editAttributes') { + if (!$this->object = new Attribute((int) Tools::getValue('id_attribute'))) { + return; + } + + $this->content .= $this->renderFormAttributes(); + } elseif ($this->display != 'view' && !$this->ajax) { + $this->content .= $this->renderList(); + $this->content .= $this->renderOptions(); + /* reset all attributes filter */ + if (!Tools::getValue('submitFilterattribute_group', 0) && !Tools::getIsset('id_attribute_group')) { + $this->processResetFilters('attribute_values'); + } + } elseif ($this->display == 'view' && !$this->ajax) { + $this->content = $this->renderView(); + } + } else { + $adminPerformanceUrl = $this->context->link->getAdminLink('AdminPerformance'); + $url = '' . $this->trans('Performance', array(), 'Admin.Global') . ''; + $this->displayWarning($this->trans('This feature has been disabled. You can activate it here: %link%.', array('%link%' => $url), 'Admin.Catalog.Notification')); + } + + $this->context->smarty->assign(array( + 'table' => $this->table, + 'current' => self::$currentIndex, + 'token' => $this->token, + 'content' => $this->content, + )); + } + + public function initPageHeaderToolbar() + { + if (Combination::isFeatureActive()) { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_attribute_group'] = array( + 'href' => self::$currentIndex . '&addattribute_group&token=' . $this->token, + 'desc' => $this->trans('Add new attribute', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + $this->page_header_toolbar_btn['new_value'] = array( + 'href' => self::$currentIndex . '&updateattribute&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&token=' . $this->token, + 'desc' => $this->trans('Add new value', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + } + } + + if ($this->display == 'view') { + $this->page_header_toolbar_btn['new_value'] = array( + 'href' => self::$currentIndex . '&updateattribute&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&token=' . $this->token, + 'desc' => $this->trans('Add new value', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function initToolbar() + { + switch ($this->display) { + // @todo defining default buttons + case 'add': + case 'edit': + case 'editAttributes': + // Default save button - action dynamically handled in javascript + $this->toolbar_btn['save'] = array( + 'href' => '#', + 'desc' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + if ($this->display == 'editAttributes' && !$this->id_attribute) { + $this->toolbar_btn['save-and-stay'] = array( + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save then add another value', array(), 'Admin.Catalog.Help'), + 'force_desc' => true, + ); + } + + $this->toolbar_btn['back'] = array( + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups'), + 'desc' => $this->trans('Back to list', array(), 'Admin.Actions'), + ); + break; + case 'view': + $this->toolbar_btn['newAttributes'] = array( + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups', true, array(), array('updateattribute' => 1, 'id_attribute_group' => (int) Tools::getValue('id_attribute_group'))), + 'desc' => $this->trans('Add New Values', array(), 'Admin.Catalog.Feature'), + 'class' => 'toolbar-new', + ); + + $this->toolbar_btn['back'] = array( + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups'), + 'desc' => $this->trans('Back to list', array(), 'Admin.Actions'), + ); + break; + default: // list + $this->toolbar_btn['new'] = array( + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups', true, array(), array('add' . $this->table => 1)), + 'desc' => $this->trans('Add New Attributes', array(), 'Admin.Catalog.Feature'), + ); + if ($this->can_import) { + $this->toolbar_btn['import'] = array( + 'href' => $this->context->link->getAdminLink('AdminImport', true, array(), array('import_type' => 'combinations')), + 'desc' => $this->trans('Import', array(), 'Admin.Actions'), + ); + } + } + } + + public function initToolbarTitle() + { + $bread_extended = $this->breadcrumbs; + + switch ($this->display) { + case 'edit': + $bread_extended[] = $this->trans('Edit New Attribute', array(), 'Admin.Catalog.Feature'); + break; + + case 'add': + $bread_extended[] = $this->trans('Add New Attribute', array(), 'Admin.Catalog.Feature'); + break; + + case 'view': + if (Tools::getIsset('viewattribute_group')) { + if (($id = Tools::getValue('id_attribute_group'))) { + if (Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $bread_extended[] = $obj->name[$this->context->employee->id_lang]; + } + } + } else { + $bread_extended[] = $this->attribute_name[$this->context->employee->id_lang]; + } + break; + + case 'editAttributes': + if ($this->id_attribute) { + if (($id = Tools::getValue('id_attribute_group'))) { + if (Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $bread_extended[] = '' . $obj->name[$this->context->employee->id_lang] . ''; + } + if (Validate::isLoadedObject($obj = new Attribute((int) $this->id_attribute))) { + $bread_extended[] = $this->trans( + 'Edit: %value%', + array( + '%value%' => $obj->name[$this->context->employee->id_lang], + ), + 'Admin.Catalog.Feature' + ); + } + } else { + $bread_extended[] = $this->trans('Edit Value', array(), 'Admin.Catalog.Feature'); + } + } else { + $bread_extended[] = $this->trans('Add New Value', array(), 'Admin.Catalog.Feature'); + } + break; + } + + if (count($bread_extended) > 0) { + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + } + + $this->toolbar_title = $bread_extended; + } + + public function initProcess() + { + $this->setTypeAttribute(); + + if (Tools::getIsset('viewattribute_group')) { + $this->list_id = 'attribute_values'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + } else { + $this->list_id = 'attribute_group'; + } + + parent::initProcess(); + + if ($this->table == 'attribute') { + $this->display = 'editAttributes'; + $this->id_attribute = (int) Tools::getValue('id_attribute'); + } + } + + protected function setTypeAttribute() + { + if (Tools::isSubmit('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute') || Tools::isSubmit('submitBulkdeleteattribute')) { + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + + if ($this->display == 'edit') { + $this->display = 'editAttributes'; + } + } + } + + public function processPosition() + { + if (Tools::getIsset('viewattribute_group')) { + $object = new Attribute((int) Tools::getValue('id_attribute')); + self::$currentIndex = self::$currentIndex . '&viewattribute_group'; + } else { + $object = new AttributeGroup((int) Tools::getValue('id_attribute_group')); + } + + if (!Validate::isLoadedObject($object)) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') . + ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', array(), 'Admin.Notifications.Error'); + } else { + $id_identifier_str = ($id_identifier = (int) Tools::getValue($this->identifier)) ? '&' . $this->identifier . '=' . $id_identifier : ''; + $redirect = self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=5' . $id_identifier_str . '&token=' . $this->token; + $this->redirect_after = $redirect; + } + + return $object; + } + + /** + * Call the right method for creating or updating object. + * + * @return mixed + */ + public function processSave() + { + if ($this->display == 'add' || $this->display == 'edit') { + $this->identifier = 'id_attribute_group'; + } + + if (!$this->id_object) { + return $this->processAdd(); + } else { + return $this->processUpdate(); + } + } + + public function postProcess() + { + if (!Combination::isFeatureActive()) { + return; + } + + if (!Tools::getValue($this->identifier) && Tools::getValue('id_attribute') && !Tools::getValue('attributeOrderby')) { + // Override var of Controller + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + } + + /* set location with current index */ + if (Tools::getIsset('id_attribute_group') && Tools::getIsset('viewattribute_group')) { + self::$currentIndex = self::$currentIndex . '&id_attribute_group=' . Tools::getValue('id_attribute_group', 0) . '&viewattribute_group'; + } + + // If it's an attribute, load object Attribute() + if (Tools::getValue('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute')) { + if (true !== $this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + + return; + } elseif (!$object = new Attribute((int) Tools::getValue($this->identifier))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') . + ' ' . $this->table . ' ' . + $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + + return; + } + + if (Tools::getValue('position') !== false && Tools::getValue('id_attribute')) { + $_POST['id_attribute_group'] = $object->id_attribute_group; + if (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', array(), 'Admin.Notifications.Error'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=5&token=' . Tools::getAdminTokenLite('AdminAttributesGroups') . '#details_details_' . $object->id_attribute_group); + } + } elseif (Tools::isSubmit('deleteattribute') && Tools::getValue('id_attribute')) { + if (!$object->delete()) { + $this->errors[] = $this->trans('Failed to delete the attribute.', array(), 'Admin.Catalog.Notification'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . Tools::getAdminTokenLite('AdminAttributesGroups')); + } + } elseif (Tools::isSubmit('submitAddattribute')) { + Hook::exec('actionObjectAttributeAddBefore'); + $this->action = 'save'; + $id_attribute = (int) Tools::getValue('id_attribute'); + // Adding last position to the attribute if not exist + if ($id_attribute <= 0) { + $sql = 'SELECT `position`+1 + FROM `' . _DB_PREFIX_ . 'attribute` + WHERE `id_attribute_group` = ' . (int) Tools::getValue('id_attribute_group') . ' + ORDER BY position DESC'; + // set the position of the new group attribute in $_POST for postProcess() method + $_POST['position'] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + } + $_POST['id_parent'] = 0; + $this->processSave($this->token); + } + + if (Tools::getValue('id_attribute') && Tools::isSubmit('submitAddattribute') && Tools::getValue('color') && !Tools::getValue('filename')) { + if (file_exists(_PS_IMG_DIR_ . $this->fieldImageSettings['dir'] . '/' . (int) Tools::getValue('id_attribute') . '.jpg')) { + unlink(_PS_IMG_DIR_ . $this->fieldImageSettings['dir'] . '/' . (int) Tools::getValue('id_attribute') . '.jpg'); + } + } + } else { + if (Tools::isSubmit('submitBulkdelete' . $this->table)) { + if ($this->access('delete')) { + if (isset($_POST[$this->list_id . 'Box'])) { + /** @var AttributeGroup $object */ + $object = new $this->className(); + if ($object->deleteSelection($_POST[$this->list_id . 'Box'])) { + AttributeGroup::cleanPositions(); + Tools::redirectAdmin(self::$currentIndex . '&conf=2' . '&token=' . $this->token); + } + $this->errors[] = $this->trans('An error occurred while deleting this selection.', array(), 'Admin.Notifications.Error'); + } else { + $this->errors[] = $this->trans('You must select at least one element to delete.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + // clean position after delete + AttributeGroup::cleanPositions(); + } elseif (Tools::isSubmit('submitAdd' . $this->table)) { + Hook::exec('actionObjectAttributeGroupAddBefore'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + // Adding last position to the attribute if not exist + if ($id_attribute_group <= 0) { + $sql = 'SELECT `position`+1 + FROM `' . _DB_PREFIX_ . 'attribute_group` + ORDER BY position DESC'; + // set the position of the new group attribute in $_POST for postProcess() method + $_POST['position'] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + } + // clean \n\r characters + foreach ($_POST as $key => $value) { + if (preg_match('/^name_/Ui', $key)) { + $_POST[$key] = str_replace('\n', '', str_replace('\r', '', $value)); + } + } + parent::postProcess(); + } else { + parent::postProcess(); + if (Tools::isSubmit('delete' . $this->table)) { + AttributeGroup::cleanPositions(); + } + } + } + } + + /** + * AdminController::getList() override. + * + * @see AdminController::getList() + * + * @param int $id_lang + * @param string|null $order_by + * @param string|null $order_way + * @param int $start + * @param int|null $limit + * @param int|bool $id_lang_shop + * + * @throws PrestaShopException + */ + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + if ($this->display == 'view') { + foreach ($this->_list as &$list) { + if (file_exists(_PS_IMG_DIR_ . $this->fieldImageSettings['dir'] . '/' . (int) $list['id_attribute'] . '.jpg')) { + if (!isset($list['color']) || !is_array($list['color'])) { + $list['color'] = array(); + } + $list['color']['texture'] = '../img/' . $this->fieldImageSettings['dir'] . '/' . (int) $list['id_attribute'] . '.jpg'; + } + } + } else { + $nb_items = count($this->_list); + for ($i = 0; $i < $nb_items; ++$i) { + $item = &$this->_list[$i]; + + $query = new DbQuery(); + $query->select('COUNT(a.id_attribute) as count_values'); + $query->from('attribute', 'a'); + $query->join(Shop::addSqlAssociation('attribute', 'a')); + $query->where('a.id_attribute_group =' . (int) $item['id_attribute_group']); + $query->groupBy('attribute_shop.id_shop'); + $query->orderBy('count_values DESC'); + $item['count_values'] = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query); + unset($query); + } + } + } + + /** + * Overrides parent to delete items from sublist. + * + * @return mixed + */ + public function processBulkDelete() + { + // If we are deleting attributes instead of attribute_groups + if (Tools::getIsset('attributeBox')) { + $this->className = 'Attribute'; + $this->table = 'attribute'; + $this->boxes = Tools::getValue($this->table . 'Box'); + } + + $result = parent::processBulkDelete(); + // Restore vars + $this->className = 'AttributeGroup'; + $this->table = 'attribute_group'; + + return $result; + } + + /* Modify group attribute position */ + public function ajaxProcessUpdateGroupsPositions() + { + $way = (int) Tools::getValue('way'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + $positions = Tools::getValue('attribute_group'); + + $new_positions = array(); + foreach ($positions as $v) { + if (count(explode('_', $v)) == 4) { + $new_positions[] = $v; + } + } + + foreach ($new_positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_attribute_group) { + if ($group_attribute = new AttributeGroup((int) $pos[2])) { + if (isset($position) && $group_attribute->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for attribute group ' . (int) $pos[2] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update the ' . (int) $id_attribute_group . ' attribute group to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "The (' . (int) $id_attribute_group . ') attribute group cannot be loaded."}'; + } + + break; + } + } + } + + /* Modify attribute position */ + public function ajaxProcessUpdateAttributesPositions() + { + $way = (int) Tools::getValue('way'); + $id_attribute = (int) Tools::getValue('id_attribute'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + $positions = Tools::getValue('attribute'); + + if (is_array($positions)) { + foreach ($positions as $position => $value) { + $pos = explode('_', $value); + + if ((isset($pos[1]) && isset($pos[2])) && (int) $pos[2] === $id_attribute) { + if ($attribute = new Attribute((int) $pos[2])) { + if (isset($position) && $attribute->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for attribute ' . (int) $pos[2] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update the ' . (int) $id_attribute . ' attribute to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "The (' . (int) $id_attribute . ') attribute cannot be loaded"}'; + } + + break; + } + } + } + } +} diff --git a/controllers/admin/AdminCarrierWizardController.php b/controllers/admin/AdminCarrierWizardController.php new file mode 100644 index 00000000..816866f3 --- /dev/null +++ b/controllers/admin/AdminCarrierWizardController.php @@ -0,0 +1,983 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Carrier $object + */ +class AdminCarrierWizardControllerCore extends AdminController +{ + protected $wizard_access; + + public function __construct() + { + $this->bootstrap = true; + $this->display = 'view'; + $this->table = 'carrier'; + $this->identifier = 'id_carrier'; + $this->className = 'Carrier'; + $this->lang = false; + $this->deleted = true; + $this->step_number = 0; + $this->type_context = Shop::getContext(); + $this->old_context = Context::getContext(); + $this->multishop_context = Shop::CONTEXT_ALL; + $this->context = Context::getContext(); + + $this->fieldImageSettings = array( + 'name' => 'logo', + 'dir' => 's', + ); + + parent::__construct(); + + $this->tabAccess = Profile::getProfileAccess($this->context->employee->id_profile, Tab::getIdFromClassName('AdminCarriers')); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryPlugin('smartWizard'); + $this->addJqueryPlugin('typewatch'); + $this->addJs(_PS_JS_DIR_ . 'admin/carrier_wizard.js'); + } + + public function initWizard() + { + $this->wizard_steps = array( + 'name' => 'carrier_wizard', + 'steps' => array( + array( + 'title' => $this->trans('General settings', array(), 'Admin.Shipping.Feature'), + ), + array( + 'title' => $this->trans('Shipping locations and costs', array(), 'Admin.Shipping.Feature'), + ), + array( + 'title' => $this->trans('Size, weight, and group access', array(), 'Admin.Shipping.Feature'), + ), + array( + 'title' => $this->trans('Summary', array(), 'Admin.Global'), + ), ), + ); + + if (Shop::isFeatureActive()) { + $multistore_step = array( + array( + 'title' => $this->trans('MultiStore', array(), 'Admin.Global'), + ), + ); + array_splice($this->wizard_steps['steps'], 1, 0, $multistore_step); + } + } + + public function renderView() + { + $this->initWizard(); + + if (Tools::getValue('id_carrier') && $this->access('edit')) { + $carrier = $this->loadObject(); + } elseif ($this->access('add')) { + $carrier = new Carrier(); + } + + if ((!$this->access('edit') && Tools::getValue('id_carrier')) || (!$this->access('add') && !Tools::getValue('id_carrier'))) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', array(), 'Admin.Shipping.Notification'); + + return; + } + + $currency = $this->getActualCurrency(); + + $this->tpl_view_vars = array( + 'currency_sign' => $currency->sign, + 'PS_WEIGHT_UNIT' => Configuration::get('PS_WEIGHT_UNIT'), + 'enableAllSteps' => Validate::isLoadedObject($carrier), + 'wizard_steps' => $this->wizard_steps, + 'validate_url' => $this->context->link->getAdminLink('AdminCarrierWizard'), + 'carrierlist_url' => $this->context->link->getAdminLink('AdminCarriers') . '&conf=' . ((int) Validate::isLoadedObject($carrier) ? 4 : 3), + 'multistore_enable' => Shop::isFeatureActive(), + 'wizard_contents' => array( + 'contents' => array( + 0 => $this->renderStepOne($carrier), + 1 => $this->renderStepThree($carrier), + 2 => $this->renderStepFour($carrier), + 3 => $this->renderStepFive($carrier), + ), + ), + 'labels' => array( + 'next' => $this->trans('Next', array(), 'Admin.Global'), + 'previous' => $this->trans('Previous', array(), 'Admin.Global'), + 'finish' => $this->trans('Finish', array(), 'Admin.Actions'), ), + ); + + if (Shop::isFeatureActive()) { + array_splice($this->tpl_view_vars['wizard_contents']['contents'], 1, 0, array(0 => $this->renderStepTwo($carrier))); + } + + $this->context->smarty->assign(array( + 'carrier_logo' => (Validate::isLoadedObject($carrier) && file_exists(_PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg') ? _THEME_SHIP_DIR_ . $carrier->id . '.jpg' : false), + )); + + $this->context->smarty->assign(array( + 'logo_content' => $this->createTemplate('logo.tpl')->fetch(), + )); + + $this->addjQueryPlugin(array('ajaxfileupload')); + + return parent::renderView(); + } + + public function initBreadcrumbs($tab_id = null, $tabs = null) + { + if (Tools::getValue('id_carrier')) { + $this->display = 'edit'; + } else { + $this->display = 'add'; + } + + parent::initBreadcrumbs((int) Tab::getIdFromClassName('AdminCarriers')); + + $this->display = 'view'; + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + $this->page_header_toolbar_btn['cancel'] = array( + 'href' => $this->context->link->getAdminLink('AdminCarriers'), + 'desc' => $this->trans('Cancel', array(), 'Admin.Actions'), + ); + } + + public function renderStepOne($carrier) + { + $this->fields_form = array( + 'form' => array( + 'id_form' => 'step_carrier_general', + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Carrier name', array(), 'Admin.Shipping.Feature'), + 'name' => 'name', + 'required' => true, + 'hint' => array( + $this->trans('Allowed characters: letters, spaces and "%special_chars%".', array('%special_chars%' => '().-'), 'Admin.Shipping.Help'), + $this->trans('The carrier\'s name will be displayed during checkout.', array(), 'Admin.Shipping.Help'), + $this->trans('For in-store pickup, enter 0 to replace the carrier name with your shop name.', array(), 'Admin.Shipping.Help'), + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Transit time', array(), 'Admin.Shipping.Feature'), + 'name' => 'delay', + 'lang' => true, + 'required' => true, + 'maxlength' => 512, + 'hint' => $this->trans('The delivery time will be displayed during checkout.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Speed grade', array(), 'Admin.Shipping.Feature'), + 'name' => 'grade', + 'required' => false, + 'size' => 1, + 'hint' => $this->trans('Enter "0" for a longest shipping delay, or "9" for the shortest shipping delay.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'logo', + 'label' => $this->trans('Logo', array(), 'Admin.Global'), + 'name' => 'logo', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Tracking URL', array(), 'Admin.Shipping.Feature'), + 'name' => 'url', + 'hint' => $this->trans('Delivery tracking URL: Type \'@\' where the tracking number should appear. It will be automatically replaced by the tracking number.', array(), 'Admin.Shipping.Help'), + 'desc' => $this->trans('For example: \'http://example.com/track.php?num=@\' with \'@\' where the tracking number should appear.', array(), 'Admin.Shipping.Help'), + ), + ), + ), + ); + + $tpl_vars = array('max_image_size' => (int) Configuration::get('PS_PRODUCT_PICTURE_MAX_SIZE') / 1024 / 1024); + $fields_value = $this->getStepOneFieldsValues($carrier); + + return $this->renderGenericForm(array('form' => $this->fields_form), $fields_value, $tpl_vars); + } + + public function renderStepTwo($carrier) + { + $this->fields_form = array( + 'form' => array( + 'id_form' => 'step_carrier_shops', + 'force' => true, + 'input' => array( + array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ), + ), + ), + ); + $fields_value = $this->getStepTwoFieldsValues($carrier); + + return $this->renderGenericForm(array('form' => $this->fields_form), $fields_value); + } + + public function renderStepThree($carrier) + { + $this->fields_form = array( + 'form' => array( + 'id_form' => 'step_carrier_ranges', + 'input' => array( + 'shipping_handling' => array( + 'type' => 'switch', + 'label' => $this->trans('Add handling costs', array(), 'Admin.Shipping.Feature'), + 'name' => 'shipping_handling', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'shipping_handling_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'shipping_handling_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Include the handling costs (as set in Shipping > Preferences) in the final carrier price.', array(), 'Admin.Shipping.Help'), + ), + 'is_free' => array( + 'type' => 'switch', + 'label' => $this->trans('Free shipping', array(), 'Admin.Shipping.Feature'), + 'name' => 'is_free', + 'required' => false, + 'class' => 't', + 'values' => array( + array( + 'id' => 'is_free_on', + 'value' => 1, + 'label' => '' . $this->trans('No', array(), 'Admin.Global') . '', + ), + array( + 'id' => 'is_free_off', + 'value' => 0, + 'label' => '' . $this->trans('Yes', array(), 'Admin.Global') . '', + ), + ), + ), + 'shipping_method' => array( + 'type' => 'radio', + 'label' => $this->trans('Billing', array(), 'Admin.Shipping.Feature'), + 'name' => 'shipping_method', + 'required' => false, + 'class' => 't', + 'br' => true, + 'values' => array( + array( + 'id' => 'billing_price', + 'value' => Carrier::SHIPPING_METHOD_PRICE, + 'label' => $this->trans('According to total price.', array(), 'Admin.Shipping.Feature'), + ), + array( + 'id' => 'billing_weight', + 'value' => Carrier::SHIPPING_METHOD_WEIGHT, + 'label' => $this->trans('According to total weight.', array(), 'Admin.Shipping.Feature'), + ), + ), + ), + 'id_tax_rules_group' => array( + 'type' => 'select', + 'label' => $this->trans('Tax', array(), 'Admin.Global'), + 'name' => 'id_tax_rules_group', + 'options' => array( + 'query' => TaxRulesGroup::getTaxRulesGroups(true), + 'id' => 'id_tax_rules_group', + 'name' => 'name', + 'default' => array( + 'label' => $this->trans('No tax', array(), 'Admin.Global'), + 'value' => 0, + ), + ), + ), + 'range_behavior' => array( + 'type' => 'select', + 'label' => $this->trans('Out-of-range behavior', array(), 'Admin.Shipping.Feature'), + 'name' => 'range_behavior', + 'options' => array( + 'query' => array( + array( + 'id' => 0, + 'name' => $this->trans('Apply the cost of the highest defined range', array(), 'Admin.Shipping.Feature'), + ), + array( + 'id' => 1, + 'name' => $this->trans('Disable carrier', array(), 'Admin.Shipping.Feature'), + ), + ), + 'id' => 'id', + 'name' => 'name', + ), + 'hint' => $this->trans('Out-of-range behavior occurs when no defined range matches the customer\'s cart (e.g. when the weight of the cart is greater than the highest weight limit defined by the weight ranges).', array(), 'Admin.Shipping.Help'), + ), + 'zones' => array( + 'type' => 'zone', + 'name' => 'zones', + ), + ), + ), + ); + + if (Configuration::get('PS_ATCP_SHIPWRAP')) { + unset($this->fields_form['form']['input']['id_tax_rules_group']); + } + + $tpl_vars = array(); + $tpl_vars['PS_WEIGHT_UNIT'] = Configuration::get('PS_WEIGHT_UNIT'); + + $currency = $this->getActualCurrency(); + + $tpl_vars['currency_sign'] = $currency->sign; + + $fields_value = $this->getStepThreeFieldsValues($carrier); + + $this->getTplRangesVarsAndValues($carrier, $tpl_vars, $fields_value); + + return $this->renderGenericForm(array('form' => $this->fields_form), $fields_value, $tpl_vars); + } + + /** + * @param Carrier $carrier + * + * @return string + */ + public function renderStepFour($carrier) + { + $this->fields_form = array( + 'form' => array( + 'id_form' => 'step_carrier_conf', + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Maximum package width (%s)', array('%s' => Configuration::get('PS_DIMENSION_UNIT')), 'Admin.Shipping.Feature'), + 'name' => 'max_width', + 'required' => false, + 'hint' => $this->trans('Maximum width managed by this carrier. Set the value to "0", or leave this field blank to ignore.', array(), 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Maximum package height (%s)', array('%s' => Configuration::get('PS_DIMENSION_UNIT')), 'Admin.Shipping.Feature'), + 'name' => 'max_height', + 'required' => false, + 'hint' => $this->trans('Maximum height managed by this carrier. Set the value to "0", or leave this field blank to ignore.', array(), 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Maximum package depth (%s)', array('%s' => Configuration::get('PS_DIMENSION_UNIT')), 'Admin.Shipping.Feature'), + 'name' => 'max_depth', + 'required' => false, + 'hint' => $this->trans('Maximum depth managed by this carrier. Set the value to "0", or leave this field blank to ignore.', array(), 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Maximum package weight (%s)', array('%s' => Configuration::get('PS_WEIGHT_UNIT')), 'Admin.Shipping.Feature'), + 'name' => 'max_weight', + 'required' => false, + 'hint' => $this->trans('Maximum weight managed by this carrier. Set the value to "0", or leave this field blank to ignore.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'group', + 'label' => $this->trans('Group access', array(), 'Admin.Shipping.Feature'), + 'name' => 'groupBox', + 'values' => Group::getGroups(Context::getContext()->language->id), + 'hint' => $this->trans('Mark the groups that are allowed access to this carrier.', array(), 'Admin.Shipping.Help'), + ), + ), + ), + ); + + $fields_value = $this->getStepFourFieldsValues($carrier); + + // Added values of object Group + $carrier_groups = $carrier->getGroups(); + $carrier_groups_ids = array(); + if (is_array($carrier_groups)) { + foreach ($carrier_groups as $carrier_group) { + $carrier_groups_ids[] = $carrier_group['id_group']; + } + } + + $groups = Group::getGroups($this->context->language->id); + + foreach ($groups as $group) { + $fields_value['groupBox_' . $group['id_group']] = Tools::getValue('groupBox_' . $group['id_group'], (in_array($group['id_group'], $carrier_groups_ids) || empty($carrier_groups_ids) && !$carrier->id)); + } + + return $this->renderGenericForm(array('form' => $this->fields_form), $fields_value); + } + + public function renderStepFive($carrier) + { + $this->fields_form = array( + 'form' => array( + 'id_form' => 'step_carrier_summary', + 'input' => array( + array( + 'type' => 'switch', + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + ), + array( + 'id' => 'active_off', + 'value' => 0, + ), + ), + 'hint' => $this->trans('Enable the carrier in the front office.', array(), 'Admin.Shipping.Help'), + ), + ), + ), + ); + $template = $this->createTemplate('controllers/carrier_wizard/summary.tpl'); + $fields_value = $this->getStepFiveFieldsValues($carrier); + $active_form = $this->renderGenericForm(array('form' => $this->fields_form), $fields_value); + $active_form = str_replace(array('
', '
'), '', $active_form); + $template->assign('active_form', $active_form); + + return $template->fetch(); + } + + /** + * @param Carrier $carrier + * @param array $tpl_vars + * @param array $fields_value + */ + protected function getTplRangesVarsAndValues($carrier, &$tpl_vars, &$fields_value) + { + $tpl_vars['zones'] = Zone::getZones(false, true); + $carrier_zones = $carrier->getZones(); + $carrier_zones_ids = array(); + if (is_array($carrier_zones)) { + foreach ($carrier_zones as $carrier_zone) { + $carrier_zones_ids[] = $carrier_zone['id_zone']; + } + } + + $range_table = $carrier->getRangeTable(); + $shipping_method = $carrier->getShippingMethod(); + + $zones = Zone::getZones(false); + foreach ($zones as $zone) { + $fields_value['zones'][$zone['id_zone']] = Tools::getValue('zone_' . $zone['id_zone'], (in_array($zone['id_zone'], $carrier_zones_ids))); + } + + if ($shipping_method == Carrier::SHIPPING_METHOD_FREE) { + $range_obj = $carrier->getRangeObject($carrier->shipping_method); + $price_by_range = array(); + } else { + $range_obj = $carrier->getRangeObject(); + $price_by_range = Carrier::getDeliveryPriceByRanges($range_table, (int) $carrier->id); + } + + foreach ($price_by_range as $price) { + $tpl_vars['price_by_range'][$price['id_' . $range_table]][$price['id_zone']] = $price['price']; + } + + $tmp_range = $range_obj->getRanges((int) $carrier->id); + $tpl_vars['ranges'] = array(); + if ($shipping_method != Carrier::SHIPPING_METHOD_FREE) { + foreach ($tmp_range as $id => $range) { + $tpl_vars['ranges'][$range['id_' . $range_table]] = $range; + $tpl_vars['ranges'][$range['id_' . $range_table]]['id_range'] = $range['id_' . $range_table]; + } + } + + // init blank range + if (!count($tpl_vars['ranges'])) { + $tpl_vars['ranges'][] = array('id_range' => 0, 'delimiter1' => 0, 'delimiter2' => 0); + } + } + + public function renderGenericForm($fields_form, $fields_value, $tpl_vars = array()) + { + $helper = new HelperForm(); + $helper->show_toolbar = false; + $helper->table = $this->table; + $lang = new Language((int) Configuration::get('PS_LANG_DEFAULT')); + $helper->default_form_language = $lang->id; + $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') ? Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG') : 0; + $this->fields_form = array(); + $helper->id = (int) Tools::getValue('id_carrier'); + $helper->identifier = $this->identifier; + $helper->tpl_vars = array_merge(array( + 'fields_value' => $fields_value, + 'languages' => $this->getLanguages(), + 'id_language' => $this->context->language->id, + ), $tpl_vars); + $helper->override_folder = 'carrier_wizard/'; + + return $helper->generateForm($fields_form); + } + + public function getStepOneFieldsValues($carrier) + { + return array( + 'id_carrier' => $this->getFieldValue($carrier, 'id_carrier'), + 'name' => $this->getFieldValue($carrier, 'name'), + 'delay' => $this->getFieldValue($carrier, 'delay'), + 'grade' => $this->getFieldValue($carrier, 'grade'), + 'url' => $this->getFieldValue($carrier, 'url'), + ); + } + + public function getStepTwoFieldsValues($carrier) + { + return array('shop' => $this->getFieldValue($carrier, 'shop')); + } + + public function getStepThreeFieldsValues($carrier) + { + $id_tax_rules_group = (is_object($this->object) && !$this->object->id) ? Carrier::getIdTaxRulesGroupMostUsed() : Carrier::getIdTaxRulesGroupByIdCarrier($this->object->id); + + $shipping_handling = (is_object($this->object) && !$this->object->id) ? 0 : $this->getFieldValue($carrier, 'shipping_handling'); + + return array( + 'is_free' => $this->getFieldValue($carrier, 'is_free'), + 'id_tax_rules_group' => (int) $id_tax_rules_group, + 'shipping_handling' => $shipping_handling, + 'shipping_method' => $this->getFieldValue($carrier, 'shipping_method'), + 'range_behavior' => $this->getFieldValue($carrier, 'range_behavior'), + 'zones' => $this->getFieldValue($carrier, 'zones'), + ); + } + + public function getStepFourFieldsValues($carrier) + { + return array( + 'range_behavior' => $this->getFieldValue($carrier, 'range_behavior'), + 'max_height' => $this->getFieldValue($carrier, 'max_height'), + 'max_width' => $this->getFieldValue($carrier, 'max_width'), + 'max_depth' => $this->getFieldValue($carrier, 'max_depth'), + 'max_weight' => $this->getFieldValue($carrier, 'max_weight'), + 'group' => $this->getFieldValue($carrier, 'group'), + ); + } + + public function getStepFiveFieldsValues($carrier) + { + return array('active' => $this->getFieldValue($carrier, 'active')); + } + + public function ajaxProcessChangeRanges() + { + if ((Validate::isLoadedObject($this->object) && !$this->access('edit')) || !$this->access('add')) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', array(), 'Admin.Shipping.Notification'); + + return; + } + if ((!(int) $shipping_method = Tools::getValue('shipping_method')) || !in_array($shipping_method, array(Carrier::SHIPPING_METHOD_PRICE, Carrier::SHIPPING_METHOD_WEIGHT))) { + return; + } + + $carrier = $this->loadObject(true); + $carrier->shipping_method = $shipping_method; + + $tpl_vars = array(); + $fields_value = $this->getStepThreeFieldsValues($carrier); + $this->getTplRangesVarsAndValues($carrier, $tpl_vars, $fields_value); + $template = $this->createTemplate('controllers/carrier_wizard/helpers/form/form_ranges.tpl'); + $template->assign($tpl_vars); + $template->assign('change_ranges', 1); + + $template->assign('fields_value', $fields_value); + $template->assign('input', array('type' => 'zone', 'name' => 'zones')); + + $currency = $this->getActualCurrency(); + + $template->assign('currency_sign', $currency->sign); + $template->assign('PS_WEIGHT_UNIT', Configuration::get('PS_WEIGHT_UNIT')); + + die($template->fetch()); + } + + protected function validateForm($die = true) + { + $step_number = (int) Tools::getValue('step_number'); + $return = array('has_error' => false); + + if (!$this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', array(), 'Admin.Shipping.Notification'); + } else { + if (Shop::isFeatureActive() && $step_number == 2) { + if (!Tools::getValue('checkBoxShopAsso_carrier')) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('You must choose at least one shop or group shop.', array(), 'Admin.Shipping.Notification'); + } + } else { + $this->validateRules(); + } + } + + if (count($this->errors)) { + $return['has_error'] = true; + $return['errors'] = $this->errors; + } + if (count($this->errors) || $die) { + die(json_encode($return)); + } + } + + public function ajaxProcessValidateStep() + { + $this->validateForm(true); + } + + public function processRanges($id_carrier) + { + if (!$this->access('edit') || !$this->access('add')) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', array(), 'Admin.Shipping.Notification'); + + return; + } + + $carrier = new Carrier((int) $id_carrier); + if (!Validate::isLoadedObject($carrier)) { + return false; + } + + $range_inf = Tools::getValue('range_inf'); + $range_sup = Tools::getValue('range_sup'); + $range_type = Tools::getValue('shipping_method'); + + $fees = Tools::getValue('fees'); + + $carrier->deleteDeliveryPrice($carrier->getRangeTable()); + if ($range_type != Carrier::SHIPPING_METHOD_FREE) { + foreach ($range_inf as $key => $delimiter1) { + if (!isset($range_sup[$key])) { + continue; + } + $add_range = true; + if ($range_type == Carrier::SHIPPING_METHOD_WEIGHT) { + if (!RangeWeight::rangeExist(null, (float) $delimiter1, (float) $range_sup[$key], $carrier->id_reference)) { + $range = new RangeWeight(); + } else { + $range = new RangeWeight((int) $key); + $range->id_carrier = (int) $carrier->id; + $range->save(); + $add_range = false; + } + } + + if ($range_type == Carrier::SHIPPING_METHOD_PRICE) { + if (!RangePrice::rangeExist(null, (float) $delimiter1, (float) $range_sup[$key], $carrier->id_reference)) { + $range = new RangePrice(); + } else { + $range = new RangePrice((int) $key); + $range->id_carrier = (int) $carrier->id; + $range->save(); + $add_range = false; + } + } + if ($add_range) { + $range->id_carrier = (int) $carrier->id; + $range->delimiter1 = (float) $delimiter1; + $range->delimiter2 = (float) $range_sup[$key]; + $range->save(); + } + + if (!Validate::isLoadedObject($range)) { + return false; + } + $price_list = array(); + if (is_array($fees) && count($fees)) { + foreach ($fees as $id_zone => $fee) { + $price_list[] = array( + 'id_range_price' => ($range_type == Carrier::SHIPPING_METHOD_PRICE ? (int) $range->id : null), + 'id_range_weight' => ($range_type == Carrier::SHIPPING_METHOD_WEIGHT ? (int) $range->id : null), + 'id_carrier' => (int) $carrier->id, + 'id_zone' => (int) $id_zone, + 'price' => isset($fee[$key]) ? (float) str_replace(',', '.', $fee[$key]) : 0, + ); + } + } + + if (count($price_list) && !$carrier->addDeliveryPrice($price_list, true)) { + return false; + } + } + } + + return true; + } + + public function ajaxProcessUploadLogo() + { + if (!$this->access('edit')) { + die(''); + } + + $allowedExtensions = array('jpeg', 'gif', 'png', 'jpg'); + + $logo = (isset($_FILES['carrier_logo_input']) ? $_FILES['carrier_logo_input'] : false); + if ($logo && !empty($logo['tmp_name']) && $logo['tmp_name'] != 'none' + && (!isset($logo['error']) || !$logo['error']) + && preg_match('/\.(jpe?g|gif|png)$/', $logo['name']) + && is_uploaded_file($logo['tmp_name']) + && ImageManager::isRealImage($logo['tmp_name'], $logo['type'])) { + $file = $logo['tmp_name']; + do { + $tmp_name = uniqid() . '.jpg'; + } while (file_exists(_PS_TMP_IMG_DIR_ . $tmp_name)); + if (!ImageManager::resize($file, _PS_TMP_IMG_DIR_ . $tmp_name)) { + die(''); + } + @unlink($file); + die(''); + } else { + die(''); + } + } + + public function ajaxProcessFinishStep() + { + $return = array('has_error' => false); + if (!$this->access('edit')) { + $return = array( + 'has_error' => true, + $return['errors'][] = $this->trans('You do not have permission to use this wizard.', array(), 'Admin.Shipping.Notification'), + ); + } else { + $this->validateForm(false); + if ($id_carrier = Tools::getValue('id_carrier')) { + $current_carrier = new Carrier((int) $id_carrier); + + // if update we duplicate current Carrier + /** @var Carrier $new_carrier */ + $new_carrier = $current_carrier->duplicateObject(); + + if (Validate::isLoadedObject($new_carrier)) { + // Set flag deteled to true for historization + $current_carrier->deleted = true; + $current_carrier->update(); + + // Fill the new carrier object + $this->copyFromPost($new_carrier, $this->table); + $new_carrier->position = $current_carrier->position; + $new_carrier->update(); + + $this->updateAssoShop((int) $new_carrier->id); + $this->duplicateLogo((int) $new_carrier->id, (int) $current_carrier->id); + $this->changeGroups((int) $new_carrier->id); + + //Copy default carrier + if (Configuration::get('PS_CARRIER_DEFAULT') == $current_carrier->id) { + Configuration::updateValue('PS_CARRIER_DEFAULT', (int) $new_carrier->id); + } + + // Call of hooks + Hook::exec('actionCarrierUpdate', array( + 'id_carrier' => (int) $current_carrier->id, + 'carrier' => $new_carrier, + )); + $this->postImage($new_carrier->id); + $this->changeZones($new_carrier->id); + $new_carrier->setTaxRulesGroup((int) Tools::getValue('id_tax_rules_group')); + $carrier = $new_carrier; + } + } else { + $carrier = new Carrier(); + $this->copyFromPost($carrier, $this->table); + if (!$carrier->add()) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving this carrier.', array(), 'Admin.Shipping.Notification'); + } + } + + if ($carrier->is_free) { + //if carrier is free delete shipping cost + $carrier->deleteDeliveryPrice('range_weight'); + $carrier->deleteDeliveryPrice('range_price'); + } + + if (Validate::isLoadedObject($carrier)) { + if (!$this->changeGroups((int) $carrier->id)) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving carrier groups.', array(), 'Admin.Shipping.Notification'); + } + + if (!$this->changeZones((int) $carrier->id)) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving carrier zones.', array(), 'Admin.Shipping.Notification'); + } + + if (!$carrier->is_free) { + if (!$this->processRanges((int) $carrier->id)) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving carrier ranges.', array(), 'Admin.Shipping.Notification'); + } + } + + if (Shop::isFeatureActive() && !$this->updateAssoShop((int) $carrier->id)) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving associations of shops.', array(), 'Admin.Shipping.Notification'); + } + + if (!$carrier->setTaxRulesGroup((int) Tools::getValue('id_tax_rules_group'))) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving the tax rules group.', array(), 'Admin.Shipping.Notification'); + } + + if (Tools::getValue('logo')) { + if (Tools::getValue('logo') == 'null' && file_exists(_PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg')) { + unlink(_PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg'); + } else { + $logo = basename(Tools::getValue('logo')); + if (!file_exists(_PS_TMP_IMG_DIR_ . $logo) || !copy(_PS_TMP_IMG_DIR_ . $logo, _PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg')) { + $return['has_error'] = true; + $return['errors'][] = $this->trans('An error occurred while saving carrier logo.', array(), 'Admin.Shipping.Notification'); + } + } + } + $return['id_carrier'] = $carrier->id; + } + } + die(json_encode($return)); + } + + protected function changeGroups($id_carrier, $delete = true) + { + $carrier = new Carrier((int) $id_carrier); + if (!Validate::isLoadedObject($carrier)) { + return false; + } + + return $carrier->setGroups(Tools::getValue('groupBox')); + } + + public function changeZones($id) + { + $return = true; + $carrier = new Carrier($id); + if (!Validate::isLoadedObject($carrier)) { + die($this->trans('The object cannot be loaded.', array(), 'Admin.Notifications.Error')); + } + $zones = Zone::getZones(false); + foreach ($zones as $zone) { + if (count($carrier->getZone($zone['id_zone']))) { + if (!isset($_POST['zone_' . $zone['id_zone']]) || !$_POST['zone_' . $zone['id_zone']]) { + $return &= $carrier->deleteZone((int) $zone['id_zone']); + } + } elseif (isset($_POST['zone_' . $zone['id_zone']]) && $_POST['zone_' . $zone['id_zone']]) { + $return &= $carrier->addZone((int) $zone['id_zone']); + } + } + + return $return; + } + + public function getValidationRules() + { + $step_number = (int) Tools::getValue('step_number'); + if (!$step_number) { + return; + } + + if ($step_number == 4 && !Shop::isFeatureActive() || $step_number == 5 && Shop::isFeatureActive()) { + return array('fields' => array()); + } + + $step_fields = array( + 1 => array('name', 'delay', 'grade', 'url'), + 2 => array('is_free', 'id_tax_rules_group', 'shipping_handling', 'shipping_method', 'range_behavior'), + 3 => array('range_behavior', 'max_height', 'max_width', 'max_depth', 'max_weight'), + 4 => array(), + ); + if (Shop::isFeatureActive()) { + $tmp = $step_fields; + $step_fields = array_slice($tmp, 0, 1, true) + array(2 => array('shop')); + $step_fields[3] = $tmp[2]; + $step_fields[4] = $tmp[3]; + } + + $definition = ObjectModel::getDefinition('Carrier'); + foreach ($definition['fields'] as $field => $def) { + if (is_array($step_fields[$step_number]) && !in_array($field, $step_fields[$step_number])) { + unset($definition['fields'][$field]); + } + } + + return $definition; + } + + public static function displayFieldName($field) + { + return $field; + } + + public function duplicateLogo($new_id, $old_id) + { + $old_logo = _PS_SHIP_IMG_DIR_ . '/' . (int) $old_id . '.jpg'; + if (file_exists($old_logo)) { + copy($old_logo, _PS_SHIP_IMG_DIR_ . '/' . (int) $new_id . '.jpg'); + } + + $old_tmp_logo = _PS_TMP_IMG_DIR_ . '/carrier_mini_' . (int) $old_id . '.jpg'; + if (file_exists($old_tmp_logo)) { + if (!isset($_FILES['logo'])) { + copy($old_tmp_logo, _PS_TMP_IMG_DIR_ . '/carrier_mini_' . $new_id . '.jpg'); + } + unlink($old_tmp_logo); + } + } + + public function getActualCurrency() + { + if ($this->type_context == Shop::CONTEXT_SHOP) { + Shop::setContext($this->type_context, $this->old_context->shop->id); + } elseif ($this->type_context == Shop::CONTEXT_GROUP) { + Shop::setContext($this->type_context, $this->old_context->shop->id_shop_group); + } + + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + + Shop::setContext(Shop::CONTEXT_ALL); + + return $currency; + } +} diff --git a/controllers/admin/AdminCarriersController.php b/controllers/admin/AdminCarriersController.php new file mode 100644 index 00000000..c55f9d3e --- /dev/null +++ b/controllers/admin/AdminCarriersController.php @@ -0,0 +1,737 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Carrier $object + */ +class AdminCarriersControllerCore extends AdminController +{ + protected $position_identifier = 'id_carrier'; + + public function __construct() + { + if ($id_carrier = Tools::getValue('id_carrier') && !Tools::isSubmit('deletecarrier') && !Tools::isSubmit('statuscarrier') && !Tools::isSubmit('isFreecarrier')) { + Tools::redirectAdmin(Context::getContext()->link->getAdminLink('AdminCarrierWizard', true, array(), array('id_carrier' => (int) $id_carrier))); + } + + $this->bootstrap = true; + $this->table = 'carrier'; + $this->className = 'Carrier'; + $this->lang = false; + $this->deleted = true; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_defaultOrderBy = 'position'; + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + ), + ); + + $this->fieldImageSettings = array( + 'name' => 'logo', + 'dir' => 's', + ); + + $this->fields_list = array( + 'id_carrier' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + ), + 'image' => array( + 'title' => $this->trans('Logo', array(), 'Admin.Global'), + 'align' => 'center', + 'image' => 's', + 'class' => 'fixed-width-xs', + 'orderby' => false, + 'search' => false, + ), + 'delay' => array( + 'title' => $this->trans('Delay', array(), 'Admin.Shipping.Feature'), + 'orderby' => false, + ), + 'active' => array( + 'title' => $this->trans('Status', array(), 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'class' => 'fixed-width-sm', + 'orderby' => false, + ), + 'is_free' => array( + 'title' => $this->trans('Free Shipping', array(), 'Admin.Shipping.Feature'), + 'align' => 'center', + 'active' => 'isFree', + 'type' => 'bool', + 'class' => 'fixed-width-sm', + 'orderby' => false, + ), + 'position' => array( + 'title' => $this->trans('Position', array(), 'Admin.Global'), + 'filter_key' => 'a!position', + 'align' => 'center', + 'class' => 'fixed-width-sm', + 'position' => 'position', + ), + ); + } + + public function initToolbar() + { + parent::initToolbar(); + + if (isset($this->toolbar_btn['new']) && $this->display != 'view') { + $this->toolbar_btn['new']['href'] = $this->context->link->getAdminLink('AdminCarrierWizard'); + } + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_title = $this->trans('Carriers', array(), 'Admin.Shipping.Feature'); + if ($this->display != 'view') { + $this->page_header_toolbar_btn['new_carrier'] = array( + 'href' => $this->context->link->getAdminLink('AdminCarrierWizard'), + 'desc' => $this->trans('Add new carrier', array(), 'Admin.Shipping.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + $this->_select = 'b.*'; + $this->_join = 'INNER JOIN `' . _DB_PREFIX_ . 'carrier_lang` b ON a.id_carrier = b.id_carrier' . Shop::addSqlRestrictionOnLang('b') . ' AND b.id_lang = ' . (int) $this->context->language->id . ' LEFT JOIN `' . _DB_PREFIX_ . 'carrier_tax_rules_group_shop` ctrgs ON (a.`id_carrier` = ctrgs.`id_carrier` AND ctrgs.id_shop=' . (int) $this->context->shop->id . ')'; + $this->_use_found_rows = false; + + // Removes the Recommended modules button + unset($this->page_header_toolbar_btn['modules-list']); + + // test if need to show header alert. + $sql = 'SELECT COUNT(1) FROM `' . _DB_PREFIX_ . 'carrier` WHERE deleted = 0 AND id_reference > 2'; + $showHeaderAlert = (Db::getInstance()->executeS($sql, false)->fetchColumn(0) == 0); + + // Assign them in two steps! Because renderModulesList needs it before to be called. + $this->context->smarty->assign('panel_title', $this->trans('Use one of our recommended carrier modules', array(), 'Admin.Shipping.Feature')); + $this->context->smarty->assign('panel_id', 'recommended-carriers-panel'); + + $this->context->smarty->assign(array( + 'showHeaderAlert' => $showHeaderAlert, + 'modules_list' => $this->renderModulesList('back-office,AdminCarriers,new'), + )); + + return parent::renderList(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Carriers', array(), 'Admin.Shipping.Feature'), + 'icon' => 'icon-truck', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Company', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'hint' => array( + $this->trans('Allowed characters: letters, spaces and "%special_chars%".', array('%special_chars%' => '().-'), 'Admin.Shipping.Help'), + $this->trans('Carrier name displayed during checkout', array(), 'Admin.Shipping.Help'), + $this->trans('For in-store pickup, enter 0 to replace the carrier name with your shop name.', array(), 'Admin.Shipping.Help'), + ), + ), + array( + 'type' => 'file', + 'label' => $this->trans('Logo', array(), 'Admin.Global'), + 'name' => 'logo', + 'hint' => $this->trans('Upload a logo from your computer.', array(), 'Admin.Shipping.Help') . ' (.gif, .jpg, .jpeg ' . $this->trans('or', array(), 'Admin.Shipping.Help') . ' .png)', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Transit time', array(), 'Admin.Shipping.Feature'), + 'name' => 'delay', + 'lang' => true, + 'required' => true, + 'maxlength' => 512, + 'hint' => $this->trans('Estimated delivery time will be displayed during checkout.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Speed grade', array(), 'Admin.Shipping.Feature'), + 'name' => 'grade', + 'required' => false, + 'hint' => $this->trans('Enter "0" for a longest shipping delay, or "9" for the shortest shipping delay.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('URL', array(), 'Admin.Global'), + 'name' => 'url', + 'hint' => $this->trans('Delivery tracking URL: Type \'@\' where the tracking number should appear. It will then be automatically replaced by the tracking number.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'checkbox', + 'label' => $this->trans('Zone', array(), 'Admin.Global'), + 'name' => 'zone', + 'values' => array( + 'query' => Zone::getZones(false), + 'id' => 'id_zone', + 'name' => 'name', + ), + 'hint' => $this->trans('The zones in which this carrier will be used.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'group', + 'label' => $this->trans('Group access', array(), 'Admin.Shipping.Help'), + 'name' => 'groupBox', + 'values' => Group::getGroups(Context::getContext()->language->id), + 'hint' => $this->trans('Mark the groups that are allowed access to this carrier.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Status', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Enable the carrier in the front office.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Apply shipping cost', array(), 'Admin.Shipping.Feature'), + 'name' => 'is_free', + 'required' => false, + 'class' => 't', + 'values' => array( + array( + 'id' => 'is_free_on', + 'value' => 0, + 'label' => '' . $this->trans('Yes', array(), 'Admin.Global') . '', + ), + array( + 'id' => 'is_free_off', + 'value' => 1, + 'label' => '' . $this->trans('No', array(), 'Admin.Global') . '', + ), + ), + 'hint' => $this->trans('Apply both regular shipping cost and product-specific shipping costs.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'select', + 'label' => $this->trans('Tax', array(), 'Admin.Global'), + 'name' => 'id_tax_rules_group', + 'options' => array( + 'query' => TaxRulesGroup::getTaxRulesGroups(true), + 'id' => 'id_tax_rules_group', + 'name' => 'name', + 'default' => array( + 'label' => $this->trans('No Tax', array(), 'Admin.Global'), + 'value' => 0, + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Shipping and handling', array(), 'Admin.Shipping.Feature'), + 'name' => 'shipping_handling', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'shipping_handling_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'shipping_handling_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Include the shipping and handling costs in the carrier price.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'radio', + 'label' => $this->trans('Billing', array(), 'Admin.Shipping.Feature'), + 'name' => 'shipping_method', + 'required' => false, + 'class' => 't', + 'br' => true, + 'values' => array( + array( + 'id' => 'billing_default', + 'value' => Carrier::SHIPPING_METHOD_DEFAULT, + 'label' => $this->trans('Default behavior', array(), 'Admin.Shipping.Feature'), + ), + array( + 'id' => 'billing_price', + 'value' => Carrier::SHIPPING_METHOD_PRICE, + 'label' => $this->trans('According to total price', array(), 'Admin.Shipping.Feature'), + ), + array( + 'id' => 'billing_weight', + 'value' => Carrier::SHIPPING_METHOD_WEIGHT, + 'label' => $this->trans('According to total weight', array(), 'Admin.Shipping.Feature'), + ), + ), + ), + array( + 'type' => 'select', + 'label' => $this->trans('Out-of-range behavior', array(), 'Admin.Shipping.Feature'), + 'name' => 'range_behavior', + 'options' => array( + 'query' => array( + array( + 'id' => 0, + 'name' => $this->trans('Apply the cost of the highest defined range', array(), 'Admin.Shipping.Help'), + ), + array( + 'id' => 1, + 'name' => $this->trans('Disable carrier', array(), 'Admin.Shipping.Feature'), + ), + ), + 'id' => 'id', + 'name' => 'name', + ), + 'hint' => $this->trans('Out-of-range behavior occurs when none is defined (e.g. when a customer\'s cart weight is greater than the highest range limit).', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Maximum package height', array(), 'Admin.Shipping.Feature'), + 'name' => 'max_height', + 'required' => false, + 'hint' => $this->trans('Maximum height managed by this carrier. Set the value to "0," or leave this field blank to ignore.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Maximum package width', array(), 'Admin.Shipping.Feature'), + 'name' => 'max_width', + 'required' => false, + 'hint' => $this->trans('Maximum width managed by this carrier. Set the value to "0," or leave this field blank to ignore.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Maximum package depth', array(), 'Admin.Shipping.Feature'), + 'name' => 'max_depth', + 'required' => false, + 'hint' => $this->trans('Maximum depth managed by this carrier. Set the value to "0," or leave this field blank to ignore.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Maximum package weight', array(), 'Admin.Shipping.Feature'), + 'name' => 'max_weight', + 'required' => false, + 'hint' => $this->trans('Maximum weight managed by this carrier. Set the value to "0," or leave this field blank to ignore.', array(), 'Admin.Shipping.Help'), + ), + array( + 'type' => 'hidden', + 'name' => 'is_module', + ), + array( + 'type' => 'hidden', + 'name' => 'external_module_name', + ), + array( + 'type' => 'hidden', + 'name' => 'shipping_external', + ), + array( + 'type' => 'hidden', + 'name' => 'need_range', + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->getFieldsValues($obj); + + return parent::renderForm(); + } + + public function postProcess() + { + if (Tools::getValue('action') == 'GetModuleQuickView' && Tools::getValue('ajax') == '1') { + $this->ajaxProcessGetModuleQuickView(); + } + + if (Tools::getValue('submitAdd' . $this->table)) { + /* Checking fields validity */ + $this->validateRules(); + if (!count($this->errors)) { + $id = (int) Tools::getValue('id_' . $this->table); + + /* Object update */ + if (isset($id) && !empty($id)) { + try { + if ($this->access('edit')) { + $current_carrier = new Carrier($id); + if (!Validate::isLoadedObject($current_carrier)) { + throw new PrestaShopException('Cannot load Carrier object'); + } + + /** @var Carrier $new_carrier */ + // Duplicate current Carrier + $new_carrier = $current_carrier->duplicateObject(); + if (Validate::isLoadedObject($new_carrier)) { + // Set flag deteled to true for historization + $current_carrier->deleted = true; + $current_carrier->update(); + + // Fill the new carrier object + $this->copyFromPost($new_carrier, $this->table); + $new_carrier->position = $current_carrier->position; + $new_carrier->update(); + + $this->updateAssoShop($new_carrier->id); + $new_carrier->copyCarrierData((int) $current_carrier->id); + $this->changeGroups($new_carrier->id); + // Call of hooks + Hook::exec('actionCarrierUpdate', array( + 'id_carrier' => (int) $current_carrier->id, + 'carrier' => $new_carrier, + )); + $this->postImage($new_carrier->id); + $this->changeZones($new_carrier->id); + $new_carrier->setTaxRulesGroup((int) Tools::getValue('id_tax_rules_group')); + Tools::redirectAdmin(self::$currentIndex . '&id_' . $this->table . '=' . $current_carrier->id . '&conf=4&token=' . $this->token); + } else { + $this->errors[] = $this->trans('An error occurred while updating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ''; + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } catch (PrestaShopException $e) { + $this->errors[] = $e->getMessage(); + } + } else { + // Object creation + if ($this->access('add')) { + // Create new Carrier + $carrier = new Carrier(); + $this->copyFromPost($carrier, $this->table); + $carrier->position = Carrier::getHigherPosition() + 1; + if ($carrier->add()) { + if (($_POST['id_' . $this->table] = $carrier->id /* voluntary */) && $this->postImage($carrier->id) && $this->_redirect) { + $carrier->setTaxRulesGroup((int) Tools::getValue('id_tax_rules_group'), true); + $this->changeZones($carrier->id); + $this->changeGroups($carrier->id); + $this->updateAssoShop($carrier->id); + Tools::redirectAdmin(self::$currentIndex . '&id_' . $this->table . '=' . $carrier->id . '&conf=3&token=' . $this->token); + } + } else { + $this->errors[] = $this->trans('An error occurred while creating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ''; + } + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', array(), 'Admin.Notifications.Error'); + } + } + } + parent::postProcess(); + } elseif (isset($_GET['isFree' . $this->table])) { + $this->processIsFree(); + } else { + // if deletion : removes the carrier from the warehouse/carrier association + if (Tools::isSubmit('delete' . $this->table)) { + $id = (int) Tools::getValue('id_' . $this->table); + // Delete from the reference_id and not from the carrier id + $carrier = new Carrier((int) $id); + Warehouse::removeCarrier($carrier->id_reference); + } elseif (Tools::isSubmit($this->table . 'Box') && count(Tools::isSubmit($this->table . 'Box')) > 0) { + $ids = Tools::getValue($this->table . 'Box'); + array_walk($ids, 'intval'); + foreach ($ids as $id) { + // Delete from the reference_id and not from the carrier id + $carrier = new Carrier((int) $id); + Warehouse::removeCarrier($carrier->id_reference); + } + } + parent::postProcess(); + Carrier::cleanPositions(); + } + } + + public function processIsFree() + { + $carrier = new Carrier($this->id_object); + if (!Validate::isLoadedObject($carrier)) { + $this->errors[] = $this->trans('An error occurred while updating carrier information.', array(), 'Admin.Shipping.Notification'); + } + $carrier->is_free = $carrier->is_free ? 0 : 1; + if (!$carrier->update()) { + $this->errors[] = $this->trans('An error occurred while updating carrier information.', array(), 'Admin.Shipping.Notification'); + } + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token); + } + + /** + * Overload the property $fields_value. + * + * @param object $obj + */ + public function getFieldsValues($obj) + { + if ($this->getFieldValue($obj, 'is_module')) { + $this->fields_value['is_module'] = 1; + } + + if ($this->getFieldValue($obj, 'shipping_external')) { + $this->fields_value['shipping_external'] = 1; + } + + if ($this->getFieldValue($obj, 'need_range')) { + $this->fields_value['need_range'] = 1; + } + // Added values of object Zone + $carrier_zones = $obj->getZones(); + $carrier_zones_ids = array(); + if (is_array($carrier_zones)) { + foreach ($carrier_zones as $carrier_zone) { + $carrier_zones_ids[] = $carrier_zone['id_zone']; + } + } + + $zones = Zone::getZones(false); + foreach ($zones as $zone) { + $this->fields_value['zone_' . $zone['id_zone']] = Tools::getValue('zone_' . $zone['id_zone'], (in_array($zone['id_zone'], $carrier_zones_ids))); + } + + // Added values of object Group + $carrier_groups = $obj->getGroups(); + $carrier_groups_ids = array(); + if (is_array($carrier_groups)) { + foreach ($carrier_groups as $carrier_group) { + $carrier_groups_ids[] = $carrier_group['id_group']; + } + } + + $groups = Group::getGroups($this->context->language->id); + + foreach ($groups as $group) { + $this->fields_value['groupBox_' . $group['id_group']] = Tools::getValue('groupBox_' . $group['id_group'], (in_array($group['id_group'], $carrier_groups_ids) || empty($carrier_groups_ids) && !$obj->id)); + } + + $this->fields_value['id_tax_rules_group'] = $this->object->getIdTaxRulesGroup($this->context); + } + + /** + * @param Carrier $object + * + * @return int + */ + protected function beforeDelete($object) + { + return $object->isUsed(); + } + + protected function changeGroups($id_carrier, $delete = true) + { + if ($delete) { + Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'carrier_group WHERE id_carrier = ' . (int) $id_carrier); + } + $groups = Db::getInstance()->executeS('SELECT id_group FROM `' . _DB_PREFIX_ . 'group`'); + foreach ($groups as $group) { + if (Tools::getIsset('groupBox') && in_array($group['id_group'], Tools::getValue('groupBox'))) { + Db::getInstance()->execute(' + INSERT INTO ' . _DB_PREFIX_ . 'carrier_group (id_group, id_carrier) + VALUES(' . (int) $group['id_group'] . ',' . (int) $id_carrier . ') + '); + } + } + } + + public function changeZones($id) + { + /** @var Carrier $carrier */ + $carrier = new $this->className($id); + if (!Validate::isLoadedObject($carrier)) { + die($this->trans('The object cannot be loaded.', array(), 'Admin.Notifications.Error')); + } + $zones = Zone::getZones(false); + foreach ($zones as $zone) { + if (count($carrier->getZone($zone['id_zone']))) { + if (!isset($_POST['zone_' . $zone['id_zone']]) || !$_POST['zone_' . $zone['id_zone']]) { + $carrier->deleteZone($zone['id_zone']); + } + } elseif (isset($_POST['zone_' . $zone['id_zone']]) && $_POST['zone_' . $zone['id_zone']]) { + $carrier->addZone($zone['id_zone']); + } + } + } + + /** + * Modifying initial getList method to display position feature (drag and drop). + * + * @param int $id_lang + * @param string|null $order_by + * @param string|null $order_way + * @param int $start + * @param int|null $limit + * @param int|bool $id_lang_shop + * + * @throws PrestaShopException + */ + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + foreach ($this->_list as $key => $list) { + if ($list['name'] == '0') { + $this->_list[$key]['name'] = Carrier::getCarrierNameFromShopName(); + } + } + } + + public function ajaxProcessUpdatePositions() + { + $way = (int) (Tools::getValue('way')); + $id_carrier = (int) (Tools::getValue('id')); + $positions = Tools::getValue($this->table); + + foreach ($positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_carrier) { + if ($carrier = new Carrier((int) $pos[2])) { + if (isset($position) && $carrier->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for carrier ' . (int) $pos[1] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update carrier ' . (int) $id_carrier . ' to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "This carrier (' . (int) $id_carrier . ') can t be loaded"}'; + } + + break; + } + } + } + + public function displayEditLink($token, $id, $name = null) + { + if ($this->access('edit')) { + $tpl = $this->createTemplate('helpers/list/list_action_edit.tpl'); + if (!array_key_exists('Edit', self::$cache_lang)) { + self::$cache_lang['Edit'] = $this->trans('Edit', array(), 'Admin.Actions'); + } + + $tpl->assign(array( + 'href' => $this->context->link->getAdminLink('AdminCarrierWizard', true, array(), array('id_carrier' => (int) $id)), + 'action' => self::$cache_lang['Edit'], + 'id' => $id, + )); + + return $tpl->fetch(); + } else { + return; + } + } + + public function displayDeleteLink($token, $id, $name = null) + { + if ($this->access('delete')) { + $tpl = $this->createTemplate('helpers/list/list_action_delete.tpl'); + + if (!array_key_exists('Delete', self::$cache_lang)) { + self::$cache_lang['Delete'] = $this->trans('Delete', array(), 'Admin.Actions'); + } + + if (!array_key_exists('DeleteItem', self::$cache_lang)) { + self::$cache_lang['DeleteItem'] = $this->trans('Delete selected item?', array(), 'Admin.Notifications.Info'); + } + + if (!array_key_exists('Name', self::$cache_lang)) { + self::$cache_lang['Name'] = $this->trans('Name:', array(), 'Admin.Shipping.Feature'); + } + + if (!is_null($name)) { + $name = '\n\n' . self::$cache_lang['Name'] . ' ' . $name; + } + + $data = array( + $this->identifier => $id, + 'href' => $this->context->link->getAdminLink('AdminCarriers', true, array(), array('id_carrier' => (int) $id, 'deletecarrier' => 1)), + 'action' => self::$cache_lang['Delete'], + ); + + if ($this->specificConfirmDelete !== false) { + $data['confirm'] = !is_null($this->specificConfirmDelete) ? '\r' . $this->specificConfirmDelete : addcslashes(Tools::htmlentitiesDecodeUTF8(self::$cache_lang['DeleteItem'] . $name), '\''); + } + + $tpl->assign(array_merge($this->tpl_delete_link_vars, $data)); + + return $tpl->fetch(); + } else { + return; + } + } + + protected function initTabModuleList() + { + parent::initTabModuleList(); + $this->filter_modules_list = $this->tab_modules_list['default_list'] = $this->tab_modules_list['slider_list']; + } +} diff --git a/controllers/admin/AdminCartRulesController.php b/controllers/admin/AdminCartRulesController.php new file mode 100644 index 00000000..74712566 --- /dev/null +++ b/controllers/admin/AdminCartRulesController.php @@ -0,0 +1,724 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property CartRule $object + */ +class AdminCartRulesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'cart_rule'; + $this->className = 'CartRule'; + $this->lang = true; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->_orderWay = 'DESC'; + + parent::__construct(); + + $this->bulk_actions = array('delete' => array('text' => $this->trans('Delete selected', array(), 'Admin.Actions'), 'icon' => 'icon-trash', 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'))); + + $this->fields_list = array( + 'id_cart_rule' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global')), + 'priority' => array('title' => $this->trans('Priority', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'code' => array('title' => $this->trans('Code', array(), 'Admin.Global'), 'class' => 'fixed-width-sm'), + 'quantity' => array('title' => $this->trans('Quantity', array(), 'Admin.Catalog.Feature'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'date_to' => array('title' => $this->trans('Expiration date', array(), 'Admin.Catalog.Feature'), 'type' => 'datetime', 'class' => 'fixed-width-lg'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'active' => 'status', 'type' => 'bool', 'align' => 'center', 'class' => 'fixed-width-xs', 'orderby' => false), + ); + } + + public function ajaxProcessLoadCartRules() + { + $type = $token = $search = ''; + $limit = $count = $id_cart_rule = 0; + if (Tools::getIsset('limit')) { + $limit = Tools::getValue('limit'); + } + + if (Tools::getIsset('type')) { + $type = Tools::getValue('type'); + } + + if (Tools::getIsset('count')) { + $count = Tools::getValue('count'); + } + + if (Tools::getIsset('id_cart_rule')) { + $id_cart_rule = Tools::getValue('id_cart_rule'); + } + + if (Tools::getIsset('search')) { + $search = Tools::getValue('search'); + } + + $page = floor($count / $limit); + + $html = ''; + $next_link = ''; + + if (($page * $limit) + 1 == $count || $count == 0) { + if ($count == 0) { + $count = 1; + } + + /** @var CartRule $current_object */ + $current_object = $this->loadObject(true); + $cart_rules = $current_object->getAssociatedRestrictions('cart_rule', false, true, ($page) * $limit, $limit, $search); + + if ($type == 'selected') { + $i = 1; + foreach ($cart_rules['selected'] as $cart_rule) { + $html .= ''; + if ($i == $limit) { + break; + } + ++$i; + } + if ($i == $limit) { + $next_link = Context::getContext()->link->getAdminLink('AdminCartRules') . '&ajaxMode=1&ajax=1&id_cart_rule=' . (int) $id_cart_rule . '&action=loadCartRules&limit=' . (int) $limit . '&type=selected&count=' . ($count - 1 + count($cart_rules['selected']) . '&search=' . urlencode($search)); + } + } else { + $i = 1; + foreach ($cart_rules['unselected'] as $cart_rule) { + $html .= ''; + if ($i == $limit) { + break; + } + ++$i; + } + if ($i == $limit) { + $next_link = Context::getContext()->link->getAdminLink('AdminCartRules') . '&ajaxMode=1&ajax=1&id_cart_rule=' . (int) $id_cart_rule . '&action=loadCartRules&limit=' . (int) $limit . '&type=unselected&count=' . ($count - 1 + count($cart_rules['unselected']) . '&search=' . urlencode($search)); + } + } + } + echo json_encode(array('html' => $html, 'next_link' => $next_link)); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryPlugin(array('typewatch', 'fancybox', 'autocomplete')); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_cart_rule'] = array( + 'href' => self::$currentIndex . '&addcart_rule&token=' . $this->token, + 'desc' => $this->trans('Add new cart rule', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function postProcess() + { + if (Tools::isSubmit('submitAddcart_rule') || Tools::isSubmit('submitAddcart_ruleAndStay')) { + // If the reduction is associated to a specific product, then it must be part of the product restrictions + if ((int) Tools::getValue('reduction_product') && Tools::getValue('apply_discount_to') == 'specific' && Tools::getValue('apply_discount') != 'off') { + $reduction_product = (int) Tools::getValue('reduction_product'); + + // First, check if it is not already part of the restrictions + $already_restricted = false; + if (is_array($rule_group_array = Tools::getValue('product_rule_group')) && count($rule_group_array) && Tools::getValue('product_restriction')) { + foreach ($rule_group_array as $rule_group_id) { + if (is_array($rule_array = Tools::getValue('product_rule_' . $rule_group_id)) && count($rule_array)) { + foreach ($rule_array as $rule_id) { + if (Tools::getValue('product_rule_' . $rule_group_id . '_' . $rule_id . '_type') == 'products' + && in_array($reduction_product, Tools::getValue('product_rule_select_' . $rule_group_id . '_' . $rule_id))) { + $already_restricted = true; + break 2; + } + } + } + } + } + if ($already_restricted == false) { + // Check the product restriction + $_POST['product_restriction'] = 1; + + // Add a new rule group + $rule_group_id = 1; + if (is_array($rule_group_array)) { + // Empty for (with a ; at the end), that just find the first rule_group_id available in rule_group_array + for ($rule_group_id = 1; in_array($rule_group_id, $rule_group_array); ++$rule_group_id) { + 42; + } + $_POST['product_rule_group'][] = $rule_group_id; + } else { + $_POST['product_rule_group'] = array($rule_group_id); + } + + // Set a quantity of 1 for this new rule group + $_POST['product_rule_group_' . $rule_group_id . '_quantity'] = 1; + // Add one rule to the new rule group + $_POST['product_rule_' . $rule_group_id] = array(1); + // Set a type 'product' for this 1 rule + $_POST['product_rule_' . $rule_group_id . '_1_type'] = 'products'; + // Add the product in the selected products + $_POST['product_rule_select_' . $rule_group_id . '_1'] = array($reduction_product); + } + } + + // These are checkboxes (which aren't sent through POST when they are not check), so they are forced to 0 + foreach (array('country', 'carrier', 'group', 'cart_rule', 'product', 'shop') as $type) { + if (!Tools::getValue($type . '_restriction')) { + $_POST[$type . '_restriction'] = 0; + } + } + + // Remove the gift if the radio button is set to "no" + if (!(int) Tools::getValue('free_gift')) { + $_POST['gift_product'] = 0; + } + + // Retrieve the product attribute id of the gift (if available) + if ($id_product = (int) Tools::getValue('gift_product')) { + $_POST['gift_product_attribute'] = (int) Tools::getValue('ipa_' . $id_product); + } + + // Idiot-proof control + if (strtotime(Tools::getValue('date_from')) > strtotime(Tools::getValue('date_to'))) { + $this->errors[] = $this->trans('The voucher cannot end before it begins.', array(), 'Admin.Catalog.Notification'); + } + if ((int) Tools::getValue('minimum_amount') < 0) { + $this->errors[] = $this->trans('The minimum amount cannot be lower than zero.', array(), 'Admin.Catalog.Notification'); + } + if ((float) Tools::getValue('reduction_percent') < 0 || (float) Tools::getValue('reduction_percent') > 100) { + $this->errors[] = $this->trans('Reduction percentage must be between 0% and 100%', array(), 'Admin.Catalog.Notification'); + } + if ((int) Tools::getValue('reduction_amount') < 0) { + $this->errors[] = $this->trans('Reduction amount cannot be lower than zero.', array(), 'Admin.Catalog.Notification'); + } + if (Tools::getValue('code') && ($same_code = (int) CartRule::getIdByCode(Tools::getValue('code'))) && $same_code != Tools::getValue('id_cart_rule')) { + $this->errors[] = $this->trans('This cart rule code is already used (conflict with cart rule %rulename%)', array('%rulename%' => $same_code), 'Admin.Catalog.Notification'); + } + if (Tools::getValue('apply_discount') == 'off' && !Tools::getValue('free_shipping') && !Tools::getValue('free_gift')) { + $this->errors[] = $this->trans('An action is required for this cart rule.', array(), 'Admin.Catalog.Notification'); + } + } + + return parent::postProcess(); + } + + public function processDelete() + { + $res = parent::processDelete(); + if (Tools::isSubmit('delete' . $this->table)) { + $back = urldecode(Tools::getValue('back', '')); + if (!empty($back)) { + $this->redirect_after = $back; + } + } + + return $res; + } + + protected function afterUpdate($current_object) + { + // All the associations are deleted for an update, then recreated when we call the "afterAdd" method + $id_cart_rule = Tools::getValue('id_cart_rule'); + foreach (array('country', 'carrier', 'group', 'product_rule_group', 'shop') as $type) { + Db::getInstance()->delete('cart_rule_' . $type, '`id_cart_rule` = ' . (int) $id_cart_rule); + } + + Db::getInstance()->delete('cart_rule_product_rule', 'NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'cart_rule_product_rule_group` + WHERE `' . _DB_PREFIX_ . 'cart_rule_product_rule`.`id_product_rule_group` = `' . _DB_PREFIX_ . 'cart_rule_product_rule_group`.`id_product_rule_group`)'); + Db::getInstance()->delete('cart_rule_product_rule_value', 'NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'cart_rule_product_rule` + WHERE `' . _DB_PREFIX_ . 'cart_rule_product_rule_value`.`id_product_rule` = `' . _DB_PREFIX_ . 'cart_rule_product_rule`.`id_product_rule`)'); + Db::getInstance()->delete('cart_rule_combination', '`id_cart_rule_1` = ' . (int) $id_cart_rule . ' OR `id_cart_rule_2` = ' . (int) $id_cart_rule); + + $this->afterAdd($current_object); + } + + public function processAdd() + { + if ($cart_rule = parent::processAdd()) { + $this->context->smarty->assign('new_cart_rule', $cart_rule); + } + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + } + + return $cart_rule; + } + + /** + * @TODO Move this function into CartRule + * + * @param ObjectModel $currentObject + * + * @throws PrestaShopDatabaseException + */ + protected function afterAdd($currentObject) + { + // Add restrictions for generic entities like country, carrier and group + foreach (array('country', 'carrier', 'group', 'shop') as $type) { + if (Tools::getValue($type . '_restriction') && is_array($array = Tools::getValue($type . '_select')) && count($array)) { + $values = array(); + foreach ($array as $id) { + $values[] = '(' . (int) $currentObject->id . ',' . (int) $id . ')'; + } + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_' . $type . '` (`id_cart_rule`, `id_' . $type . '`) VALUES ' . implode(',', $values)); + } + } + // Add cart rule restrictions + if (Tools::getValue('cart_rule_restriction') && is_array($array = Tools::getValue('cart_rule_select')) && count($array)) { + $values = array(); + foreach ($array as $id) { + $values[] = '(' . (int) $currentObject->id . ',' . (int) $id . ')'; + } + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_combination` (`id_cart_rule_1`, `id_cart_rule_2`) VALUES ' . implode(',', $values)); + } + // Add product rule restrictions + if (Tools::getValue('product_restriction') && is_array($ruleGroupArray = Tools::getValue('product_rule_group')) && count($ruleGroupArray)) { + foreach ($ruleGroupArray as $ruleGroupId) { + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_product_rule_group` (`id_cart_rule`, `quantity`) + VALUES (' . (int) $currentObject->id . ', ' . (int) Tools::getValue('product_rule_group_' . $ruleGroupId . '_quantity') . ')'); + $id_product_rule_group = Db::getInstance()->Insert_ID(); + + if (is_array($ruleArray = Tools::getValue('product_rule_' . $ruleGroupId)) && count($ruleArray)) { + foreach ($ruleArray as $ruleId) { + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_product_rule` (`id_product_rule_group`, `type`) + VALUES (' . (int) $id_product_rule_group . ', "' . pSQL(Tools::getValue('product_rule_' . $ruleGroupId . '_' . $ruleId . '_type')) . '")'); + $id_product_rule = Db::getInstance()->Insert_ID(); + + $values = array(); + foreach (Tools::getValue('product_rule_select_' . $ruleGroupId . '_' . $ruleId) as $id) { + $values[] = '(' . (int) $id_product_rule . ',' . (int) $id . ')'; + } + $values = array_unique($values); + if (count($values)) { + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_product_rule_value` (`id_product_rule`, `id_item`) VALUES ' . implode(',', $values)); + } + } + } + } + } + + // If the new rule has no cart rule restriction, then it must be added to the white list of the other cart rules that have restrictions + if (!Tools::getValue('cart_rule_restriction')) { + Db::getInstance()->execute(' + INSERT INTO `' . _DB_PREFIX_ . 'cart_rule_combination` (`id_cart_rule_1`, `id_cart_rule_2`) ( + SELECT id_cart_rule, ' . (int) $currentObject->id . ' FROM `' . _DB_PREFIX_ . 'cart_rule` WHERE cart_rule_restriction = 1 + )'); + } else { + // And if the new cart rule has restrictions, previously unrestricted cart rules may now be restricted (a mug of coffee is strongly advised to understand this sentence) + $ruleCombinations = Db::getInstance()->executeS(' + SELECT cr.id_cart_rule + FROM ' . _DB_PREFIX_ . 'cart_rule cr + WHERE cr.id_cart_rule != ' . (int) $currentObject->id . ' + AND cr.cart_rule_restriction = 0 + AND NOT EXISTS ( + SELECT 1 + FROM ' . _DB_PREFIX_ . 'cart_rule_combination + WHERE cr.id_cart_rule = ' . _DB_PREFIX_ . 'cart_rule_combination.id_cart_rule_2 AND ' . (int) $currentObject->id . ' = id_cart_rule_1 + ) + AND NOT EXISTS ( + SELECT 1 + FROM ' . _DB_PREFIX_ . 'cart_rule_combination + WHERE cr.id_cart_rule = ' . _DB_PREFIX_ . 'cart_rule_combination.id_cart_rule_1 AND ' . (int) $currentObject->id . ' = id_cart_rule_2 + ) + '); + foreach ($ruleCombinations as $incompatibleRule) { + Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'cart_rule` SET cart_rule_restriction = 1 WHERE id_cart_rule = ' . (int) $incompatibleRule['id_cart_rule'] . ' LIMIT 1'); + Db::getInstance()->execute(' + INSERT IGNORE INTO `' . _DB_PREFIX_ . 'cart_rule_combination` (`id_cart_rule_1`, `id_cart_rule_2`) ( + SELECT id_cart_rule, ' . (int) $incompatibleRule['id_cart_rule'] . ' FROM `' . _DB_PREFIX_ . 'cart_rule` + WHERE active = 1 + AND id_cart_rule != ' . (int) $currentObject->id . ' + AND id_cart_rule != ' . (int) $incompatibleRule['id_cart_rule'] . ' + )'); + } + } + } + + /** + * Retrieve the cart rule product rule groups in the POST data + * if available, and in the database if there is none. + * + * @param CartRule $cart_rule + * + * @return array + */ + public function getProductRuleGroupsDisplay($cart_rule) + { + $productRuleGroupsArray = array(); + if (Tools::getValue('product_restriction') && is_array($array = Tools::getValue('product_rule_group')) && count($array)) { + $i = 1; + foreach ($array as $ruleGroupId) { + $productRulesArray = array(); + if (is_array($array = Tools::getValue('product_rule_' . $ruleGroupId)) && count($array)) { + foreach ($array as $ruleId) { + $productRulesArray[] = $this->getProductRuleDisplay( + $ruleGroupId, + $ruleId, + Tools::getValue('product_rule_' . $ruleGroupId . '_' . $ruleId . '_type'), + Tools::getValue('product_rule_select_' . $ruleGroupId . '_' . $ruleId) + ); + } + } + + $productRuleGroupsArray[] = $this->getProductRuleGroupDisplay( + $i++, + (int) Tools::getValue('product_rule_group_' . $ruleGroupId . '_quantity'), + $productRulesArray + ); + } + } else { + $i = 1; + foreach ($cart_rule->getProductRuleGroups() as $productRuleGroup) { + $j = 1; + $productRulesDisplay = array(); + foreach ($productRuleGroup['product_rules'] as $productRule) { + $productRulesDisplay[] = $this->getProductRuleDisplay($i, $j++, $productRule['type'], $productRule['values']); + } + $productRuleGroupsArray[] = $this->getProductRuleGroupDisplay($i++, $productRuleGroup['quantity'], $productRulesDisplay); + } + } + + return $productRuleGroupsArray; + } + + /* Return the form for a single cart rule group either with or without product_rules set up */ + public function getProductRuleGroupDisplay($product_rule_group_id, $product_rule_group_quantity = 1, $product_rules = null) + { + Context::getContext()->smarty->assign('product_rule_group_id', $product_rule_group_id); + Context::getContext()->smarty->assign('product_rule_group_quantity', $product_rule_group_quantity); + Context::getContext()->smarty->assign('product_rules', $product_rules); + + return $this->createTemplate('product_rule_group.tpl')->fetch(); + } + + public function getProductRuleDisplay($product_rule_group_id, $product_rule_id, $product_rule_type, $selected = array()) + { + Context::getContext()->smarty->assign( + array( + 'product_rule_group_id' => (int) $product_rule_group_id, + 'product_rule_id' => (int) $product_rule_id, + 'product_rule_type' => $product_rule_type, + ) + ); + + switch ($product_rule_type) { + case 'attributes': + $attributes = array('selected' => array(), 'unselected' => array()); + $results = Db::getInstance()->executeS(' + SELECT CONCAT(agl.name, " - ", al.name) as name, a.id_attribute as id + FROM ' . _DB_PREFIX_ . 'attribute_group_lang agl + LEFT JOIN ' . _DB_PREFIX_ . 'attribute a ON a.id_attribute_group = agl.id_attribute_group + LEFT JOIN ' . _DB_PREFIX_ . 'attribute_lang al ON (a.id_attribute = al.id_attribute AND al.id_lang = ' . (int) Context::getContext()->language->id . ') + WHERE agl.id_lang = ' . (int) Context::getContext()->language->id . ' + ORDER BY agl.name, al.name'); + foreach ($results as $row) { + $attributes[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $attributes); + $choose_content = $this->createTemplate('controllers/cart_rules/product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + break; + case 'products': + $products = array('selected' => array(), 'unselected' => array()); + $results = Db::getInstance()->executeS(' + SELECT DISTINCT name, p.id_product as id + FROM ' . _DB_PREFIX_ . 'product p + LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl + ON (p.`id_product` = pl.`id_product` + AND pl.`id_lang` = ' . (int) Context::getContext()->language->id . Shop::addSqlRestrictionOnLang('pl') . ') + ' . Shop::addSqlAssociation('product', 'p') . ' + WHERE id_lang = ' . (int) Context::getContext()->language->id . ' + ORDER BY name'); + foreach ($results as $row) { + $products[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $products); + $choose_content = $this->createTemplate('product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + break; + case 'manufacturers': + $products = array('selected' => array(), 'unselected' => array()); + $results = Db::getInstance()->executeS(' + SELECT name, id_manufacturer as id + FROM ' . _DB_PREFIX_ . 'manufacturer + ORDER BY name'); + foreach ($results as $row) { + $products[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $products); + $choose_content = $this->createTemplate('product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + break; + case 'suppliers': + $products = array('selected' => array(), 'unselected' => array()); + $results = Db::getInstance()->executeS(' + SELECT name, id_supplier as id + FROM ' . _DB_PREFIX_ . 'supplier + ORDER BY name'); + foreach ($results as $row) { + $products[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $products); + $choose_content = $this->createTemplate('product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + break; + case 'categories': + $categories = array('selected' => array(), 'unselected' => array()); + $results = Db::getInstance()->executeS(' + SELECT DISTINCT name, c.id_category as id + FROM ' . _DB_PREFIX_ . 'category c + LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl + ON (c.`id_category` = cl.`id_category` + AND cl.`id_lang` = ' . (int) Context::getContext()->language->id . Shop::addSqlRestrictionOnLang('cl') . ') + ' . Shop::addSqlAssociation('category', 'c') . ' + WHERE id_lang = ' . (int) Context::getContext()->language->id . ' + ORDER BY name'); + foreach ($results as $row) { + $categories[in_array($row['id'], $selected) ? 'selected' : 'unselected'][] = $row; + } + Context::getContext()->smarty->assign('product_rule_itemlist', $categories); + $choose_content = $this->createTemplate('product_rule_itemlist.tpl')->fetch(); + Context::getContext()->smarty->assign('product_rule_choose_content', $choose_content); + break; + default: + Context::getContext()->smarty->assign('product_rule_itemlist', array('selected' => array(), 'unselected' => array())); + Context::getContext()->smarty->assign('product_rule_choose_content', ''); + } + + return $this->createTemplate('product_rule.tpl')->fetch(); + } + + public function ajaxProcess() + { + if (Tools::isSubmit('newProductRule')) { + die($this->getProductRuleDisplay(Tools::getValue('product_rule_group_id'), Tools::getValue('product_rule_id'), Tools::getValue('product_rule_type'))); + } + if (Tools::isSubmit('newProductRuleGroup') && $product_rule_group_id = Tools::getValue('product_rule_group_id')) { + die($this->getProductRuleGroupDisplay($product_rule_group_id, Tools::getValue('product_rule_group_' . $product_rule_group_id . '_quantity', 1))); + } + + if (Tools::isSubmit('customerFilter')) { + $search_query = trim(Tools::getValue('q')); + $customers = Db::getInstance()->executeS(' + SELECT `id_customer`, `email`, CONCAT(`firstname`, \' \', `lastname`) as cname + FROM `' . _DB_PREFIX_ . 'customer` + WHERE `deleted` = 0 AND is_guest = 0 AND active = 1 + AND ( + `id_customer` = ' . (int) $search_query . ' + OR `email` LIKE "%' . pSQL($search_query) . '%" + OR `firstname` LIKE "%' . pSQL($search_query) . '%" + OR `lastname` LIKE "%' . pSQL($search_query) . '%" + ) + ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) . ' + ORDER BY `firstname`, `lastname` ASC + LIMIT 50'); + die(json_encode($customers)); + } + // Both product filter (free product and product discount) search for products + if (Tools::isSubmit('giftProductFilter') || Tools::isSubmit('reductionProductFilter')) { + $products = Product::searchByName(Context::getContext()->language->id, trim(Tools::getValue('q'))); + die(json_encode($products)); + } + } + + protected function searchProducts($search) + { + if ($products = Product::searchByName((int) $this->context->language->id, $search)) { + foreach ($products as &$product) { + $combinations = array(); + $productObj = new Product((int) $product['id_product'], false, (int) $this->context->language->id); + $attributes = $productObj->getAttributesGroups((int) $this->context->language->id); + $product['formatted_price'] = Tools::displayPrice(Tools::convertPrice($product['price_tax_incl'], $this->context->currency), $this->context->currency); + + foreach ($attributes as $attribute) { + if (!isset($combinations[$attribute['id_product_attribute']]['attributes'])) { + $combinations[$attribute['id_product_attribute']]['attributes'] = ''; + } + $combinations[$attribute['id_product_attribute']]['attributes'] .= $attribute['attribute_name'] . ' - '; + $combinations[$attribute['id_product_attribute']]['id_product_attribute'] = $attribute['id_product_attribute']; + $combinations[$attribute['id_product_attribute']]['default_on'] = $attribute['default_on']; + if (!isset($combinations[$attribute['id_product_attribute']]['price'])) { + $price_tax_incl = Product::getPriceStatic((int) $product['id_product'], true, $attribute['id_product_attribute']); + $combinations[$attribute['id_product_attribute']]['formatted_price'] = Tools::displayPrice(Tools::convertPrice($price_tax_incl, $this->context->currency), $this->context->currency); + } + } + + foreach ($combinations as &$combination) { + $combination['attributes'] = rtrim($combination['attributes'], ' - '); + } + $product['combinations'] = $combinations; + } + + return array( + 'products' => $products, + 'found' => true, + ); + } else { + return array('found' => false, 'notfound' => $this->trans('No product has been found.', array(), 'Admin.Catalog.Notification')); + } + } + + public function ajaxProcessSearchProducts() + { + $array = $this->searchProducts(Tools::getValue('product_search')); + $this->content = trim(json_encode($array)); + } + + public function renderForm() + { + $limit = 40; + $this->toolbar_btn['save-and-stay'] = array( + 'href' => '#', + 'desc' => $this->trans('Save and stay', array(), 'Admin.Actions'), + ); + + /** @var CartRule $current_object */ + $current_object = $this->loadObject(true); + + // All the filter are prefilled with the correct information + $customer_filter = ''; + if (Validate::isUnsignedId($current_object->id_customer) && + ($customer = new Customer($current_object->id_customer)) && + Validate::isLoadedObject($customer)) { + $customer_filter = $customer->firstname . ' ' . $customer->lastname . ' (' . $customer->email . ')'; + } + + $gift_product_filter = ''; + if (Validate::isUnsignedId($current_object->gift_product) && + ($product = new Product($current_object->gift_product, false, $this->context->language->id)) && + Validate::isLoadedObject($product)) { + $gift_product_filter = (!empty($product->reference) ? $product->reference : $product->name); + } + + $reduction_product_filter = ''; + if (Validate::isUnsignedId($current_object->reduction_product) && + ($product = new Product($current_object->reduction_product, false, $this->context->language->id)) && + Validate::isLoadedObject($product)) { + $reduction_product_filter = (!empty($product->reference) ? $product->reference : $product->name); + } + + $product_rule_groups = $this->getProductRuleGroupsDisplay($current_object); + + $attribute_groups = AttributeGroup::getAttributesGroups($this->context->language->id); + $currencies = Currency::getCurrencies(false, true, true); + $languages = Language::getLanguages(); + $countries = $current_object->getAssociatedRestrictions('country', true, true); + $groups = $current_object->getAssociatedRestrictions('group', false, true); + $shops = $current_object->getAssociatedRestrictions('shop', false, false); + $cart_rules = $current_object->getAssociatedRestrictions('cart_rule', false, true, 0, $limit); + $carriers = $current_object->getAssociatedRestrictions('carrier', true, false); + foreach ($carriers as &$carriers2) { + foreach ($carriers2 as &$carrier) { + foreach ($carrier as $field => &$value) { + if ($field == 'name' && $value == '0') { + $value = Configuration::get('PS_SHOP_NAME'); + } + } + } + } + + $gift_product_select = ''; + $gift_product_attribute_select = ''; + if ((int) $current_object->gift_product) { + $search_products = $this->searchProducts($gift_product_filter); + if (isset($search_products['products']) && is_array($search_products['products'])) { + foreach ($search_products['products'] as $product) { + $gift_product_select .= ' + '; + + if (count($product['combinations'])) { + $gift_product_attribute_select .= ''; + } + } + } + } + + $product = new Product($current_object->gift_product); + $this->context->smarty->assign( + array( + 'show_toolbar' => true, + 'toolbar_btn' => $this->toolbar_btn, + 'toolbar_scroll' => $this->toolbar_scroll, + 'title' => array($this->trans('Payment: ', array(), 'Admin.Catalog.Feature'), $this->trans('Cart Rules', array(), 'Admin.Catalog.Feature')), + 'defaultDateFrom' => date('Y-m-d H:00:00'), + 'defaultDateTo' => date('Y-m-d H:00:00', strtotime('+1 month')), + 'customerFilter' => $customer_filter, + 'giftProductFilter' => $gift_product_filter, + 'gift_product_select' => $gift_product_select, + 'gift_product_attribute_select' => $gift_product_attribute_select, + 'reductionProductFilter' => $reduction_product_filter, + 'defaultCurrency' => Configuration::get('PS_CURRENCY_DEFAULT'), + 'id_lang_default' => Configuration::get('PS_LANG_DEFAULT'), + 'languages' => $languages, + 'currencies' => $currencies, + 'countries' => $countries, + 'carriers' => $carriers, + 'groups' => $groups, + 'shops' => $shops, + 'cart_rules' => $cart_rules, + 'product_rule_groups' => $product_rule_groups, + 'product_rule_groups_counter' => count($product_rule_groups), + 'attribute_groups' => $attribute_groups, + 'currentIndex' => self::$currentIndex, + 'currentToken' => $this->token, + 'currentObject' => $current_object, + 'currentTab' => $this, + 'hasAttribute' => $product->hasAttributes(), + ) + ); + Media::addJsDef(array('baseHref' => $this->context->link->getAdminLink('AdminCartRules') . '&ajaxMode=1&ajax=1&id_cart_rule=' . + (int) Tools::getValue('id_cart_rule') . '&action=loadCartRules&limit=' . (int) $limit . '&count=0', )); + $this->content .= $this->createTemplate('form.tpl')->fetch(); + + $this->addJqueryUI('ui.datepicker'); + $this->addJqueryPlugin(array('jscroll', 'typewatch')); + + return parent::renderForm(); + } + + public function displayAjaxSearchCartRuleVouchers() + { + $found = false; + if ($vouchers = CartRule::getCartsRuleByCode(Tools::getValue('q'), (int) $this->context->language->id, true)) { + $found = true; + } + echo json_encode(array('found' => $found, 'vouchers' => $vouchers)); + } +} diff --git a/controllers/admin/AdminCartsController.php b/controllers/admin/AdminCartsController.php new file mode 100644 index 00000000..e6627594 --- /dev/null +++ b/controllers/admin/AdminCartsController.php @@ -0,0 +1,937 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Cart $object + */ +class AdminCartsControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'cart'; + $this->className = 'Cart'; + $this->lang = false; + $this->explicitSelect = true; + + parent::__construct(); + + $this->addRowAction('view'); + $this->addRowAction('delete'); + $this->allow_export = true; + $this->_orderWay = 'DESC'; + + $this->_select = 'CONCAT(LEFT(c.`firstname`, 1), \'. \', c.`lastname`) `customer`, a.id_cart total, ca.name carrier, o.id_order, + IF (IFNULL(o.id_order, \'' . $this->trans('Non ordered', array(), 'Admin.Orderscustomers.Feature') . '\') = \'' . $this->trans('Non ordered', array(), 'Admin.Orderscustomers.Feature') . '\', IF(TIME_TO_SEC(TIMEDIFF(\'' . pSQL(date('Y-m-d H:i:00', time())) . '\', a.`date_add`)) > 86400, \'' . $this->trans('Abandoned cart', array(), 'Admin.Orderscustomers.Feature') . '\', \'' . $this->trans('Non ordered', array(), 'Admin.Orderscustomers.Feature') . '\'), o.id_order) AS status, IF(o.id_order, 1, 0) badge_success, IF(o.id_order, 0, 1) badge_danger, IF(co.id_guest, 1, 0) id_guest'; + $this->_join = 'LEFT JOIN ' . _DB_PREFIX_ . 'customer c ON (c.id_customer = a.id_customer) + LEFT JOIN ' . _DB_PREFIX_ . 'currency cu ON (cu.id_currency = a.id_currency) + LEFT JOIN ' . _DB_PREFIX_ . 'carrier ca ON (ca.id_carrier = a.id_carrier) + LEFT JOIN ' . _DB_PREFIX_ . 'orders o ON (o.id_cart = a.id_cart) + LEFT JOIN ( + SELECT `id_guest` + FROM `' . _DB_PREFIX_ . 'connections` + WHERE + TIME_TO_SEC(TIMEDIFF(\'' . pSQL(date('Y-m-d H:i:00', time())) . '\', `date_add`)) < 1800 + LIMIT 1 + ) AS co ON co.`id_guest` = a.`id_guest`'; + + if (Tools::getValue('action') && Tools::getValue('action') == 'filterOnlyAbandonedCarts') { + $this->_having = 'status = \'' . $this->trans('Abandoned cart', array(), 'Admin.Orderscustomers.Feature') . '\''; + } else { + $this->_use_found_rows = false; + } + + $this->fields_list = array( + 'id_cart' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + ), + 'status' => array( + 'title' => $this->trans('Order ID', array(), 'Admin.Orderscustomers.Feature'), + 'align' => 'text-center', + 'badge_danger' => true, + 'havingFilter' => true, + ), + 'customer' => array( + 'title' => $this->trans('Customer', array(), 'Admin.Global'), + 'filter_key' => 'c!lastname', + ), + 'total' => array( + 'title' => $this->trans('Total', array(), 'Admin.Global'), + 'callback' => 'getOrderTotalUsingTaxCalculationMethod', + 'orderby' => false, + 'search' => false, + 'align' => 'text-right', + 'badge_success' => true, + ), + 'carrier' => array( + 'title' => $this->trans('Carrier', array(), 'Admin.Shipping.Feature'), + 'align' => 'text-left', + 'callback' => 'replaceZeroByShopName', + 'filter_key' => 'ca!name', + ), + 'date_add' => array( + 'title' => $this->trans('Date', array(), 'Admin.Global'), + 'align' => 'text-left', + 'type' => 'datetime', + 'class' => 'fixed-width-lg', + 'filter_key' => 'a!date_add', + ), + ); + + if (Configuration::get('PS_GUEST_CHECKOUT_ENABLED')) { + $this->fields_list['id_guest'] = array( + 'title' => $this->trans('Online', array(), 'Admin.Global'), + 'align' => 'text-center', + 'type' => 'bool', + 'havingFilter' => true, + 'class' => 'fixed-width-xs', + ); + } + + $this->shopLinkType = 'shop'; + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['export_cart'] = array( + 'href' => self::$currentIndex . '&exportcart&token=' . $this->token, + 'desc' => $this->trans('Export carts', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'process-icon-export', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderKpis() + { + $time = time(); + $kpis = array(); + + /* The data generation is located in AdminStatsControllerCore */ + $helper = new HelperKpi(); + $helper->id = 'box-conversion-rate'; + $helper->icon = 'icon-sort-by-attributes-alt'; + //$helper->chart = true; + $helper->color = 'color1'; + $helper->title = $this->trans('Conversion Rate', array(), 'Admin.Global'); + $helper->subtitle = $this->trans('30 days', array(), 'Admin.Global'); + if (ConfigurationKPI::get('CONVERSION_RATE') !== false) { + $helper->value = ConfigurationKPI::get('CONVERSION_RATE'); + } + if (ConfigurationKPI::get('CONVERSION_RATE_CHART') !== false) { + $helper->data = ConfigurationKPI::get('CONVERSION_RATE_CHART'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=conversion_rate'; + $helper->refresh = (bool) (ConfigurationKPI::get('CONVERSION_RATE_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-carts'; + $helper->icon = 'icon-shopping-cart'; + $helper->color = 'color2'; + $helper->title = $this->trans('Abandoned Carts', array(), 'Admin.Orderscustomers.Feature'); + $date_from = date(Context::getContext()->language->date_format_lite, strtotime('-2 day')); + $date_to = date(Context::getContext()->language->date_format_lite, strtotime('-1 day')); + $helper->subtitle = $this->trans('From %date1% to %date2%', array('%date1%' => $date_from, '%date2%' => $date_to), 'Admin.Orderscustomers.Feature'); + $helper->href = $this->context->link->getAdminLink('AdminCarts') . '&action=filterOnlyAbandonedCarts'; + if (ConfigurationKPI::get('ABANDONED_CARTS') !== false) { + $helper->value = ConfigurationKPI::get('ABANDONED_CARTS'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=abandoned_cart'; + $helper->refresh = (bool) (ConfigurationKPI::get('ABANDONED_CARTS_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-average-order'; + $helper->icon = 'icon-money'; + $helper->color = 'color3'; + $helper->title = $this->trans('Average Order Value', array(), 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('30 days', array(), 'Admin.Global'); + if (ConfigurationKPI::get('AVG_ORDER_VALUE') !== false) { + $helper->value = $this->trans('%amount% tax excl.', array('%amount%' => ConfigurationKPI::get('AVG_ORDER_VALUE')), 'Admin.Orderscustomers.Feature'); + } + if (ConfigurationKPI::get('AVG_ORDER_VALUE_EXPIRE') < $time) { + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=average_order_value'; + } + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-net-profit-visitor'; + $helper->icon = 'icon-user'; + $helper->color = 'color4'; + $helper->title = $this->trans('Net Profit per Visitor', array(), 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('30 days', array(), 'Admin.Global'); + if (ConfigurationKPI::get('NETPROFIT_VISITOR') !== false) { + $helper->value = ConfigurationKPI::get('NETPROFIT_VISITOR'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=netprofit_visitor'; + $helper->refresh = (bool) (ConfigurationKPI::get('NETPROFIT_VISITOR_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpiRow(); + $helper->kpis = $kpis; + + return $helper->generate(); + } + + public function renderView() + { + /** @var Cart $cart */ + if (!($cart = $this->loadObject(true))) { + return; + } + $customer = new Customer($cart->id_customer); + $currency = new Currency($cart->id_currency); + $this->context->cart = $cart; + $this->context->currency = $currency; + $this->context->customer = $customer; + $this->toolbar_title = $this->trans('Cart #%ID%', array('%ID%' => $this->context->cart->id), 'Admin.Orderscustomers.Feature'); + $products = $cart->getProducts(); + $summary = $cart->getSummaryDetails(); + + /* Display order information */ + $id_order = (int) Order::getIdByCartId($cart->id); + $order = new Order($id_order); + if (Validate::isLoadedObject($order)) { + $tax_calculation_method = $order->getTaxCalculationMethod(); + $id_shop = (int) $order->id_shop; + } else { + $id_shop = (int) $cart->id_shop; + $tax_calculation_method = Group::getPriceDisplayMethod(Group::getCurrent()->id); + } + + if ($tax_calculation_method == PS_TAX_EXC) { + $total_products = $summary['total_products']; + $total_discounts = $summary['total_discounts_tax_exc']; + $total_wrapping = $summary['total_wrapping_tax_exc']; + $total_price = $summary['total_price_without_tax']; + $total_shipping = $summary['total_shipping_tax_exc']; + } else { + $total_products = $summary['total_products_wt']; + $total_discounts = $summary['total_discounts']; + $total_wrapping = $summary['total_wrapping']; + $total_price = $summary['total_price']; + $total_shipping = $summary['total_shipping']; + } + foreach ($products as &$product) { + if ($tax_calculation_method == PS_TAX_EXC) { + $product['product_price'] = $product['price']; + $product['product_total'] = $product['total']; + } else { + $product['product_price'] = $product['price_wt']; + $product['product_total'] = $product['total_wt']; + } + $image = array(); + if (isset($product['id_product_attribute']) && (int) $product['id_product_attribute']) { + $image = Db::getInstance()->getRow('SELECT id_image FROM ' . _DB_PREFIX_ . 'product_attribute_image WHERE id_product_attribute = ' . (int) $product['id_product_attribute']); + } + if (!isset($image['id_image'])) { + $image = Db::getInstance()->getRow('SELECT id_image FROM ' . _DB_PREFIX_ . 'image WHERE id_product = ' . (int) $product['id_product'] . ' AND cover = 1'); + } + + $product['qty_in_stock'] = StockAvailable::getQuantityAvailableByProduct($product['id_product'], isset($product['id_product_attribute']) ? $product['id_product_attribute'] : null, (int) $id_shop); + + $image_product = new Image($image['id_image']); + $product['image'] = (isset($image['id_image']) ? ImageManager::thumbnail(_PS_IMG_DIR_ . 'p/' . $image_product->getExistingImgPath() . '.jpg', 'product_mini_' . (int) $product['id_product'] . (isset($product['id_product_attribute']) ? '_' . (int) $product['id_product_attribute'] : '') . '.jpg', 45, 'jpg') : '--'); + + $customized_datas = Product::getAllCustomizedDatas($this->context->cart->id, null, true, null, (int) $product['id_customization']); + $this->context->cart->setProductCustomizedDatas($product, $customized_datas); + if ($customized_datas) { + Product::addProductCustomizationPrice($product, $customized_datas); + } + } + + $helper = new HelperKpi(); + $helper->id = 'box-kpi-cart'; + $helper->icon = 'icon-shopping-cart'; + $helper->color = 'color1'; + $helper->title = $this->trans('Total Cart', array(), 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('Cart #%ID%', array('%ID%' => $cart->id), 'Admin.Orderscustomers.Feature'); + $helper->value = Tools::displayPrice($total_price, $currency); + $kpi = $helper->generate(); + + $this->tpl_view_vars = array( + 'kpi' => $kpi, + 'products' => $products, + 'discounts' => $cart->getCartRules(), + 'order' => $order, + 'cart' => $cart, + 'currency' => $currency, + 'customer' => $customer, + 'customer_stats' => $customer->getStats(), + 'total_products' => $total_products, + 'total_discounts' => $total_discounts, + 'total_wrapping' => $total_wrapping, + 'total_price' => $total_price, + 'total_shipping' => $total_shipping, + 'tax_calculation_method' => $tax_calculation_method, + ); + + return parent::renderView(); + } + + public function ajaxPreProcess() + { + if ($this->access('edit')) { + $id_customer = (int) Tools::getValue('id_customer'); + $customer = new Customer((int) $id_customer); + $this->context->customer = $customer; + $id_cart = (int) Tools::getValue('id_cart'); + if (!$id_cart) { + $id_cart = $customer->getLastEmptyCart(false); + } + $this->context->cart = new Cart((int) $id_cart); + + if (!$this->context->cart->id) { + $this->context->cart->recyclable = 0; + $this->context->cart->gift = 0; + } + + if (!$this->context->cart->id_customer) { + $this->context->cart->id_customer = $id_customer; + } + if (Validate::isLoadedObject($this->context->cart) && $this->context->cart->OrderExists()) { + return; + } + if (!$this->context->cart->secure_key) { + $this->context->cart->secure_key = $this->context->customer->secure_key; + } + if (!$this->context->cart->id_shop) { + $this->context->cart->id_shop = (int) $this->context->shop->id; + } + if (!$this->context->cart->id_lang) { + $this->context->cart->id_lang = (($id_lang = (int) Tools::getValue('id_lang')) ? $id_lang : Configuration::get('PS_LANG_DEFAULT')); + } + if (!$this->context->cart->id_currency) { + $this->context->cart->id_currency = (($id_currency = (int) Tools::getValue('id_currency')) ? $id_currency : Configuration::get('PS_CURRENCY_DEFAULT')); + } + + $addresses = $customer->getAddresses((int) $this->context->cart->id_lang); + $id_address_delivery = (int) Tools::getValue('id_address_delivery'); + $id_address_invoice = (int) Tools::getValue('id_address_delivery'); + + if (!$this->context->cart->id_address_invoice && isset($addresses[0])) { + $this->context->cart->id_address_invoice = (int) $addresses[0]['id_address']; + } elseif ($id_address_invoice) { + $this->context->cart->id_address_invoice = (int) $id_address_invoice; + } + if (!$this->context->cart->id_address_delivery && isset($addresses[0])) { + $this->context->cart->id_address_delivery = $addresses[0]['id_address']; + } elseif ($id_address_delivery) { + $this->context->cart->id_address_delivery = (int) $id_address_delivery; + } + $this->context->cart->setNoMultishipping(); + $this->context->cart->save(); + $currency = new Currency((int) $this->context->cart->id_currency); + $this->context->currency = $currency; + } + } + + public function ajaxProcessDeleteProduct() + { + if ($this->access('edit')) { + $errors = array(); + if ((!$id_product = (int) Tools::getValue('id_product')) || !Validate::isInt($id_product)) { + $errors[] = $this->trans('Invalid product', array(), 'Admin.Catalog.Notification'); + } + if (($id_product_attribute = (int) Tools::getValue('id_product_attribute')) && !Validate::isInt($id_product_attribute)) { + $errors[] = $this->trans('Invalid combination', array(), 'Admin.Catalog.Notification'); + } + if (count($errors)) { + die(json_encode($errors)); + } + if ($this->context->cart->deleteProduct($id_product, $id_product_attribute, (int) Tools::getValue('id_customization'))) { + echo json_encode($this->ajaxReturnVars()); + } + } + } + + public function ajaxProcessUpdateCustomizationFields() + { + $errors = array(); + if ($this->access('edit')) { + $errors = array(); + if (Tools::getValue('only_display') != 1) { + if (!$this->context->cart->id || (!$id_product = (int) Tools::getValue('id_product'))) { + return; + } + $product = new Product((int) $id_product); + if (!$customization_fields = $product->getCustomizationFieldIds()) { + return; + } + foreach ($customization_fields as $customization_field) { + $field_id = 'customization_' . $id_product . '_' . $customization_field['id_customization_field']; + if ($customization_field['type'] == Product::CUSTOMIZE_TEXTFIELD) { + if (!Tools::getValue($field_id)) { + if ($customization_field['required']) { + $errors[] = $this->trans('Please fill in all the required fields.', array(), 'Admin.Notifications.Error'); + } + continue; + } + if (!Validate::isMessage(Tools::getValue($field_id))) { + $errors[] = $this->trans('Invalid message', array(), 'Admin.Notifications.Error'); + } + $this->context->cart->addTextFieldToProduct((int) $product->id, (int) $customization_field['id_customization_field'], Product::CUSTOMIZE_TEXTFIELD, Tools::getValue($field_id)); + } elseif ($customization_field['type'] == Product::CUSTOMIZE_FILE) { + if (!isset($_FILES[$field_id]) || !isset($_FILES[$field_id]['tmp_name']) || empty($_FILES[$field_id]['tmp_name'])) { + if ($customization_field['required']) { + $errors[] = $this->trans('Please fill in all the required fields.', array(), 'Admin.Notifications.Error'); + } + continue; + } + if ($error = ImageManager::validateUpload($_FILES[$field_id], (int) Configuration::get('PS_PRODUCT_PICTURE_MAX_SIZE'))) { + $errors[] = $error; + } + if (!($tmp_name = tempnam(_PS_TMP_IMG_DIR_, 'PS')) || !move_uploaded_file($_FILES[$field_id]['tmp_name'], $tmp_name)) { + $errors[] = $this->trans('An error occurred during the image upload process.', array(), 'Admin.Catalog.Notification'); + } + $file_name = md5(uniqid(rand(), true)); + if (!ImageManager::resize($tmp_name, _PS_UPLOAD_DIR_ . $file_name)) { + continue; + } elseif (!ImageManager::resize($tmp_name, _PS_UPLOAD_DIR_ . $file_name . '_small', (int) Configuration::get('PS_PRODUCT_PICTURE_WIDTH'), (int) Configuration::get('PS_PRODUCT_PICTURE_HEIGHT'))) { + $errors[] = $this->trans('An error occurred during the image upload process.', array(), 'Admin.Catalog.Notification'); + } elseif (!chmod(_PS_UPLOAD_DIR_ . $file_name, 0777) || !chmod(_PS_UPLOAD_DIR_ . $file_name . '_small', 0777)) { + $errors[] = $this->trans('An error occurred during the image upload process.', array(), 'Admin.Catalog.Notification'); + } else { + $this->context->cart->addPictureToProduct((int) $product->id, (int) $customization_field['id_customization_field'], Product::CUSTOMIZE_FILE, $file_name); + } + unlink($tmp_name); + } + } + } + $this->setMedia(false); + $this->initFooter(); + $this->context->smarty->assign(array('customization_errors' => implode('
', $errors), + 'css_files' => $this->css_files, )); + + return $this->smartyOutputContent('controllers/orders/form_customization_feedback.tpl'); + } + } + + public function ajaxProcessUpdateQty() + { + if ($this->access('edit')) { + $errors = array(); + if (!$this->context->cart->id) { + return; + } + if ($this->context->cart->OrderExists()) { + $errors[] = $this->trans('An order has already been placed with this cart.', array(), 'Admin.Catalog.Notification'); + } elseif (!($id_product = (int) Tools::getValue('id_product')) || !($product = new Product((int) $id_product, true, $this->context->language->id))) { + $errors[] = $this->trans('Invalid product', array(), 'Admin.Catalog.Notification'); + } elseif (!($qty = Tools::getValue('qty')) || $qty == 0) { + $errors[] = $this->trans('Invalid quantity', array(), 'Admin.Catalog.Notification'); + } + + // Don't try to use a product if not instanciated before due to errors + if (isset($product) && $product->id) { + if (($id_product_attribute = Tools::getValue('id_product_attribute')) != 0) { + if (!Product::isAvailableWhenOutOfStock($product->out_of_stock) && !Attribute::checkAttributeQty((int) $id_product_attribute, (int) $qty)) { + $errors[] = $this->trans('There are not enough products in stock.', array(), 'Admin.Catalog.Notification'); + } + } elseif (!$product->checkQty((int) $qty)) { + $errors[] = $this->trans('There are not enough products in stock.', array(), 'Admin.Catalog.Notification'); + } + if (!($id_customization = (int) Tools::getValue('id_customization', 0)) && !$product->hasAllRequiredCustomizableFields()) { + $errors[] = $this->trans('Please fill in all the required fields.', array(), 'Admin.Notifications.Error'); + } + $this->context->cart->save(); + } else { + $errors[] = $this->trans('This product cannot be added to the cart.', array(), 'Admin.Catalog.Notification'); + } + + if (!count($errors)) { + if ((int) $qty < 0) { + $qty = str_replace('-', '', $qty); + $operator = 'down'; + } else { + $operator = 'up'; + } + + if (!($qty_upd = $this->context->cart->updateQty($qty, $id_product, (int) $id_product_attribute, (int) $id_customization, $operator))) { + $errors[] = $this->trans('You already have the maximum quantity available for this product.', array(), 'Admin.Catalog.Notification'); + } elseif ($qty_upd < 0) { + $minimal_qty = $id_product_attribute ? Attribute::getAttributeMinimalQty((int) $id_product_attribute) : $product->minimal_quantity; + $errors[] = $this->trans('You must add a minimum quantity of %d', array($minimal_qty), 'Admin.Orderscustomers.Notification'); + } + } + + echo json_encode(array_merge($this->ajaxReturnVars(), array('errors' => $errors))); + } + } + + public function ajaxProcessUpdateDeliveryOption() + { + if ($this->access('edit')) { + $delivery_option = Tools::getValue('delivery_option'); + if ($delivery_option !== false) { + $this->context->cart->setDeliveryOption(array($this->context->cart->id_address_delivery => $delivery_option)); + } + if (Validate::isBool(($recyclable = (int) Tools::getValue('recyclable')))) { + $this->context->cart->recyclable = $recyclable; + } + if (Validate::isBool(($gift = (int) Tools::getValue('gift')))) { + $this->context->cart->gift = $gift; + } + if (Validate::isMessage(($gift_message = pSQL(Tools::getValue('gift_message'))))) { + $this->context->cart->gift_message = $gift_message; + } + $this->context->cart->save(); + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessUpdateOrderMessage() + { + if ($this->access('edit')) { + $id_message = false; + if ($old_message = Message::getMessageByCartId((int) $this->context->cart->id)) { + $id_message = $old_message['id_message']; + } + $message = new Message((int) $id_message); + if ($message_content = Tools::getValue('message')) { + if (Validate::isMessage($message_content)) { + $message->message = $message_content; + $message->id_cart = (int) $this->context->cart->id; + $message->id_customer = (int) $this->context->cart->id_customer; + $message->save(); + } + } elseif (Validate::isLoadedObject($message)) { + $message->delete(); + } + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessUpdateCurrency() + { + if ($this->access('edit')) { + $currency = new Currency((int) Tools::getValue('id_currency')); + if (Validate::isLoadedObject($currency) && !$currency->deleted && $currency->active) { + $this->context->cart->id_currency = (int) $currency->id; + $this->context->currency = $currency; + $this->context->cart->save(); + } + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessUpdateLang() + { + if ($this->access('edit')) { + $lang = new Language((int) Tools::getValue('id_lang')); + if (Validate::isLoadedObject($lang) && $lang->active) { + $this->context->cart->id_lang = (int) $lang->id; + $this->context->cart->save(); + } + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessDuplicateOrder() + { + if ($this->access('edit')) { + $errors = array(); + if (!$id_order = Tools::getValue('id_order')) { + $errors[] = $this->trans('Invalid order', array(), 'Admin.Orderscustomers.Notification'); + } + $cart = Cart::getCartByOrderId($id_order); + $new_cart = $cart->duplicate(); + if (!$new_cart || !Validate::isLoadedObject($new_cart['cart'])) { + $errors[] = $this->trans('The order cannot be renewed.', array(), 'Admin.Orderscustomers.Notification'); + } elseif (!$new_cart['success']) { + $errors[] = $this->trans('The order cannot be renewed.', array(), 'Admin.Orderscustomers.Notification'); + } else { + $this->context->cart = $new_cart['cart']; + echo json_encode($this->ajaxReturnVars()); + } + } + } + + public function ajaxProcessDeleteVoucher() + { + if ($this->access('edit')) { + if ($this->context->cart->removeCartRule((int) Tools::getValue('id_cart_rule'))) { + echo json_encode($this->ajaxReturnVars()); + } + } + } + + public function ajaxProcessupdateFreeShipping() + { + if ($this->access('edit')) { + if (!$id_cart_rule = CartRule::getIdByCode(CartRule::BO_ORDER_CODE_PREFIX . (int) $this->context->cart->id)) { + $cart_rule = new CartRule(); + $cart_rule->code = CartRule::BO_ORDER_CODE_PREFIX . (int) $this->context->cart->id; + $cart_rule->name = array(Configuration::get('PS_LANG_DEFAULT') => $this->trans('Free Shipping', array(), 'Admin.Orderscustomers.Feature')); + $cart_rule->id_customer = (int) $this->context->cart->id_customer; + $cart_rule->free_shipping = true; + $cart_rule->quantity = 1; + $cart_rule->quantity_per_user = 1; + $cart_rule->minimum_amount_currency = (int) $this->context->cart->id_currency; + $cart_rule->reduction_currency = (int) $this->context->cart->id_currency; + $cart_rule->date_from = date('Y-m-d H:i:s', time()); + $cart_rule->date_to = date('Y-m-d H:i:s', time() + 24 * 36000); + $cart_rule->active = 1; + $cart_rule->add(); + } else { + $cart_rule = new CartRule((int) $id_cart_rule); + } + + $this->context->cart->removeCartRule((int) $cart_rule->id); + if (Tools::getValue('free_shipping')) { + $this->context->cart->addCartRule((int) $cart_rule->id); + } + + echo json_encode($this->ajaxReturnVars()); + } + } + + public function ajaxProcessAddVoucher() + { + if ($this->access('edit')) { + $errors = array(); + if (!($id_cart_rule = Tools::getValue('id_cart_rule')) || !$cart_rule = new CartRule((int) $id_cart_rule)) { + $errors[] = $this->trans('Invalid voucher.', array(), 'Admin.Catalog.Notification'); + } elseif ($err = $cart_rule->checkValidity($this->context)) { + $errors[] = $err; + } + if (!count($errors)) { + if (!$this->context->cart->addCartRule((int) $cart_rule->id)) { + $errors[] = $this->trans('Can\'t add the voucher.', array(), 'Admin.Advparameters.Notification'); + } + } + echo json_encode(array_merge($this->ajaxReturnVars(), array('errors' => $errors))); + } + } + + public function ajaxProcessUpdateAddress() + { + if ($this->access('edit')) { + echo json_encode(array('addresses' => $this->context->customer->getAddresses((int) $this->context->cart->id_lang))); + } + } + + public function ajaxProcessUpdateAddresses() + { + if ($this->access('edit')) { + if (($id_address_delivery = (int) Tools::getValue('id_address_delivery')) && + ($address_delivery = new Address((int) $id_address_delivery)) && + $address_delivery->id_customer == $this->context->cart->id_customer) { + $this->context->cart->id_address_delivery = (int) $address_delivery->id; + } + + if (($id_address_invoice = (int) Tools::getValue('id_address_invoice')) && + ($address_invoice = new Address((int) $id_address_invoice)) && + $address_invoice->id_customer = $this->context->cart->id_customer) { + $this->context->cart->id_address_invoice = (int) $address_invoice->id; + } + $this->context->cart->save(); + + echo json_encode($this->ajaxReturnVars()); + } + } + + protected function getCartSummary() + { + $summary = $this->context->cart->getSummaryDetails(null, true); + $currency = Context::getContext()->currency; + if (count($summary['products'])) { + foreach ($summary['products'] as &$product) { + $product['numeric_price'] = $product['price']; + $product['numeric_total'] = $product['total']; + $product['price'] = str_replace($currency->sign, '', Tools::displayPrice($product['price'], $currency)); + $product['total'] = str_replace($currency->sign, '', Tools::displayPrice($product['total'], $currency)); + $product['image_link'] = $this->context->link->getImageLink($product['link_rewrite'], $product['id_image'], 'small_default'); + if (!isset($product['attributes_small'])) { + $product['attributes_small'] = ''; + } + $product['customized_datas'] = Product::getAllCustomizedDatas((int) $this->context->cart->id, null, true, null, (int) $product['id_customization']); + } + } + if (count($summary['discounts'])) { + foreach ($summary['discounts'] as &$voucher) { + $voucher['value_real'] = Tools::displayPrice($voucher['value_real'], $currency); + } + } + + if (isset($summary['gift_products']) && count($summary['gift_products'])) { + foreach ($summary['gift_products'] as &$product) { + $product['image_link'] = $this->context->link->getImageLink($product['link_rewrite'], $product['id_image'], 'small_default'); + if (!isset($product['attributes_small'])) { + $product['attributes_small'] = ''; + } + } + } + + return $summary; + } + + protected function getDeliveryOptionList() + { + $delivery_option_list_formated = array(); + $delivery_option_list = $this->context->cart->getDeliveryOptionList(); + + if (!count($delivery_option_list)) { + return array(); + } + + $id_default_carrier = (int) Configuration::get('PS_CARRIER_DEFAULT'); + foreach (current($delivery_option_list) as $key => $delivery_option) { + $name = ''; + $first = true; + $id_default_carrier_delivery = false; + foreach ($delivery_option['carrier_list'] as $carrier) { + if (!$first) { + $name .= ', '; + } else { + $first = false; + } + + $name .= $carrier['instance']->name; + + if ($delivery_option['unique_carrier']) { + $name .= ' - ' . $carrier['instance']->delay[$this->context->employee->id_lang]; + } + + if (!$id_default_carrier_delivery) { + $id_default_carrier_delivery = (int) $carrier['instance']->id; + } + if ($carrier['instance']->id == $id_default_carrier) { + $id_default_carrier_delivery = $id_default_carrier; + } + if (!$this->context->cart->id_carrier) { + $this->context->cart->setDeliveryOption(array($this->context->cart->id_address_delivery => (int) $carrier['instance']->id . ',')); + $this->context->cart->save(); + } + } + $delivery_option_list_formated[] = array('name' => $name, 'key' => $key); + } + + return $delivery_option_list_formated; + } + + public function displayAjaxSearchCarts() + { + $id_customer = (int) Tools::getValue('id_customer'); + $carts = Cart::getCustomerCarts((int) $id_customer, false); + $orders = Order::getCustomerOrders((int) $id_customer); + + if (count($carts)) { + foreach ($carts as $key => &$cart) { + $cart_obj = new Cart((int) $cart['id_cart']); + if ($cart['id_cart'] == $this->context->cart->id) { + unset($carts[$key]); + continue; + } + $currency = new Currency((int) $cart['id_currency']); + $cart['total_price'] = Tools::displayPrice($cart_obj->getOrderTotal(), $currency); + } + } + if (count($orders)) { + foreach ($orders as &$order) { + $order['total_paid_real'] = Tools::displayPrice($order['total_paid_real'], $currency); + } + } + if ($orders || $carts) { + $to_return = array_merge($this->ajaxReturnVars(), + array( + 'carts' => $carts, + 'orders' => $orders, + 'found' => true, + ) + ); + } else { + $to_return = array_merge($this->ajaxReturnVars(), array('found' => false)); + } + + echo json_encode($to_return); + } + + public function ajaxReturnVars() + { + $id_cart = (int) $this->context->cart->id; + $message_content = ''; + if ($message = Message::getMessageByCartId((int) $this->context->cart->id)) { + $message_content = $message['message']; + } + $cart_rules = $this->context->cart->getCartRules(CartRule::FILTER_ACTION_SHIPPING); + + $free_shipping = false; + if (count($cart_rules)) { + foreach ($cart_rules as $cart_rule) { + if ($cart_rule['id_cart_rule'] == CartRule::getIdByCode(CartRule::BO_ORDER_CODE_PREFIX . (int) $this->context->cart->id)) { + $free_shipping = true; + break; + } + } + } + + $addresses = $this->context->customer->getAddresses((int) $this->context->cart->id_lang); + + foreach ($addresses as &$data) { + $address = new Address((int) $data['id_address']); + $data['formated_address'] = AddressFormat::generateAddress($address, array(), '
'); + } + + return array( + 'summary' => $this->getCartSummary(), + 'delivery_option_list' => $this->getDeliveryOptionList(), + 'cart' => $this->context->cart, + 'currency' => new Currency($this->context->cart->id_currency), + 'addresses' => $addresses, + 'id_cart' => $id_cart, + 'order_message' => $message_content, + 'link_order' => $this->context->link->getPageLink( + 'order', false, + (int) $this->context->cart->id_lang, + 'step=3&recover_cart=' . $id_cart . '&token_cart=' . md5(_COOKIE_KEY_ . 'recover_cart_' . $id_cart) + ), + 'free_shipping' => (int) $free_shipping, + ); + } + + public function initToolbar() + { + parent::initToolbar(); + unset($this->toolbar_btn['new']); + } + + public function displayAjaxGetSummary() + { + echo json_encode($this->ajaxReturnVars()); + } + + public function ajaxProcessUpdateProductPrice() + { + if ($this->access('edit')) { + SpecificPrice::deleteByIdCart((int) $this->context->cart->id, (int) Tools::getValue('id_product'), (int) Tools::getValue('id_product_attribute')); + $specific_price = new SpecificPrice(); + $specific_price->id_cart = (int) $this->context->cart->id; + $specific_price->id_shop = 0; + $specific_price->id_shop_group = 0; + $specific_price->id_currency = 0; + $specific_price->id_country = 0; + $specific_price->id_group = 0; + $specific_price->id_customer = (int) $this->context->customer->id; + $specific_price->id_product = (int) Tools::getValue('id_product'); + $specific_price->id_product_attribute = (int) Tools::getValue('id_product_attribute'); + $specific_price->price = (float) Tools::getValue('price'); + $specific_price->from_quantity = 1; + $specific_price->reduction = 0; + $specific_price->reduction_type = 'amount'; + $specific_price->from = '0000-00-00 00:00:00'; + $specific_price->to = '0000-00-00 00:00:00'; + $specific_price->add(); + echo json_encode($this->ajaxReturnVars()); + } + } + + public static function getOrderTotalUsingTaxCalculationMethod($id_cart) + { + $context = Context::getContext(); + $context->cart = new Cart($id_cart); + $context->currency = new Currency((int) $context->cart->id_currency); + $context->customer = new Customer((int) $context->cart->id_customer); + + return Cart::getTotalCart($id_cart, true, Cart::BOTH_WITHOUT_SHIPPING); + } + + public static function replaceZeroByShopName($echo, $tr) + { + return $echo == '0' ? Carrier::getCarrierNameFromShopName() : $echo; + } + + public function displayDeleteLink($token, $id, $name = null) + { + // don't display ordered carts + foreach ($this->_list as $row) { + if ($row['id_cart'] == $id && isset($row['id_order']) && is_numeric($row['id_order'])) { + return; + } + } + + return $this->helper->displayDeleteLink($token, $id, $name); + } + + public function renderList() + { + if (!($this->fields_list && is_array($this->fields_list))) { + return false; + } + $this->getList($this->context->language->id); + + $helper = new HelperList(); + + // Empty list is ok + if (!is_array($this->_list)) { + $this->displayWarning($this->trans('Bad SQL query', array(), 'Admin.Notifications.Error') . '
' . htmlspecialchars($this->_list_error)); + + return false; + } + + $this->setHelperDisplay($helper); + $helper->tpl_vars = $this->tpl_list_vars; + $helper->tpl_delete_link_vars = $this->tpl_delete_link_vars; + + // For compatibility reasons, we have to check standard actions in class attributes + foreach ($this->actions_available as $action) { + if (!in_array($action, $this->actions) && isset($this->$action) && $this->$action) { + $this->actions[] = $action; + } + } + $helper->is_cms = $this->is_cms; + $skip_list = array(); + + foreach ($this->_list as $row) { + if (isset($row['id_order']) && is_numeric($row['id_order'])) { + $skip_list[] = $row['id_cart']; + } + } + + if (array_key_exists('delete', $helper->list_skip_actions)) { + $helper->list_skip_actions['delete'] = array_merge($helper->list_skip_actions['delete'], (array) $skip_list); + } else { + $helper->list_skip_actions['delete'] = (array) $skip_list; + } + + $list = $helper->generateList($this->_list, $this->fields_list); + + return $list; + } +} diff --git a/controllers/admin/AdminCategoriesController.php b/controllers/admin/AdminCategoriesController.php new file mode 100644 index 00000000..befa19be --- /dev/null +++ b/controllers/admin/AdminCategoriesController.php @@ -0,0 +1,1104 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Category $object + */ +class AdminCategoriesControllerCore extends AdminController +{ + /** + * @var object Category() instance for navigation + */ + protected $_category = null; + protected $position_identifier = 'id_category_to_move'; + + /** @var bool does the product have to be removed during the delete process */ + public $remove_products = true; + + /** @var bool does the product have to be disable during the delete process */ + public $disable_products = false; + + private $original_filter = ''; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'category'; + $this->className = 'Category'; + $this->lang = true; + $this->deleted = false; + $this->explicitSelect = true; + $this->_defaultOrderBy = 'position'; + $this->allow_export = true; + + parent::__construct(); + + $this->fieldImageSettings = array( + 'name' => 'image', + 'dir' => 'c', + ); + + $this->fields_list = array( + 'id_category' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + ), + 'description' => array( + 'title' => $this->trans('Description', array(), 'Admin.Global'), + 'callback' => 'getDescriptionClean', + 'orderby' => false, + ), + 'position' => array( + 'title' => $this->trans('Position', array(), 'Admin.Global'), + 'filter_key' => 'sa!position', + 'position' => 'position', + 'align' => 'center', + ), + 'active' => array( + 'title' => $this->trans('Displayed', array(), 'Admin.Global'), + 'active' => 'status', + 'type' => 'bool', + 'class' => 'fixed-width-xs', + 'align' => 'center', + 'ajax' => true, + 'orderby' => false, + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + ), + ); + $this->specificConfirmDelete = false; + } + + public function init() + { + parent::init(); + + // context->shop is set in the init() function, so we move the _category instanciation after that + if (($id_category = Tools::getValue('id_category')) && $this->action != 'select_delete') { + $this->_category = new Category($id_category); + } else { + if (Shop::getContext() == Shop::CONTEXT_SHOP) { + $this->_category = new Category($this->context->shop->id_category); + } elseif (count(Category::getCategoriesWithoutParent()) > 1 && Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE') && count(Shop::getShops(true, null, true)) != 1) { + $this->_category = Category::getTopCategory(); + } else { + $this->_category = new Category(Configuration::get('PS_HOME_CATEGORY')); + } + } + + $count_categories_without_parent = count(Category::getCategoriesWithoutParent()); + + if (Tools::isSubmit('id_category')) { + $id_parent = $this->_category->id; + } elseif (!Shop::isFeatureActive() && $count_categories_without_parent > 1) { + $id_parent = (int) Configuration::get('PS_ROOT_CATEGORY'); + } elseif (Shop::isFeatureActive() && $count_categories_without_parent == 1) { + $id_parent = (int) Configuration::get('PS_HOME_CATEGORY'); + } elseif (Shop::isFeatureActive() && $count_categories_without_parent > 1 && Shop::getContext() != Shop::CONTEXT_SHOP) { + if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE') && count(Shop::getShops(true, null, true)) == 1) { + $id_parent = $this->context->shop->id_category; + } else { + $id_parent = (int) Configuration::get('PS_ROOT_CATEGORY'); + } + } else { + $id_parent = $this->context->shop->id_category; + } + $this->_select = 'sa.position position'; + $this->original_filter = $this->_filter .= ' AND `id_parent` = ' . (int) $id_parent . ' '; + $this->_use_found_rows = false; + + if (Shop::getContext() == Shop::CONTEXT_SHOP) { + $this->_join .= ' LEFT JOIN `' . _DB_PREFIX_ . 'category_shop` sa ON (a.`id_category` = sa.`id_category` AND sa.id_shop = ' . (int) $this->context->shop->id . ') '; + } else { + $this->_join .= ' LEFT JOIN `' . _DB_PREFIX_ . 'category_shop` sa ON (a.`id_category` = sa.`id_category` AND sa.id_shop = a.id_shop_default) '; + } + + // we add restriction for shop + if (Shop::getContext() == Shop::CONTEXT_SHOP && Shop::isFeatureActive()) { + $this->_where = ' AND sa.`id_shop` = ' . (int) Context::getContext()->shop->id; + } + + // if we are not in a shop context, we remove the position column + if (Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_SHOP) { + unset($this->fields_list['position']); + } + // shop restriction : if category is not available for current shop, we redirect to the list from default category + if (Validate::isLoadedObject($this->_category) && !$this->_category->isAssociatedToShop() && Shop::getContext() == Shop::CONTEXT_SHOP) { + if ($this->_category->id === $this->context->shop->getCategory()) { + $this->redirect_after = $this->context->link->getAdminLink('AdminDashboard') . '&error=1'; + } else { + $this->redirect_after = self::$currentIndex . '&id_category=' . (int) $this->context->shop->getCategory() . '&token=' . $this->token; + } + + $this->redirect(); + } + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + if ($this->display != 'edit' && $this->display != 'add') { + if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE')) { + $this->page_header_toolbar_btn['new-url'] = array( + 'href' => self::$currentIndex . '&add' . $this->table . 'root&token=' . $this->token, + 'desc' => $this->trans('Add new root category', array(), 'Admin.Catalog.Feature'), + ); + } + + $id_category = (Tools::isSubmit('id_category')) ? '&id_parent=' . (int) Tools::getValue('id_category') : ''; + $this->page_header_toolbar_btn['new_category'] = array( + 'href' => self::$currentIndex . '&addcategory&token=' . $this->token . $id_category, + 'desc' => $this->trans('Add new category', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + } + } + + public function initContent() + { + if ($this->action == 'select_delete') { + $this->context->smarty->assign(array( + 'delete_form' => true, + 'url_delete' => htmlentities($_SERVER['REQUEST_URI']), + 'boxes' => $this->boxes, + )); + } + + parent::initContent(); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryUi('ui.widget'); + $this->addJqueryPlugin('tagify'); + } + + public function renderList() + { + if (isset($this->_filter) && trim($this->_filter) == '') { + $this->_filter = $this->original_filter; + } + + $this->addRowAction('view'); + $this->addRowAction('add'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $count_categories_without_parent = count(Category::getCategoriesWithoutParent()); + $categories_tree = $this->_category->getParentsCategories(); + + if (empty($categories_tree) + && ($this->_category->id != (int) Configuration::get('PS_ROOT_CATEGORY') || Tools::isSubmit('id_category')) + && (Shop::getContext() == Shop::CONTEXT_SHOP && !Shop::isFeatureActive() && $count_categories_without_parent > 1)) { + $categories_tree = array(array('name' => $this->_category->name[$this->context->language->id])); + } + + $categories_tree = array_reverse($categories_tree); + if (!empty($categories_tree)) { + $link = Context::getContext()->link; + foreach ($categories_tree as $k => $tree) { + $categories_tree[$k]['edit_link'] = $link->getAdminLink('AdminCategories', true) . '&id_category=' . (int) $tree['id_category'] . '&updatecategory'; + } + } + + $this->tpl_list_vars['categories_tree'] = $categories_tree; + $this->tpl_list_vars['categories_tree_current_id'] = $this->_category->id; + + if (Tools::isSubmit('submitBulkdelete' . $this->table) || Tools::isSubmit('delete' . $this->table)) { + $category = new Category(Tools::getValue('id_category')); + if ($category->is_root_category) { + $this->tpl_list_vars['need_delete_mode'] = false; + } else { + $this->tpl_list_vars['need_delete_mode'] = true; + } + $this->tpl_list_vars['delete_category'] = true; + $this->tpl_list_vars['REQUEST_URI'] = $_SERVER['REQUEST_URI']; + $this->tpl_list_vars['POST'] = $_POST; + } + + return parent::renderList(); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, Context::getContext()->shop->id); + // Check each row to see if there are combinations and get the correct action in consequence + + $nb_items = count($this->_list); + for ($i = 0; $i < $nb_items; ++$i) { + $item = &$this->_list[$i]; + $category_tree = Category::getChildren((int) $item['id_category'], $this->context->language->id, false); + if (!count($category_tree)) { + $this->addRowActionSkipList('view', array($item['id_category'])); + } + } + } + + public function renderView() + { + $this->initToolbar(); + + return $this->renderList(); + } + + public function initToolbar() + { + if (empty($this->display)) { + $this->toolbar_btn['new'] = array( + 'href' => self::$currentIndex . '&add' . $this->table . '&token=' . $this->token, + 'desc' => $this->trans('Add New', array(), 'Admin.Actions'), + ); + + if ($this->can_import) { + $this->toolbar_btn['import'] = array( + 'href' => $this->context->link->getAdminLink('AdminImport', true) . '&import_type=categories', + 'desc' => $this->trans('Import', array(), 'Admin.Actions'), + ); + } + } + + // be able to edit the Home category + if (count(Category::getCategoriesWithoutParent()) == 1 && !Tools::isSubmit('id_category') + && ($this->display == 'view' || empty($this->display)) + && !empty($this->_category) + ) { + $this->toolbar_btn['edit'] = array( + 'href' => self::$currentIndex . '&update' . $this->table . '&id_category=' . (int) $this->_category->id . '&token=' . $this->token, + 'desc' => $this->trans('Edit', array(), 'Admin.Actions'), + ); + } + if (Tools::getValue('id_category') && !Tools::isSubmit('updatecategory')) { + $this->toolbar_btn['edit'] = array( + 'href' => self::$currentIndex . '&update' . $this->table . '&id_category=' . (int) Tools::getValue('id_category') . '&token=' . $this->token, + 'desc' => $this->trans('Edit', array(), 'Admin.Actions'), + ); + } + + if ($this->display == 'view') { + $this->toolbar_btn['new'] = array( + 'href' => self::$currentIndex . '&add' . $this->table . '&id_parent=' . (int) Tools::getValue('id_category') . '&token=' . $this->token, + 'desc' => $this->trans('Add New', array(), 'Admin.Actions'), + ); + } + parent::initToolbar(); + + if (!empty($this->_category) && $this->_category->id == (int) Configuration::get('PS_ROOT_CATEGORY') && isset($this->toolbar_btn['new'])) { + unset($this->toolbar_btn['new']); + } + // after adding a category + if (empty($this->display)) { + $id_category = (Tools::isSubmit('id_category')) ? '&id_parent=' . (int) Tools::getValue('id_category') : ''; + $this->toolbar_btn['new'] = array( + 'href' => self::$currentIndex . '&add' . $this->table . '&token=' . $this->token . $id_category, + 'desc' => $this->trans('Add New', array(), 'Admin.Actions'), + ); + + if (Tools::isSubmit('id_category')) { + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + $this->toolbar_btn['back'] = array( + 'href' => $back, + 'desc' => $this->trans('Back to list', array(), 'Admin.Actions'), + ); + } + } + if (!$this->lite_display + && isset($this->toolbar_btn['back']['href']) + && !empty($this->_category) + && $this->_category->level_depth > 1 + && $this->_category->id_parent + && $this->_category->id_parent != (int) Configuration::get('PS_ROOT_CATEGORY') + ) { + $this->toolbar_btn['back']['href'] .= '&id_category=' . (int) $this->_category->id_parent; + } + } + + public function initProcess() + { + if (Tools::isSubmit('add' . $this->table . 'root')) { + if ($this->access('add')) { + $this->action = 'add' . $this->table . 'root'; + $obj = $this->loadObject(true); + if (Validate::isLoadedObject($obj)) { + $this->display = 'edit'; + } else { + $this->display = 'add'; + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } + + parent::initProcess(); + + if ($this->action == 'delete' || $this->action == 'bulkdelete') { + if (Tools::getIsset('cancel')) { + Tools::redirectAdmin(self::$currentIndex . '&token=' . Tools::getAdminTokenLite('AdminCategories')); + } elseif (Tools::getValue('deleteMode') == 'link' || Tools::getValue('deleteMode') == 'linkanddisable' || Tools::getValue('deleteMode') == 'delete') { + $this->delete_mode = Tools::getValue('deleteMode'); + } else { + $this->action = 'select_delete'; + } + } + } + + public function renderKpis() + { + $time = time(); + $kpis = array(); + + /* The data generation is located in AdminStatsControllerCore */ + + $helper = new HelperKpi(); + $helper->id = 'box-disabled-categories'; + $helper->icon = 'icon-off'; + $helper->color = 'color1'; + $helper->title = $this->trans('Disabled Categories', array(), 'Admin.Catalog.Feature'); + if (ConfigurationKPI::get('DISABLED_CATEGORIES') !== false) { + $helper->value = ConfigurationKPI::get('DISABLED_CATEGORIES'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=disabled_categories'; + $helper->refresh = (bool) (ConfigurationKPI::get('DISABLED_CATEGORIES_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-empty-categories'; + $helper->icon = 'icon-bookmark-empty'; + $helper->color = 'color2'; + $helper->href = $this->context->link->getAdminLink('AdminTracking'); + $helper->title = $this->trans('Empty Categories', array(), 'Admin.Catalog.Feature'); + if (ConfigurationKPI::get('EMPTY_CATEGORIES') !== false) { + $helper->value = ConfigurationKPI::get('EMPTY_CATEGORIES'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=empty_categories'; + $helper->refresh = (bool) (ConfigurationKPI::get('EMPTY_CATEGORIES_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-top-category'; + $helper->icon = 'icon-money'; + $helper->color = 'color3'; + $helper->title = $this->trans('Top Category', array(), 'Admin.Catalog.Feature'); + $helper->subtitle = $this->trans('30 days', array(), 'Admin.Global'); + if (ConfigurationKPI::get('TOP_CATEGORY', $this->context->employee->id_lang) !== false) { + $helper->value = ConfigurationKPI::get('TOP_CATEGORY', $this->context->employee->id_lang); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=top_category'; + $helper->refresh = (bool) (ConfigurationKPI::get('TOP_CATEGORY_EXPIRE', $this->context->employee->id_lang) < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-products-per-category'; + $helper->icon = 'icon-search'; + $helper->color = 'color4'; + $helper->title = $this->trans('Average number of products per category', array(), 'Admin.Catalog.Feature'); + if (ConfigurationKPI::get('PRODUCTS_PER_CATEGORY') !== false) { + $helper->value = ConfigurationKPI::get('PRODUCTS_PER_CATEGORY'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=products_per_category'; + $helper->refresh = (bool) (ConfigurationKPI::get('PRODUCTS_PER_CATEGORY_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpiRow(); + $helper->kpis = $kpis; + + return $helper->generate(); + } + + public function renderForm() + { + $this->initToolbar(); + + /** @var Category $obj */ + $obj = $this->loadObject(true); + $context = Context::getContext(); + $id_shop = $context->shop->id; + $selected_categories = array((isset($obj->id_parent) && $obj->isParentCategoryAvailable($id_shop)) ? (int) $obj->id_parent : (int) Tools::getValue('id_parent', Category::getRootCategory()->id)); + $unidentified = new Group(Configuration::get('PS_UNIDENTIFIED_GROUP')); + $guest = new Group(Configuration::get('PS_GUEST_GROUP')); + $default = new Group(Configuration::get('PS_CUSTOMER_GROUP')); + + $unidentified_group_information = $this->trans('%group_name% - All people without a valid customer account.', array('%group_name%' => '' . $unidentified->name[$this->context->language->id] . ''), 'Admin.Catalog.Feature'); + $guest_group_information = $this->trans('%group_name% - Customer who placed an order with the guest checkout.', array('%group_name%' => '' . $guest->name[$this->context->language->id] . ''), 'Admin.Catalog.Feature'); + $default_group_information = $this->trans('%group_name% - All people who have created an account on this site.', array('%group_name%' => '' . $default->name[$this->context->language->id] . ''), 'Admin.Catalog.Feature'); + + if (!($obj = $this->loadObject(true))) { + return; + } + + $image = _PS_CAT_IMG_DIR_ . $obj->id . '.' . $this->imageType; + $image_url = ImageManager::thumbnail($image, $this->table . '_' . (int) $obj->id . '.' . $this->imageType, 350, $this->imageType, true, true); + + $image_size = file_exists($image) ? filesize($image) / 1000 : false; + $images_types = ImageType::getImagesTypes('categories'); + $format = array(); + $thumb = $thumb_url = ''; + $formatted_category = ImageType::getFormattedName('category'); + $formatted_small = ImageType::getFormattedName('small'); + foreach ($images_types as $k => $image_type) { + if ($formatted_category == $image_type['name']) { + $format['category'] = $image_type; + } elseif ($formatted_small == $image_type['name']) { + $format['small'] = $image_type; + $thumb = _PS_CAT_IMG_DIR_ . $obj->id . '-' . $image_type['name'] . '.' . $this->imageType; + if (is_file($thumb)) { + $thumb_url = ImageManager::thumbnail($thumb, $this->table . '_' . (int) $obj->id . '-thumb.' . $this->imageType, (int) $image_type['width'], $this->imageType, true, true); + } + } + } + + if (!is_file($thumb)) { + $thumb = $image; + $thumb_url = ImageManager::thumbnail($image, $this->table . '_' . (int) $obj->id . '-thumb.' . $this->imageType, 125, $this->imageType, true, true); + ImageManager::resize(_PS_TMP_IMG_DIR_ . $this->table . '_' . (int) $obj->id . '-thumb.' . $this->imageType, _PS_TMP_IMG_DIR_ . $this->table . '_' . (int) $obj->id . '-thumb.' . $this->imageType, (int) $image_type['width'], (int) $image_type['height']); + } + + $thumb_size = file_exists($thumb) ? filesize($thumb) / 1000 : false; + + $menu_thumbnails = []; + for ($i = 0; $i < 3; ++$i) { + if (file_exists(_PS_CAT_IMG_DIR_ . (int) $obj->id . '-' . $i . '_thumb.jpg')) { + $menu_thumbnails[$i]['type'] = HelperImageUploader::TYPE_IMAGE; + $menu_thumbnails[$i]['image'] = ImageManager::thumbnail(_PS_CAT_IMG_DIR_ . (int) $obj->id . '-' . $i . '_thumb.jpg', $this->context->controller->table . '_' . (int) $obj->id . '-' . $i . '_thumb.jpg', 100, 'jpg', true, true); + $menu_thumbnails[$i]['delete_url'] = Context::getContext()->link->getAdminLink('AdminCategories') . '&deleteThumb=' . $i . '&id_category=' . (int) $obj->id; + } + } + + $this->fields_form = array( + 'tinymce' => true, + 'legend' => array( + 'title' => $this->trans('Category', array(), 'Admin.Catalog.Feature'), + 'icon' => 'icon-tags', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'class' => 'copy2friendlyUrl', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Displayed', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('Click on "Displayed" to index the category on your shop.', array(), 'Admin.Catalog.Help'), + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'categories', + 'label' => $this->trans('Parent category', array(), 'Admin.Catalog.Feature'), + 'name' => 'id_parent', + 'tree' => array( + 'id' => 'categories-tree', + 'selected_categories' => $selected_categories, + 'disabled_categories' => (!Tools::isSubmit('add' . $this->table) && !Tools::isSubmit('submitAdd' . $this->table)) ? array($this->_category->id) : null, + 'root_category' => $context->shop->getCategory(), + ), + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + 'autoload_rte' => true, + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'file', + 'label' => $this->trans('Category Cover Image', array(), 'Admin.Catalog.Feature'), + 'name' => 'image', + 'display_image' => true, + 'image' => $image_url ? $image_url : false, + 'size' => $image_size, + 'delete_url' => self::$currentIndex . '&' . $this->identifier . '=' . $this->_category->id . '&token=' . $this->token . '&deleteImage=1', + 'hint' => $this->trans('This is the main image for your category, displayed in the category page. The category description will overlap this image and appear in its top-left corner.', array(), 'Admin.Catalog.Help'), + 'format' => $format['category'], + ), + array( + 'type' => 'file', + 'label' => $this->trans('Category thumbnail', array(), 'Admin.Catalog.Feature'), + 'name' => 'thumb', + 'display_image' => true, + 'image' => $thumb_url ? $thumb_url : false, + 'size' => $thumb_size, + 'format' => isset($format['small']) ? $format['small'] : $format['category'], + 'hint' => $this->trans('Displays a small image in the parent category\'s page, if the theme allows it.', array(), 'Admin.Catalog.Help'), + ), + array( + 'type' => 'file', + 'label' => $this->trans('Menu thumbnails', array(), 'Admin.Catalog.Feature'), + 'name' => 'thumbnail', + 'ajax' => true, + 'multiple' => true, + 'max_files' => 3, + 'files' => $menu_thumbnails, + 'url' => Context::getContext()->link->getAdminLink('AdminCategories') . '&ajax=1&id_category=' . $this->id . '&action=uploadThumbnailImages', + 'hint' => $this->trans('The category thumbnail appears in the menu as a small image representing the category, if the theme allows it.', array(), 'Admin.Catalog.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta title', array(), 'Admin.Global'), + 'name' => 'meta_title', + 'maxlength' => 255, + 'maxchar' => 255, + 'lang' => true, + 'rows' => 5, + 'cols' => 100, + 'hint' => $this->trans('Forbidden characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Meta description', array(), 'Admin.Global'), + 'name' => 'meta_description', + 'maxlength' => 512, + 'maxchar' => 512, + 'lang' => true, + 'rows' => 5, + 'cols' => 100, + 'hint' => $this->trans('Forbidden characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'tags', + 'label' => $this->trans('Meta keywords', array(), 'Admin.Global'), + 'name' => 'meta_keywords', + 'lang' => true, + 'hint' => $this->trans('To add "tags," click in the field, write something, and then press "Enter."', array(), 'Admin.Catalog.Help') . ' ' . $this->trans('Forbidden characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Friendly URL', array(), 'Admin.Global'), + 'name' => 'link_rewrite', + 'lang' => true, + 'required' => true, + 'hint' => $this->trans('Only letters, numbers, underscore (_) and the minus (-) character are allowed.', array(), 'Admin.Catalog.Help'), + ), + array( + 'type' => 'group', + 'label' => $this->trans('Group access', array(), 'Admin.Catalog.Feature'), + 'name' => 'groupBox', + 'values' => Group::getGroups(Context::getContext()->language->id), + 'info_introduction' => $this->trans('You now have three default customer groups.', array(), 'Admin.Catalog.Help'), + 'unidentified' => $unidentified_group_information, + 'guest' => $guest_group_information, + 'customer' => $default_group_information, + 'hint' => $this->trans('Mark all of the customer groups which you would like to have access to this category.', array(), 'Admin.Catalog.Help'), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + 'name' => 'submitAdd' . $this->table . ($this->_category->is_root_category && !Tools::isSubmit('add' . $this->table) && !Tools::isSubmit('add' . $this->table . 'root') ? '' : 'AndBackToParent'), + ), + ); + + $this->tpl_form_vars['shared_category'] = Validate::isLoadedObject($obj) && $obj->hasMultishopEntries(); + $this->tpl_form_vars['PS_ALLOW_ACCENTED_CHARS_URL'] = (int) Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL'); + $this->tpl_form_vars['displayBackOfficeCategory'] = Hook::exec('displayBackOfficeCategory'); + + // Display this field only if multistore option is enabled + if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE') && Tools::isSubmit('add' . $this->table . 'root')) { + $this->fields_form['input'][] = array( + 'type' => 'switch', + 'label' => $this->trans('Root Category', array(), 'Admin.Catalog.Feature'), + 'name' => 'is_root_category', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'is_root_on', + 'value' => 1, + 'label' => $this->trans('Yes', array(), 'Admin.Global'), + ), + array( + 'id' => 'is_root_off', + 'value' => 0, + 'label' => $this->trans('No', array(), 'Admin.Global'), + ), + ), + ); + unset($this->fields_form['input'][2], $this->fields_form['input'][3]); + } + // Display this field only if multistore option is enabled AND there are several stores configured + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + // remove category tree and radio button "is_root_category" if this category has the root category as parent category to avoid any conflict + if ($this->_category->id_parent == (int) Configuration::get('PS_ROOT_CATEGORY') && Tools::isSubmit('updatecategory')) { + foreach ($this->fields_form['input'] as $k => $input) { + if (in_array($input['name'], array('id_parent', 'is_root_category'))) { + unset($this->fields_form['input'][$k]); + } + } + } + + if (!($obj = $this->loadObject(true))) { + return; + } + + $image = ImageManager::thumbnail(_PS_CAT_IMG_DIR_ . '/' . $obj->id . '.' . $this->imageType, $this->table . '_' . (int) $obj->id . '.' . $this->imageType, 350, $this->imageType, true); + + $this->fields_value = array( + 'image' => $image ? $image : false, + 'size' => $image ? filesize(_PS_CAT_IMG_DIR_ . '/' . $obj->id . '.' . $this->imageType) / 1000 : false, + ); + + // Added values of object Group + $category_groups_ids = $obj->getGroups(); + + $groups = Group::getGroups($this->context->language->id); + // if empty $carrier_groups_ids : object creation : we set the default groups + if (empty($category_groups_ids)) { + $preselected = array(Configuration::get('PS_UNIDENTIFIED_GROUP'), Configuration::get('PS_GUEST_GROUP'), Configuration::get('PS_CUSTOMER_GROUP')); + $category_groups_ids = array_merge($category_groups_ids, $preselected); + } + foreach ($groups as $group) { + $this->fields_value['groupBox_' . $group['id_group']] = Tools::getValue('groupBox_' . $group['id_group'], (in_array($group['id_group'], $category_groups_ids))); + } + + $this->fields_value['is_root_category'] = (bool) Tools::isSubmit('add' . $this->table . 'root'); + + return parent::renderForm(); + } + + public function postProcess() + { + if (!in_array($this->display, array('edit', 'add'))) { + $this->multishop_context_group = false; + } + if (Tools::isSubmit('forcedeleteImage') || (isset($_FILES['image']) && $_FILES['image']['size'] > 0) || Tools::getValue('deleteImage')) { + $this->processForceDeleteImage(); + $this->processForceDeleteThumb(); + if (Tools::isSubmit('forcedeleteImage')) { + Tools::redirectAdmin(self::$currentIndex . '&token=' . Tools::getAdminTokenLite('AdminCategories') . '&conf=7'); + } + } + if (($id_thumb = Tools::getValue('deleteThumb', false)) !== false) { + if (file_exists(_PS_CAT_IMG_DIR_ . (int) Tools::getValue('id_category') . '-' . (int) $id_thumb . '_thumb.jpg') + && !unlink(_PS_CAT_IMG_DIR_ . (int) Tools::getValue('id_category') . '-' . (int) $id_thumb . '_thumb.jpg')) { + $this->context->controller->errors[] = $this->trans('Error while delete', array(), 'Admin.Notifications.Error'); + } + + if (empty($this->context->controller->errors)) { + Tools::clearSmartyCache(); + } + + Tools::redirectAdmin(Context::getContext()->link->getAdminLink('AdminCategories') . '&id_category=' + . (int) Tools::getValue('id_category') . '&updatecategory'); + } + + return parent::postProcess(); + } + + public function processForceDeleteThumb() + { + $category = $this->loadObject(true); + + if (Validate::isLoadedObject($category)) { + if (file_exists(_PS_TMP_IMG_DIR_ . $this->table . '_' . $category->id . '-thumb.' . $this->imageType) + && !unlink(_PS_TMP_IMG_DIR_ . $this->table . '_' . $category->id . '-thumb.' . $this->imageType)) { + return false; + } + if (file_exists(_PS_CAT_IMG_DIR_ . $category->id . '_thumb.' . $this->imageType) + && !unlink(_PS_CAT_IMG_DIR_ . $category->id . '_thumb.' . $this->imageType)) { + return false; + } + + $images_types = ImageType::getImagesTypes('categories'); + $formatted_small = ImageType::getFormattedName('small'); + foreach ($images_types as $image_type) { + if ($formatted_small == $image_type['name'] && + file_exists(_PS_CAT_IMG_DIR_ . $category->id . '-' . $image_type['name'] . '.' . $this->imageType) && + !unlink(_PS_CAT_IMG_DIR_ . $category->id . '-' . $image_type['name'] . '.' . $this->imageType) + ) { + return false; + } + } + } + + return true; + } + + public function processForceDeleteImage() + { + $category = $this->loadObject(true); + if (Validate::isLoadedObject($category)) { + $category->deleteImage(true); + } + } + + public function processAdd() + { + $id_category = (int) Tools::getValue('id_category'); + $id_parent = (int) Tools::getValue('id_parent'); + + // if true, we are in a root category creation + if (!$id_parent) { + $_POST['is_root_category'] = $_POST['level_depth'] = 1; + $_POST['id_parent'] = $id_parent = (int) Configuration::get('PS_ROOT_CATEGORY'); + } + + if ($id_category) { + if ($id_category != $id_parent) { + if (!Category::checkBeforeMove($id_category, $id_parent)) { + $this->errors[] = $this->trans('The category cannot be moved here.', array(), 'Admin.Catalog.Notification'); + } + } else { + $this->errors[] = $this->trans('The category cannot be a parent of itself.', array(), 'Admin.Catalog.Notification'); + } + } + $object = parent::processAdd(); + + //if we create a you root category you have to associate to a shop before to add sub categories in. So we redirect to AdminCategories listing + if ($object && Tools::getValue('is_root_category')) { + Tools::redirectAdmin(self::$currentIndex . '&id_category=' . (int) Configuration::get('PS_ROOT_CATEGORY') . '&token=' . Tools::getAdminTokenLite('AdminCategories') . '&conf=3'); + } + + return $object; + } + + protected function setDeleteMode() + { + if ($this->delete_mode == 'link' || $this->delete_mode == 'linkanddisable') { + $this->remove_products = false; + if ($this->delete_mode == 'linkanddisable') { + $this->disable_products = true; + } + } elseif ($this->delete_mode != 'delete') { + $this->errors[] = $this->trans('Unknown delete mode: %s', array($this->deleted), 'Admin.Catalog.Notification'); + } + } + + protected function processBulkDelete() + { + if ($this->access('delete')) { + $cats_ids = array(); + foreach (Tools::getValue($this->table . 'Box') as $id_category) { + $category = new Category((int) $id_category); + if (!$category->isRootCategoryForAShop()) { + $cats_ids[$category->id] = $category->id_parent; + } + } + + if (parent::processBulkDelete()) { + $this->setDeleteMode(); + foreach ($cats_ids as $id_parent) { + $this->processFatherlessProducts((int) $id_parent); + } + + return true; + } else { + return false; + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } + + public function processDelete() + { + if ($this->access('delete')) { + /** @var Category $category */ + $category = $this->loadObject(); + if ($category->isRootCategoryForAShop()) { + $this->errors[] = $this->trans('You cannot remove this category because one of your shops uses it as a root category.', array(), 'Admin.Catalog.Notification'); + } elseif (parent::processDelete()) { + $this->setDeleteMode(); + $this->processFatherlessProducts((int) $category->id_parent); + + return true; + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + + return false; + } + + public function processFatherlessProducts($id_parent) + { + /* Delete or link products which were not in others categories */ + $fatherless_products = Db::getInstance()->executeS(' + SELECT p.`id_product` FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + WHERE NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'category_product` cp WHERE cp.`id_product` = p.`id_product`)'); + + foreach ($fatherless_products as $id_poor_product) { + $poor_product = new Product((int) $id_poor_product['id_product']); + if (Validate::isLoadedObject($poor_product)) { + if ($this->remove_products || $id_parent == 0) { + $poor_product->delete(); + } else { + if ($this->disable_products) { + $poor_product->active = 0; + } + $poor_product->id_category_default = (int) $id_parent; + $poor_product->addToCategories((int) $id_parent); + $poor_product->save(); + } + } + } + } + + public function processPosition() + { + if ($this->access('edit') !== '1') { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } elseif (!Validate::isLoadedObject($object = new Category((int) Tools::getValue($this->identifier, Tools::getValue('id_category_to_move', 1))))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') . ' ' . + $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + if (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', array(), 'Admin.Notifications.Error'); + } else { + $object->regenerateEntireNtree(); + Tools::redirectAdmin(self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=5' . (($id_category = (int) Tools::getValue($this->identifier, Tools::getValue('id_category_parent', 1))) ? ('&' . $this->identifier . '=' . $id_category) : '') . '&token=' . Tools::getAdminTokenLite('AdminCategories')); + } + } + + protected function postImage($id) + { + $ret = parent::postImage($id); + if (($id_category = (int) Tools::getValue('id_category')) && isset($_FILES) && count($_FILES)) { + $name = 'image'; + if ($_FILES[$name]['name'] != null && file_exists(_PS_CAT_IMG_DIR_ . $id_category . '.' . $this->imageType)) { + $images_types = ImageType::getImagesTypes('categories'); + foreach ($images_types as $k => $image_type) { + if (!ImageManager::resize( + _PS_CAT_IMG_DIR_ . $id_category . '.' . $this->imageType, + _PS_CAT_IMG_DIR_ . $id_category . '-' . stripslashes($image_type['name']) . '.' . $this->imageType, + (int) $image_type['width'], + (int) $image_type['height'] + )) { + $this->errors = $this->trans('An error occurred while uploading category image.', array(), 'Admin.Catalog.Notification'); + } + } + } + + $name = 'thumb'; + if ($_FILES[$name]['name'] != null) { + if (!isset($images_types)) { + $images_types = ImageType::getImagesTypes('categories'); + } + $formatted_small = ImageType::getFormattedName('small'); + foreach ($images_types as $k => $image_type) { + if ($formatted_small == $image_type['name']) { + if ($error = ImageManager::validateUpload($_FILES[$name], Tools::getMaxUploadSize())) { + $this->errors[] = $error; + } elseif (!($tmpName = tempnam(_PS_TMP_IMG_DIR_, 'PS')) || !move_uploaded_file($_FILES[$name]['tmp_name'], $tmpName)) { + $ret = false; + } else { + if (!ImageManager::resize( + $tmpName, + _PS_CAT_IMG_DIR_ . $id_category . '-' . stripslashes($image_type['name']) . '.' . $this->imageType, + (int) $image_type['width'], + (int) $image_type['height'] + )) { + $this->errors = $this->trans('An error occurred while uploading thumbnail image.', array(), 'Admin.Catalog.Notification'); + } elseif (($infos = getimagesize($tmpName)) && is_array($infos)) { + ImageManager::resize( + $tmpName, + _PS_CAT_IMG_DIR_ . $id_category . '_' . $name . '.' . $this->imageType, + (int) $infos[0], + (int) $infos[1] + ); + } + if (count($this->errors)) { + $ret = false; + } + unlink($tmpName); + $ret = true; + } + } + } + } + } + + return $ret; + } + + public static function getDescriptionClean($description) + { + return Tools::getDescriptionClean($description); + } + + public function ajaxProcessUpdatePositions() + { + $id_category_to_move = (int) Tools::getValue('id_category_to_move'); + $id_category_parent = (int) Tools::getValue('id_category_parent'); + $way = (int) Tools::getValue('way'); + $positions = Tools::getValue('category'); + $found_first = (bool) Tools::getValue('found_first'); + if (is_array($positions)) { + foreach ($positions as $key => $value) { + $pos = explode('_', $value); + if ((isset($pos[1]) && isset($pos[2])) && ($pos[1] == $id_category_parent && $pos[2] == $id_category_to_move)) { + $position = $key; + break; + } + } + } + + $category = new Category($id_category_to_move); + if (Validate::isLoadedObject($category)) { + if (isset($position) && $category->updatePosition($way, $position)) { + /* Position '0' was not found in given positions so try to reorder parent category*/ + if (!$found_first) { + $category->cleanPositions((int) $category->id_parent); + } + + die(true); + } else { + die('{"hasError" : true, errors : "Cannot update categories position"}'); + } + } else { + die('{"hasError" : true, "errors" : "This category cannot be loaded"}'); + } + } + + public function ajaxProcessStatusCategory() + { + if (!$id_category = (int) Tools::getValue('id_category')) { + die(json_encode(array('success' => false, 'error' => true, 'text' => $this->trans('Failed to update the status', array(), 'Admin.Notifications.Error')))); + } else { + $category = new Category((int) $id_category); + if (Validate::isLoadedObject($category)) { + $category->active = $category->active == 1 ? 0 : 1; + $category->save() ? + die(json_encode(array('success' => true, 'text' => $this->trans('The status has been updated successfully', array(), 'Admin.Notifications.Success')))) : + die(json_encode(array('success' => false, 'error' => true, 'text' => $this->trans('Failed to update the status', array(), 'Admin.Notifications.Success')))); + } + } + } + + public function ajaxProcessuploadThumbnailImages() + { + $category = new Category((int) Tools::getValue('id_category')); + + if (isset($_FILES['thumbnail'])) { + //Get total of image already present in directory + $files = scandir(_PS_CAT_IMG_DIR_, SCANDIR_SORT_NONE); + $assigned_keys = array(); + $allowed_keys = array(0, 1, 2); + + foreach ($files as $file) { + $matches = array(); + + if (preg_match('/^' . $category->id . '-([0-9])?_thumb.jpg/i', $file, $matches) === 1) { + $assigned_keys[] = (int) $matches[1]; + } + } + + $available_keys = array_diff($allowed_keys, $assigned_keys); + $helper = new HelperImageUploader('thumbnail'); + $files = $helper->process(); + $total_errors = array(); + + if (count($available_keys) < count($files)) { + $total_errors['name'] = $this->trans('An error occurred while uploading the image:', array(), 'Admin.Catalog.Notification'); + $total_errors['error'] = $this->trans('You cannot upload more files', array(), 'Admin.Notifications.Error'); + die(Tools::jsonEncode(array('thumbnail' => array($total_errors)))); + } + + foreach ($files as &$file) { + $id = array_shift($available_keys); + $errors = array(); + // Evaluate the memory required to resize the image: if it's too much, you can't resize it. + if (isset($file['save_path']) && !ImageManager::checkImageMemoryLimit($file['save_path'])) { + $errors[] = $this->trans('Due to memory limit restrictions, this image cannot be loaded. Please increase your memory_limit value via your server\'s configuration settings. ', array(), 'Admin.Notifications.Error'); + } + // Copy new image + if (!isset($file['save_path']) || (empty($errors) && !ImageManager::resize($file['save_path'], _PS_CAT_IMG_DIR_ + . (int) Tools::getValue('id_category') . '-' . $id . '_thumb.jpg'))) { + $errors[] = $this->trans('An error occurred while uploading the image.', array(), 'Admin.Catalog.Notification'); + } + + if (count($errors)) { + $total_errors = array_merge($total_errors, $errors); + } + + if (isset($file['save_path']) && is_file($file['save_path'])) { + unlink($file['save_path']); + } + //Necesary to prevent hacking + if (isset($file['save_path'])) { + unset($file['save_path']); + } + + if (isset($file['tmp_name'])) { + unset($file['tmp_name']); + } + + //Add image preview and delete url + $file['image'] = ImageManager::thumbnail(_PS_CAT_IMG_DIR_ . (int) $category->id . '-' . $id . '_thumb.jpg', + $this->context->controller->table . '_' . (int) $category->id . '-' . $id . '_thumb.jpg', 100, 'jpg', true, true); + $file['delete_url'] = Context::getContext()->link->getAdminLink('AdminCategories') . '&deleteThumb=' + . $id . '&id_category=' . (int) $category->id . '&updatecategory'; + } + + if (count($total_errors)) { + $this->context->controller->errors = array_merge($this->context->controller->errors, $total_errors); + } else { + Tools::clearSmartyCache(); + } + + die(Tools::jsonEncode(array('thumbnail' => $files))); + } + } +} diff --git a/controllers/admin/AdminCmsCategoriesController.php b/controllers/admin/AdminCmsCategoriesController.php new file mode 100644 index 00000000..252f18a9 --- /dev/null +++ b/controllers/admin/AdminCmsCategoriesController.php @@ -0,0 +1,311 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property CMSCategory $object + */ +class AdminCmsCategoriesControllerCore extends AdminController +{ + /** @var object CMSCategory() instance for navigation */ + protected $cms_category; + + protected $position_identifier = 'id_cms_category_to_move'; + + public function __construct() + { + $this->bootstrap = true; + $this->is_cms = true; + $this->table = 'cms_category'; + $this->list_id = 'cms_category'; + $this->className = 'CMSCategory'; + $this->lang = true; + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->_orderBy = 'position'; + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + $this->tpl_list_vars['icon'] = 'icon-folder-close'; + $this->tpl_list_vars['title'] = $this->trans('Categories', array(), 'Admin.Catalog.Feature'); + $this->fields_list = array( + 'id_cms_category' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'width' => 'auto', 'callback' => 'hideCMSCategoryPosition', 'callback_object' => 'CMSCategory'), + 'description' => array('title' => $this->trans('Description', array(), 'Admin.Global'), 'maxlength' => 90, 'orderby' => false), + 'position' => array('title' => $this->trans('Position', array(), 'Admin.Global'), 'filter_key' => 'position', 'align' => 'center', 'class' => 'fixed-width-sm', 'position' => 'position'), + 'active' => array( + 'title' => $this->trans('Displayed', array(), 'Admin.Global'), 'class' => 'fixed-width-sm', 'active' => 'status', + 'align' => 'center', 'type' => 'bool', 'orderby' => false, + ), ); + + // The controller can't be call directly + // In this case, AdminCmsContentController::getCurrentCMSCategory() is null + if (!AdminCmsContentController::getCurrentCMSCategory()) { + $this->redirect_after = '?controller=AdminCmsContent&token=' . Tools::getAdminTokenLite('AdminCmsContent'); + $this->redirect(); + } + + $this->cms_category = AdminCmsContentController::getCurrentCMSCategory(); + $this->_where = ' AND `id_parent` = ' . (int) $this->cms_category->id; + $this->_select = 'position '; + } + + public function getTabSlug() + { + return 'ROLE_MOD_TAB_ADMINCMSCONTENT_'; + } + + public function renderList() + { + $this->initToolbar(); + $this->_group = 'GROUP BY a.`id_cms_category`'; + if (isset($this->toolbar_btn['new'])) { + $this->toolbar_btn['new']['href'] .= '&id_parent=' . (int) Tools::getValue('id_cms_category'); + } + + return parent::renderList(); + } + + public function postProcess() + { + if (Tools::isSubmit('submitAdd' . $this->table)) { + $this->action = 'save'; + if ($id_cms_category = (int) Tools::getValue('id_cms_category')) { + $this->id_object = $id_cms_category; + if (!CMSCategory::checkBeforeMove($id_cms_category, (int) Tools::getValue('id_parent'))) { + $this->errors[] = $this->trans('The page Category cannot be moved here.', array(), 'Admin.Design.Notification'); + + return false; + } + } + $object = parent::postProcess(); + $this->updateAssoShop((int) Tools::getValue('id_cms_category')); + if ($object !== false) { + Tools::redirectAdmin(self::$currentIndex . '&conf=3&id_cms_category=' . (int) $object->id . '&token=' . Tools::getValue('token')); + } + + return $object; + } elseif (Tools::isSubmit('statuscms_category') && Tools::getValue($this->identifier)) { + // Change object statuts (active, inactive) + if ($this->access('edit')) { + if (Validate::isLoadedObject($object = $this->loadObject())) { + if ($object->toggleStatus()) { + $identifier = ((int) $object->id_parent ? '&id_cms_category=' . (int) $object->id_parent : ''); + Tools::redirectAdmin(self::$currentIndex . '&conf=5' . $identifier . '&token=' . Tools::getValue('token')); + } else { + $this->errors[] = $this->trans('An error occurred while updating the status.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('delete' . $this->table)) { + // Delete object + if ($this->access('delete')) { + if (Validate::isLoadedObject($object = $this->loadObject()) && isset($this->fieldImageSettings)) { + // check if request at least one object with noZeroObject + if (isset($object->noZeroObject) && count($taxes = call_user_func(array($this->className, $object->noZeroObject))) <= 1) { + $this->errors[] = $this->trans('You need at least one object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . '
' . $this->trans('You cannot delete all of the items.', array(), 'Admin.Notifications.Error'); + } else { + $identifier = ((int) $object->id_parent ? '&' . $this->identifier . '=' . (int) $object->id_parent : ''); + if ($this->deleted) { + $object->deleted = 1; + if ($object->update()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . Tools::getValue('token') . $identifier); + } + } elseif ($object->delete()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . Tools::getValue('token') . $identifier); + } + $this->errors[] = $this->trans('An error occurred during deletion.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('An error occurred while deleting the object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('position')) { + $object = new CMSCategory((int) Tools::getValue($this->identifier, Tools::getValue('id_cms_category_to_move', 1))); + if (!$this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } elseif (!Validate::isLoadedObject($object)) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', array(), 'Admin.Notifications.Error'); + } else { + $identifier = ((int) $object->id_parent ? '&' . $this->identifier . '=' . (int) $object->id_parent : ''); + $token = Tools::getAdminTokenLite('AdminCmsContent'); + Tools::redirectAdmin( + self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=5' . $identifier . '&token=' . $token + ); + } + } elseif (Tools::getValue('submitDel' . $this->table) || Tools::getValue('submitBulkdelete' . $this->table)) { + // Delete multiple objects + if ($this->access('delete')) { + if (Tools::isSubmit($this->table . 'Box')) { + $cms_category = new CMSCategory(); + $result = true; + $result = $cms_category->deleteSelection(Tools::getValue($this->table . 'Box')); + if ($result) { + $cms_category->cleanPositions((int) Tools::getValue('id_cms_category')); + $token = Tools::getAdminTokenLite('AdminCmsContent'); + Tools::redirectAdmin(self::$currentIndex . '&conf=2&token=' . $token . '&id_cms_category=' . (int) Tools::getValue('id_cms_category')); + } + $this->errors[] = $this->trans('An error occurred while deleting this selection.', array(), 'Admin.Notifications.Error'); + } else { + $this->errors[] = $this->trans('You must select at least one element to delete.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } + parent::postProcess(); + } + + public function renderForm() + { + $this->display = 'edit'; + $this->initToolbar(); + if (!$this->loadObject(true)) { + return; + } + + $categories = CMSCategory::getCategories($this->context->language->id, false); + $html_categories = CMSCategory::recurseCMSCategory($categories, $categories[0][1], 1, $this->getFieldValue($this->object, 'id_parent'), 1); + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('CMS Category', array(), 'Admin.Design.Feature'), + 'icon' => 'icon-folder-close', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'class' => 'copyMeta2friendlyURL', + 'required' => true, + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Displayed', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + // custom template + array( + 'type' => 'select_category', + 'label' => $this->trans('Parent category', array(), 'Admin.Design.Feature'), + 'name' => 'id_parent', + 'options' => array( + 'html' => $html_categories, + ), + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + 'lang' => true, + 'rows' => 5, + 'cols' => 40, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta title', array(), 'Admin.Global'), + 'name' => 'meta_title', + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta description', array(), 'Admin.Global'), + 'name' => 'meta_description', + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta keywords', array(), 'Admin.Global'), + 'name' => 'meta_keywords', + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Friendly URL', array(), 'Admin.Global'), + 'name' => 'link_rewrite', + 'required' => true, + 'lang' => true, + 'hint' => $this->trans('Only letters and the minus (-) character are allowed.', array(), 'Admin.Catalog.Help'), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->tpl_form_vars['PS_ALLOW_ACCENTED_CHARS_URL'] = (int) Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL'); + + return parent::renderForm(); + } +} diff --git a/controllers/admin/AdminCmsContentController.php b/controllers/admin/AdminCmsContentController.php new file mode 100644 index 00000000..b4b08194 --- /dev/null +++ b/controllers/admin/AdminCmsContentController.php @@ -0,0 +1,304 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property CMS $object + */ +class AdminCmsContentControllerCore extends AdminController +{ + /** @var object adminCMSCategories() instance */ + protected $admin_cms_categories; + + /** @var object adminCMS() instance */ + protected $admin_cms; + + /** @var object Category() instance for navigation */ + protected static $category = null; + + public function __construct() + { + $this->bootstrap = true; + /* Get current category */ + $id_cms_category = (int) Tools::getValue('id_cms_category', Tools::getValue('id_cms_category_parent', 1)); + self::$category = new CMSCategory($id_cms_category); + if (!Validate::isLoadedObject(self::$category)) { + die('Category cannot be loaded'); + } + + parent::__construct(); + + $this->table = 'cms'; + $this->className = 'CMS'; + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->admin_cms_categories = new AdminCmsCategoriesController(); + $this->admin_cms_categories->tabAccess = $this->tabAccess; + $this->admin_cms_categories->init(); + $this->admin_cms = new AdminCmsController(); + $this->admin_cms->tabAccess = $this->tabAccess; + $this->admin_cms->init(); + $this->context->controller = $this; + } + + /** + * Return current category. + * + * @return object + */ + public static function getCurrentCMSCategory() + { + return self::$category; + } + + public function initProcess() + { + if (((Tools::isSubmit('submitAddcms_category') || Tools::isSubmit('submitAddcms_categoryAndStay')) && count($this->admin_cms_categories->errors)) + || Tools::isSubmit('updatecms_category') + || Tools::isSubmit('addcms_category')) { + $this->display = 'edit_category'; + } elseif (((Tools::isSubmit('submitAddcms') || Tools::isSubmit('submitAddcmsAndStay')) && count($this->admin_cms->errors)) + || Tools::isSubmit('updatecms') + || Tools::isSubmit('addcms')) { + $this->display = 'edit_page'; + } else { + $this->display = 'list'; + } + } + + public function initContent() + { + $id_cms_category = (int) Tools::getValue('id_cms_category'); + + if (!$id_cms_category) { + $id_cms_category = 1; + } + + if ($this->display == 'list') { + $this->page_header_toolbar_btn['new_cms_category'] = array( + 'href' => self::$currentIndex . '&addcms_category&token=' . $this->token, + 'desc' => $this->trans('Add new page category', array(), 'Admin.Design.Help'), + 'icon' => 'process-icon-new', + ); + $this->page_header_toolbar_btn['new_cms_page'] = array( + 'href' => self::$currentIndex . '&addcms&id_cms_category=' . (int) $id_cms_category . '&token=' . $this->token, + 'desc' => $this->trans('Add new page', array(), 'Admin.Design.Help'), + 'icon' => 'process-icon-new', + ); + } + + $this->page_header_toolbar_title = implode(' ' . Configuration::get('PS_NAVIGATION_PIPE') . ' ', $this->toolbar_title); + + if (is_array($this->page_header_toolbar_btn) + && $this->page_header_toolbar_btn instanceof Traversable + || trim($this->page_header_toolbar_title) != '') { + $this->show_page_header_toolbar = true; + } + + $this->admin_cms_categories->token = $this->token; + $this->admin_cms->token = $this->token; + + if ($this->display == 'edit_category') { + $this->content .= $this->admin_cms_categories->renderForm(); + } elseif ($this->display == 'edit_page') { + $this->content .= $this->admin_cms->renderForm(); + } elseif ($this->display == 'list') { + $id_cms_category = (int) Tools::getValue('id_cms_category'); + if (!$id_cms_category) { + $id_cms_category = 1; + } + + // CMS categories breadcrumb + $cms_tabs = array('cms_category', 'cms'); + // Cleaning links + $cat_bar_index = self::$currentIndex; + foreach ($cms_tabs as $tab) { + if (Tools::getValue($tab . 'Orderby') && Tools::getValue($tab . 'Orderway')) { + $cat_bar_index = preg_replace('/&' . $tab . 'Orderby=([a-z _]*)&' . $tab . 'Orderway=([a-z]*)/i', '', self::$currentIndex); + } + } + $this->context->smarty->assign(array( + 'cms_breadcrumb' => Tools::getPath($cat_bar_index, $id_cms_category, '', '', 'cms'), + )); + + $this->content .= $this->admin_cms_categories->renderList(); + $this->admin_cms->id_cms_category = $id_cms_category; + $this->content .= $this->admin_cms->renderList(); + } + + $this->context->smarty->assign(array( + 'content' => $this->content, + 'show_page_header_toolbar' => $this->show_page_header_toolbar, + 'title' => $this->page_header_toolbar_title, + 'toolbar_btn' => $this->page_header_toolbar_btn, + 'page_header_toolbar_btn' => $this->page_header_toolbar_btn, + 'page_header_toolbar_title' => $this->toolbar_title, + )); + } + + public function initToolbarTitle() + { + $this->toolbar_title = is_array($this->breadcrumbs) ? array_unique($this->breadcrumbs) : array($this->breadcrumbs); + + $id_cms_category = (int) Tools::getValue('id_cms_category'); + if ($id_cms_category && $id_cms_category !== 1) { + $cms_category = new CMSCategory($id_cms_category); + } + $id_cms_page = Tools::getValue('id_cms'); + + if ($this->display == 'edit_category') { + if (Tools::getValue('addcms_category') !== false) { + $this->toolbar_title[] = $this->trans('Add new category', array(), 'Admin.Design.Feature'); + } else { + if (isset($cms_category)) { + $this->toolbar_title[] = $this->trans('Edit category: %name%', array('%name%' => $cms_category->name[$this->context->employee->id_lang]), 'Admin.Design.Feature'); + } + } + } elseif ($this->display == 'edit_page') { + if (Tools::getValue('addcms') !== false) { + $this->toolbar_title[] = $this->trans('Add new page', array(), 'Admin.Design.Feature'); + } elseif ($id_cms_page) { + $cms_page = new CMS($id_cms_page); + $this->toolbar_title[] = $this->trans('Edit page: %meta_title%', array('%meta_title%' => $cms_page->meta_title[$this->context->employee->id_lang]), 'Admin.Design.Feature'); + } + } elseif ($this->display == 'list' && isset($cms_category)) { + $this->toolbar_title[] = $this->trans('Category: %category%', array('%category%' => $cms_category->name[$this->context->employee->id_lang]), 'Admin.Design.Feature'); + } + } + + public function postProcess() + { + $this->admin_cms->postProcess(); + $this->admin_cms_categories->postProcess(); + + parent::postProcess(); + + if (isset($this->admin_cms->errors)) { + $this->errors = array_merge($this->errors, $this->admin_cms->errors); + } + + if (isset($this->admin_cms_categories->errors)) { + $this->errors = array_merge($this->errors, $this->admin_cms_categories->errors); + } + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryUi('ui.widget'); + $this->addJqueryPlugin('tagify'); + } + + public function ajaxProcessUpdateCmsPositions() + { + if ($this->access('edit')) { + $id_cms = (int) Tools::getValue('id_cms'); + $id_category = (int) Tools::getValue('id_cms_category'); + $way = (int) Tools::getValue('way'); + $positions = Tools::getValue('cms'); + if (is_array($positions)) { + foreach ($positions as $key => $value) { + $pos = explode('_', $value); + if ((isset($pos[1]) && isset($pos[2])) && ($pos[1] == $id_category && $pos[2] == $id_cms)) { + $position = $key; + break; + } + } + } + $cms = new CMS($id_cms); + if (Validate::isLoadedObject($cms)) { + if (isset($position) && $cms->updatePosition($way, $position)) { + die(true); + } else { + die('{"hasError" : true, "errors" : "Can not update cms position"}'); + } + } else { + die('{"hasError" : true, "errors" : "This cms can not be loaded"}'); + } + } + } + + public function ajaxProcessUpdateCmsCategoriesPositions() + { + if ($this->access('edit')) { + $id_cms_category_to_move = (int) Tools::getValue('id_cms_category_to_move'); + $id_cms_category_parent = (int) Tools::getValue('id_cms_category_parent'); + $way = (int) Tools::getValue('way'); + $positions = Tools::getValue('cms_category'); + if (is_array($positions)) { + foreach ($positions as $key => $value) { + $pos = explode('_', $value); + if ((isset($pos[1]) && isset($pos[2])) && ($pos[1] == $id_cms_category_parent && $pos[2] == $id_cms_category_to_move)) { + $position = $key; + break; + } + } + } + $cms_category = new CMSCategory($id_cms_category_to_move); + if (Validate::isLoadedObject($cms_category)) { + if (isset($position) && $cms_category->updatePosition($way, $position)) { + die(true); + } else { + die('{"hasError" : true, "errors" : "Can not update cms categories position"}'); + } + } else { + die('{"hasError" : true, "errors" : "This cms category can not be loaded"}'); + } + } + } + + public function ajaxProcessPublishCMS() + { + if ($this->access('edit')) { + if ($id_cms = (int) Tools::getValue('id_cms')) { + $bo_cms_url = _PS_BASE_URL_ . __PS_BASE_URI__ . basename(_PS_ADMIN_DIR_) . '/index.php?tab=AdminCmsContent&id_cms=' . (int) $id_cms . '&updatecms&token=' . $this->token; + + if (Tools::getValue('redirect')) { + die($bo_cms_url); + } + + $cms = new CMS((int) (Tools::getValue('id_cms'))); + if (!Validate::isLoadedObject($cms)) { + die('error: invalid id'); + } + + $cms->active = 1; + if ($cms->save()) { + die($bo_cms_url); + } else { + die('error: saving'); + } + } else { + die('error: parameters'); + } + } + } +} diff --git a/controllers/admin/AdminCmsController.php b/controllers/admin/AdminCmsController.php new file mode 100644 index 00000000..fd94d42b --- /dev/null +++ b/controllers/admin/AdminCmsController.php @@ -0,0 +1,459 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property CMS $object + */ +class AdminCmsControllerCore extends AdminController +{ + protected $category; + + public $id_cms_category; + + protected $position_identifier = 'id_cms'; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'cms'; + $this->list_id = 'cms'; + $this->className = 'CMS'; + $this->lang = true; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->_orderBy = 'position'; + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + $this->fields_list = array( + 'id_cms' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'link_rewrite' => array( + 'title' => $this->trans('URL', array(), 'Admin.Global'), + ), + 'meta_title' => array( + 'title' => $this->trans('Title', array(), 'Admin.Global'), + 'filter_key' => 'b!meta_title', + 'maxlength' => 50, + ), + 'head_seo_title' => array( + 'title' => $this->trans('Meta title', array(), 'Admin.Global'), + 'filter_key' => 'b!head_seo_title', + 'maxlength' => 50, + ), + 'position' => array( + 'title' => $this->trans('Position', array(), 'Admin.Global'), + 'filter_key' => 'position', + 'align' => 'center', + 'class' => 'fixed-width-sm', + 'position' => 'position', + ), + 'active' => array( + 'title' => $this->trans('Displayed', array(), 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'class' => 'fixed-width-sm', + 'type' => 'bool', + 'orderby' => false, + ), + ); + + // The controller can't be call directly + // In this case, AdminCmsContentController::getCurrentCMSCategory() is null + if (!AdminCmsContentController::getCurrentCMSCategory()) { + $this->redirect_after = '?controller=AdminCmsContent&token=' . Tools::getAdminTokenLite('AdminCmsContent'); + $this->redirect(); + } + + $this->_category = AdminCmsContentController::getCurrentCMSCategory(); + $this->tpl_list_vars['icon'] = 'icon-folder-close'; + $this->tpl_list_vars['title'] = $this->trans('Pages in category "%name%"', array('%name%' => $this->_category->name[Context::getContext()->employee->id_lang]), 'Admin.Design.Feature'); + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'cms_category` c ON (c.`id_cms_category` = a.`id_cms_category`)'; + $this->_select = 'a.position '; + $this->_where = ' AND c.id_cms_category = ' . (int) $this->_category->id; + } + + public function getTabSlug() + { + return 'ROLE_MOD_TAB_ADMINCMSCONTENT_'; + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_btn['save-and-preview'] = array( + 'href' => '#', + 'desc' => $this->trans('Save and preview', array(), 'Admin.Actions'), + ); + $this->page_header_toolbar_btn['save-and-stay'] = array( + 'short' => $this->trans('Save and stay', array(), 'Admin.Actions'), + 'href' => '#', + 'desc' => $this->trans('Save and stay', array(), 'Admin.Actions'), + ); + + return parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + if (!$this->loadObject(true)) { + return; + } + + if (Validate::isLoadedObject($this->object)) { + $this->display = 'edit'; + } else { + $this->display = 'add'; + } + + $this->initToolbar(); + $this->initPageHeaderToolbar(); + + $categories = CMSCategory::getCategories($this->context->language->id, false); + $html_categories = CMSCategory::recurseCMSCategory($categories, $categories[0][1], 1, $this->getFieldValue($this->object, 'id_cms_category'), 1); + + $this->fields_form = array( + 'tinymce' => true, + 'legend' => array( + 'title' => $this->l('Page'), + 'icon' => 'icon-folder-close', + ), + 'input' => array( + // custom template + array( + 'type' => 'select_category', + 'label' => $this->trans('Page Category', array(), 'Admin.Design.Feature'), + 'name' => 'id_cms_category', + 'options' => array( + 'html' => $html_categories, + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Title', array(), 'Admin.Global'), + 'name' => 'meta_title', + 'id' => 'name', // for copyMeta2friendlyURL compatibility + 'lang' => true, + 'required' => true, + 'class' => 'copyMeta2friendlyURL', + 'hint' => array( + $this->trans('Used in the h1 page tag, and as the default title tag value.', array(), 'Admin.Design.Help'), + $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta title', array(), 'Admin.Global'), + 'name' => 'head_seo_title', + 'lang' => true, + 'hint' => array( + $this->trans('Used to override the title tag value. If left blank, the default title value is used.', array(), 'Admin.Design.Help'), + $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta description', array(), 'Admin.Global'), + 'name' => 'meta_description', + 'lang' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'tags', + 'label' => $this->trans('Meta keywords', array(), 'Admin.Global'), + 'name' => 'meta_keywords', + 'lang' => true, + 'hint' => array( + $this->trans('To add "tags" click in the field, write something, and then press "Enter."', array(), 'Admin.Design.Help'), + $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Friendly URL', array(), 'Admin.Global'), + 'name' => 'link_rewrite', + 'required' => true, + 'lang' => true, + 'hint' => $this->trans('Only letters and the hyphen (-) character are allowed.', array(), 'Admin.Design.Feature'), + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Page content', array(), 'Admin.Design.Feature'), + 'name' => 'content', + 'autoload_rte' => true, + 'lang' => true, + 'rows' => 5, + 'cols' => 40, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Indexation by search engines', array(), 'Admin.Design.Feature'), + 'name' => 'indexation', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'indexation_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'indexation_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Displayed', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + 'buttons' => array( + 'save_and_preview' => array( + 'name' => 'viewcms', + 'type' => 'submit', + 'title' => $this->trans('Save and preview', array(), 'Admin.Actions'), + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-preview', + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + if (Validate::isLoadedObject($this->object)) { + $this->context->smarty->assign('url_prev', $this->getPreviewUrl($this->object)); + } + + $this->tpl_form_vars = array( + 'active' => $this->object->active, + 'PS_ALLOW_ACCENTED_CHARS_URL', (int) Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL'), + ); + + return parent::renderForm(); + } + + public function renderList() + { + $this->_group = 'GROUP BY a.`id_cms`'; + //self::$currentIndex = self::$currentIndex.'&cms'; + $this->position_group_identifier = (int) $this->id_cms_category; + + $this->toolbar_title = $this->trans('Pages in this category', array(), 'Admin.Design.Feature'); + $this->toolbar_btn['new'] = array( + 'href' => self::$currentIndex . '&add' . $this->table . '&id_cms_category=' . (int) $this->id_cms_category . '&token=' . $this->token, + 'desc' => $this->trans('Add new', array(), 'Admin.Actions'), + ); + + return parent::renderList(); + } + + public function displayList($token = null) + { + /* Display list header (filtering, pagination and column names) */ + $this->displayListHeader($token); + if (!count($this->_list)) { + echo '' . $this->trans('No items found', array(), 'Admin.Design.Notification') . ''; + } + + /* Show the content of the table */ + $this->displayListContent($token); + + /* Close list table and submit button */ + $this->displayListFooter($token); + } + + public function postProcess() + { + if (Tools::isSubmit('viewcms') && ($id_cms = (int) Tools::getValue('id_cms'))) { + parent::postProcess(); + if (($cms = new CMS($id_cms, $this->context->language->id)) && Validate::isLoadedObject($cms)) { + Tools::redirectAdmin(self::$currentIndex . '&id_cms=' . $id_cms . '&conf=4&updatecms&token=' . Tools::getAdminTokenLite('AdminCmsContent') . '&url_preview=1'); + } + } elseif (Tools::isSubmit('deletecms')) { + if (Tools::getValue('id_cms') == Configuration::get('PS_CONDITIONS_CMS_ID')) { + Configuration::updateValue('PS_CONDITIONS', 0); + Configuration::updateValue('PS_CONDITIONS_CMS_ID', 0); + } + $cms = new CMS((int) Tools::getValue('id_cms')); + $cms->cleanPositions($cms->id_cms_category); + if (!$cms->delete()) { + $this->errors[] = $this->trans('An error occurred while deleting the object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' (' . Db::getInstance()->getMsgError() . ')'; + } else { + Tools::redirectAdmin(self::$currentIndex . '&id_cms_category=' . $cms->id_cms_category . '&conf=1&token=' . Tools::getAdminTokenLite('AdminCmsContent')); + } + } elseif (Tools::getValue('submitDel' . $this->table)) { + // Delete multiple objects + if ($this->access('delete')) { + if (Tools::isSubmit($this->table . 'Box')) { + $cms = new CMS(); + $result = true; + $result = $cms->deleteSelection(Tools::getValue($this->table . 'Box')); + if ($result) { + $cms->cleanPositions((int) Tools::getValue('id_cms_category')); + $token = Tools::getAdminTokenLite('AdminCmsContent'); + Tools::redirectAdmin(self::$currentIndex . '&conf=2&token=' . $token . '&id_cms_category=' . (int) Tools::getValue('id_cms_category')); + } + $this->errors[] = $this->trans('An error occurred while deleting this selection.', array(), 'Admin.Notifications.Error'); + } else { + $this->errors[] = $this->trans('You must select at least one element to delete.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitAddcms') || Tools::isSubmit('submitAddcmsAndPreview')) { + parent::validateRules(); + if (count($this->errors)) { + return false; + } + if (!$id_cms = (int) Tools::getValue('id_cms')) { + $cms = new CMS(); + $this->copyFromPost($cms, 'cms'); + if (!$cms->add()) { + $this->errors[] = $this->trans('An error occurred while creating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ' (' . Db::getInstance()->getMsgError() . ')'; + } else { + $this->updateAssoShop($cms->id); + } + } else { + $cms = new CMS($id_cms); + $this->copyFromPost($cms, 'cms'); + if (!$cms->update()) { + $this->errors[] = $this->trans('An error occurred while updating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ' (' . Db::getInstance()->getMsgError() . ')'; + } else { + $this->updateAssoShop($cms->id); + } + } + if (Tools::isSubmit('view' . $this->table)) { + Tools::redirectAdmin(self::$currentIndex . '&id_cms=' . $cms->id . '&conf=4&updatecms&token=' . Tools::getAdminTokenLite('AdminCmsContent') . '&url_preview=1'); + } elseif (Tools::isSubmit('submitAdd' . $this->table . 'AndStay')) { + Tools::redirectAdmin(self::$currentIndex . '&' . $this->identifier . '=' . $cms->id . '&conf=4&update' . $this->table . '&token=' . Tools::getAdminTokenLite('AdminCmsContent')); + } else { + Tools::redirectAdmin(self::$currentIndex . '&id_cms_category=' . $cms->id_cms_category . '&conf=4&token=' . Tools::getAdminTokenLite('AdminCmsContent')); + } + } elseif (Tools::isSubmit('way') && Tools::isSubmit('id_cms') && (Tools::isSubmit('position'))) { + /* @var CMS $object */ + if (!$this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } elseif (!Validate::isLoadedObject($object = $this->loadObject())) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', array(), 'Admin.Notifications.Error'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=4&id_cms_category=' . (int) $object->id_cms_category . '&token=' . Tools::getAdminTokenLite('AdminCmsContent')); + } + } elseif (Tools::isSubmit('statuscms') && Tools::isSubmit($this->identifier)) { + // Change object status (active, inactive) + if ($this->access('edit')) { + if (Validate::isLoadedObject($object = $this->loadObject())) { + /** @var CMS $object */ + if ($object->toggleStatus()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=5&id_cms_category=' . (int) $object->id_cms_category . '&token=' . Tools::getValue('token')); + } else { + $this->errors[] = $this->trans('An error occurred while updating the status.', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') + . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitBulkdeletecms')) { + // Delete multiple CMS content + if ($this->access('delete')) { + $this->action = 'bulkdelete'; + $this->boxes = Tools::getValue($this->table . 'Box'); + if (is_array($this->boxes) && array_key_exists(0, $this->boxes)) { + $firstCms = new CMS((int) $this->boxes[0]); + $id_cms_category = (int) $firstCms->id_cms_category; + if (!$res = parent::postProcess(true)) { + return $res; + } + Tools::redirectAdmin(self::$currentIndex . '&conf=2&token=' . Tools::getAdminTokenLite('AdminCmsContent') . '&id_cms_category=' . $id_cms_category); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } else { + parent::postProcess(true); + } + } + + public function getPreviewUrl(CMS $cms) + { + $preview_url = $this->context->link->getCMSLink($cms, null, null, $this->context->language->id); + if (!$cms->active) { + $params = http_build_query(array( + 'adtoken' => Tools::getAdminTokenLite('AdminCmsContent'), + 'ad' => basename(_PS_ADMIN_DIR_), + 'id_employee' => (int) $this->context->employee->id, + ) + ); + $preview_url .= (strpos($preview_url, '?') === false ? '?' : '&') . $params; + } + + return $preview_url; + } +} diff --git a/controllers/admin/AdminContactsController.php b/controllers/admin/AdminContactsController.php new file mode 100644 index 00000000..61771cf6 --- /dev/null +++ b/controllers/admin/AdminContactsController.php @@ -0,0 +1,156 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Contact $object + */ +class AdminContactsControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'contact'; + $this->className = 'Contact'; + $this->lang = true; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->fields_list = array( + 'id_contact' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Title', array(), 'Admin.Global'), + 'maxlength' => 30, + ), + 'email' => array( + 'title' => $this->trans('Email address', array(), 'Admin.Global'), + 'maxlength' => 50, + ), + 'description' => array( + 'title' => $this->trans('Description', array(), 'Admin.Global'), + ), + ); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Contacts', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-envelope-alt', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Title', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'lang' => true, + 'col' => 4, + 'hint' => $this->trans('Contact name (e.g. Customer Support).', array(), 'Admin.Shopparameters.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Email address', array(), 'Admin.Global'), + 'name' => 'email', + 'required' => false, + 'col' => 4, + 'hint' => $this->trans('Emails will be sent to this address.', array(), 'Admin.Shopparameters.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Save messages?', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'customer_service', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'hint' => $this->trans('If enabled, all messages will be saved in the "Customer Service" page under the "Customer" menu.', array(), 'Admin.Shopparameters.Help'), + 'values' => array( + array( + 'id' => 'customer_service_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'customer_service_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + 'required' => false, + 'lang' => true, + 'col' => 6, + 'hint' => $this->trans('Further information regarding this contact.', array(), 'Admin.Shopparameters.Help'), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + return parent::renderForm(); + } + + public function initPageHeaderToolbar() + { + $this->initToolbar(); + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_contact'] = array( + 'href' => self::$currentIndex . '&addcontact&token=' . $this->token, + 'desc' => $this->trans('Add new contact', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } +} diff --git a/controllers/admin/AdminCountriesController.php b/controllers/admin/AdminCountriesController.php new file mode 100644 index 00000000..adf5effd --- /dev/null +++ b/controllers/admin/AdminCountriesController.php @@ -0,0 +1,528 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Country $object + */ +class AdminCountriesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'country'; + $this->className = 'Country'; + $this->lang = true; + $this->deleted = false; + $this->_defaultOrderBy = 'name'; + $this->_defaultOrderWay = 'ASC'; + + $this->explicitSelect = true; + $this->addRowAction('edit'); + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array('text' => $this->trans('Delete selected', array(), 'Admin.Actions'), 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Actions')), + 'AffectZone' => array('text' => $this->trans('Assign to a new zone', array(), 'Admin.International.Feature')), + ); + + $this->fieldImageSettings = array( + 'name' => 'logo', + 'dir' => 'st', + ); + + $this->fields_options = array( + 'general' => array( + 'title' => $this->trans('Country options', array(), 'Admin.International.Feature'), + 'fields' => array( + 'PS_RESTRICT_DELIVERED_COUNTRIES' => array( + 'title' => $this->trans('Restrict country selections in front office to those covered by active carriers', array(), 'Admin.International.Help'), + 'cast' => 'intval', + 'type' => 'bool', + 'default' => '0', + ), + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ), + ); + + $zones_array = array(); + $this->zones = Zone::getZones(); + foreach ($this->zones as $zone) { + $zones_array[$zone['id_zone']] = $zone['name']; + } + + $this->fields_list = array( + 'id_country' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Country', array(), 'Admin.Global'), + 'filter_key' => 'b!name', + ), + 'iso_code' => array( + 'title' => $this->trans('ISO code', array(), 'Admin.International.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'call_prefix' => array( + 'title' => $this->trans('Call prefix', array(), 'Admin.International.Feature'), + 'align' => 'center', + 'callback' => 'displayCallPrefix', + 'class' => 'fixed-width-sm', + ), + 'zone' => array( + 'title' => $this->trans('Zone', array(), 'Admin.Global'), + 'type' => 'select', + 'list' => $zones_array, + 'filter_key' => 'z!id_zone', + 'filter_type' => 'int', + 'order_key' => 'z!name', + ), + 'active' => array( + 'title' => $this->trans('Enabled', array(), 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + 'filter_key' => 'a!active', + 'class' => 'fixed-width-sm', + ), + ); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_country'] = array( + 'href' => self::$currentIndex . '&addcountry&token=' . $this->token, + 'desc' => $this->trans('Add new country', array(), 'Admin.International.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + /** + * AdminController::setMedia() override. + * + * @see AdminController::setMedia() + */ + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + + $this->addJqueryPlugin('fieldselection'); + } + + public function renderList() + { + $this->_select = 'z.`name` AS zone'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'zone` z ON (z.`id_zone` = a.`id_zone`)'; + $this->_use_found_rows = false; + + $this->tpl_list_vars['zones'] = Zone::getZones(); + $this->tpl_list_vars['REQUEST_URI'] = $_SERVER['REQUEST_URI']; + $this->tpl_list_vars['POST'] = $_POST; + + return parent::renderList(); + } + + public function renderForm() + { + if (!($obj = $this->loadObject(true))) { + return; + } + + $address_layout = AddressFormat::getAddressCountryFormat($obj->id); + if ($value = Tools::getValue('address_layout')) { + $address_layout = $value; + } + + $default_layout = ''; + + // TODO: Use format from XML + $default_layout_tab = [ + array('firstname', 'lastname'), + array('company'), + array('vat_number'), + array('address1'), + array('address2'), + array('postcode', 'city'), + array('Country:name'), + array('phone'), + ]; + + foreach ($default_layout_tab as $line) { + $default_layout .= implode(' ', $line) . "\r\n"; + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Countries', array(), 'Admin.International.Feature'), + 'icon' => 'icon-globe', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Country', array(), 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->trans('Country name', array(), 'Admin.International.Feature') . ' - ' . $this->trans('Invalid characters:', array(), 'Admin.Global') . ' <>;=#{} ', + ), + array( + 'type' => 'text', + 'label' => $this->trans('ISO code', array(), 'Admin.International.Feature'), + 'name' => 'iso_code', + 'maxlength' => 3, + 'class' => 'uppercase', + 'required' => true, + 'hint' => $this->trans('Two -- or three -- letter ISO code (e.g. "us" for United States).', array(), 'Admin.International.Help'), + /* @TODO - add two lines for the hint? */ + /*'desc' => $this->l('Two -- or three -- letter ISO code (e.g. U.S. for United States)').'. + '. + $this->l('Official list here').' + .'*/ + ), + array( + 'type' => 'text', + 'label' => $this->trans('Call prefix', array(), 'Admin.International.Feature'), + 'name' => 'call_prefix', + 'maxlength' => 3, + 'class' => 'uppercase', + 'required' => true, + 'hint' => $this->trans('International call prefix, (e.g. 1 for United States).', array(), 'Admin.International.Help'), + ), + array( + 'type' => 'select', + 'label' => $this->trans('Default currency', array(), 'Admin.International.Feature'), + 'name' => 'id_currency', + 'options' => array( + 'query' => Currency::getCurrencies(false, true, true), + 'id' => 'id_currency', + 'name' => 'name', + 'default' => array( + 'label' => $this->trans('Default store currency', array(), 'Admin.International.Feature'), + 'value' => 0, + ), + ), + ), + array( + 'type' => 'select', + 'label' => $this->trans('Zone', array(), 'Admin.Global'), + 'name' => 'id_zone', + 'options' => array( + 'query' => Zone::getZones(), + 'id' => 'id_zone', + 'name' => 'name', + ), + 'hint' => $this->trans('Geographical region.', array(), 'Admin.International.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Does it need Zip/postal code?', array(), 'Admin.International.Feature'), + 'name' => 'need_zip_code', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'need_zip_code_on', + 'value' => 1, + 'label' => $this->trans('Yes', array(), 'Admin.Global'), + ), + array( + 'id' => 'need_zip_code_off', + 'value' => 0, + 'label' => $this->trans('No', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Zip/postal code format', array(), 'Admin.International.Feature'), + 'name' => 'zip_code_format', + 'required' => true, + 'desc' => $this->trans('Indicate the format of the postal code: use L for a letter, N for a number, and C for the country\'s ISO 3166-1 alpha-2 code. For example, NNNNN for the United States, France, Poland and many other; LNNNNLLL for Argentina, etc. If you do not want PrestaShop to verify the postal code for this country, leave it blank.', array(), 'Admin.International.Help'), + ), + array( + 'type' => 'address_layout', + 'label' => $this->trans('Address format', array(), 'Admin.International.Feature'), + 'name' => 'address_layout', + 'address_layout' => $address_layout, + 'encoding_address_layout' => urlencode($address_layout), + 'encoding_default_layout' => urlencode($default_layout), + 'display_valid_fields' => $this->displayValidFields(), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Active', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Display this country to your customers (the selected country will always be displayed in the Back Office).', array(), 'Admin.International.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Contains states', array(), 'Admin.International.Feature'), + 'name' => 'contains_states', + 'required' => false, + 'values' => array( + array( + 'id' => 'contains_states_on', + 'value' => 1, + 'label' => '' . $this->trans('Yes', array(), 'Admin.Global') . '' . $this->trans('Yes', array(), 'Admin.Global'), + ), + array( + 'id' => 'contains_states_off', + 'value' => 0, + 'label' => '' . $this->trans('No', array(), 'Admin.Global') . '' . $this->trans('No', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Do you need a tax identification number?', array(), 'Admin.International.Feature'), + 'name' => 'need_identification_number', + 'required' => false, + 'values' => array( + array( + 'id' => 'need_identification_number_on', + 'value' => 1, + 'label' => '' . $this->trans('Yes', array(), 'Admin.Global') . '' . $this->trans('Yes', array(), 'Admin.Global'), + ), + array( + 'id' => 'need_identification_number_off', + 'value' => 0, + 'label' => '' . $this->trans('No', array(), 'Admin.Global') . '' . $this->trans('No', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Display tax label (e.g. "Tax incl.")', array(), 'Admin.International.Feature'), + 'name' => 'display_tax_label', + 'required' => false, + 'values' => array( + array( + 'id' => 'display_tax_label_on', + 'value' => 1, + 'label' => '' . $this->trans('Yes', array(), 'Admin.Global') . '' . $this->trans('Yes', array(), 'Admin.Global'), + ), + array( + 'id' => 'display_tax_label_off', + 'value' => 0, + 'label' => '' . $this->trans('No', array(), 'Admin.Global') . '' . $this->trans('No', array(), 'Admin.Global'), + ), + ), + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + return parent::renderForm(); + } + + public function processUpdate() + { + /** @var Country $country */ + $country = $this->loadObject(); + if (Validate::isLoadedObject($country) && Tools::getValue('id_zone')) { + $old_id_zone = $country->id_zone; + $results = Db::getInstance()->executeS('SELECT `id_state` FROM `' . _DB_PREFIX_ . 'state` WHERE `id_country` = ' . (int) $country->id . ' AND `id_zone` = ' . (int) $old_id_zone); + + if ($results && count($results)) { + $ids = array(); + foreach ($results as $res) { + $ids[] = (int) $res['id_state']; + } + + if (count($ids)) { + $res = Db::getInstance()->execute( + 'UPDATE `' . _DB_PREFIX_ . 'state` + SET `id_zone` = ' . (int) Tools::getValue('id_zone') . ' + WHERE `id_state` IN (' . implode(',', $ids) . ')'); + } + } + } + + return parent::processUpdate(); + } + + public function postProcess() + { + if (!Tools::getValue('id_' . $this->table)) { + if (Validate::isLanguageIsoCode(Tools::getValue('iso_code')) && (int) Country::getByIso(Tools::getValue('iso_code'))) { + $this->errors[] = $this->trans('This ISO code already exists.You cannot create two countries with the same ISO code.', array(), 'Admin.International.Notification'); + } + } elseif (Validate::isLanguageIsoCode(Tools::getValue('iso_code'))) { + $id_country = (int) Country::getByIso(Tools::getValue('iso_code')); + if ($id_country != 0 && $id_country != Tools::getValue('id_' . $this->table)) { + $this->errors[] = $this->trans('This ISO code already exists.You cannot create two countries with the same ISO code.', array(), 'Admin.International.Notification'); + } + } + + return parent::postProcess(); + } + + public function processSave() + { + if (!$this->id_object) { + $tmp_addr_format = new AddressFormat(); + } else { + $tmp_addr_format = new AddressFormat($this->id_object); + } + + $tmp_addr_format->format = Tools::getValue('address_layout'); + + if (!$tmp_addr_format->checkFormatFields()) { + $error_list = $tmp_addr_format->getErrorList(); + foreach ($error_list as $error) { + $this->errors[] = $error; + } + } + if (strlen($tmp_addr_format->format) <= 0) { + $this->errors[] = $this->trans('Address format invalid', array(), 'Admin.Notifications.Error'); + } + + $country = parent::processSave(); + + if (!count($this->errors)) { + if (is_null($tmp_addr_format->id_country)) { + $tmp_addr_format->id_country = $country->id; + } + + if (!$tmp_addr_format->save()) { + $this->errors[] = $this->trans('Invalid address layout %s', array(Db::getInstance()->getMsgError()), 'Admin.International.Notification'); + } + } + + return $country; + } + + public function processStatus() + { + parent::processStatus(); + + /** @var Country $object */ + if (Validate::isLoadedObject($object = $this->loadObject()) && $object->active == 1) { + return Country::addModuleRestrictions(array(), array(array('id_country' => $object->id)), array()); + } + + return false; + } + + /** + * Allow the assignation of zone only if the form is displayed. + */ + protected function processBulkAffectZone() + { + $zone_to_affect = Tools::getValue('zone_to_affect'); + if ($zone_to_affect && $zone_to_affect !== 0) { + parent::processBulkAffectZone(); + } + + if (Tools::getIsset('submitBulkAffectZonecountry')) { + $this->tpl_list_vars['assign_zone'] = true; + } + + return; + } + + protected function displayValidFields() + { + /* The following translations are needed later - don't remove the comments! + $this->trans('Customer', array(), 'Admin.Global'); + $this->l('Warehouse'); + $this->trans('Country', array(), 'Admin.Global'); + $this->l('State'); + $this->l('Address'); + */ + + $html_tabnav = ''; + $html_tabcontent .= ''; + + return $html_tabnav . $html_tabcontent; + } + + public static function displayCallPrefix($prefix) + { + return (int) $prefix ? '+' . $prefix : '-'; + } +} diff --git a/controllers/admin/AdminCurrenciesController.php b/controllers/admin/AdminCurrenciesController.php new file mode 100644 index 00000000..a9875598 --- /dev/null +++ b/controllers/admin/AdminCurrenciesController.php @@ -0,0 +1,340 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Currency $object + */ +use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; + +class AdminCurrenciesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'currency'; + $this->className = 'Currency'; + $this->lang = false; + $this->cldr = Tools::getCldr(Context::getContext()); + + parent::__construct(); + + $this->fields_list = array( + 'name' => array('title' => $this->trans('Currency', array(), 'Admin.Global'), 'orderby' => false, 'search' => false), + 'sign' => array('title' => $this->trans('Symbol', array(), 'Admin.International.Feature'), 'width' => 20, 'align' => 'center', 'orderby' => false, 'search' => false, 'class' => 'fixed-width-xs'), + 'iso_code' => array('title' => $this->trans('ISO code', array(), 'Admin.International.Feature'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'conversion_rate' => array('title' => $this->trans('Exchange rate', array(), 'Admin.International.Feature'), 'type' => 'float', 'align' => 'center', 'width' => 130, 'search' => false, 'filter_key' => 'currency_shop!conversion_rate'), + 'active' => array('title' => $this->trans('Enabled', array(), 'Admin.Global'), 'width' => 25, 'align' => 'center', 'active' => 'status', 'type' => 'bool', 'orderby' => false, 'class' => 'fixed-width-sm'), + ); + + $this->_select .= 'currency_shop.conversion_rate conversion_rate'; + $this->_join .= Shop::addSqlAssociation('currency', 'a'); + $this->_group .= 'GROUP BY a.id_currency'; + } + + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_where = 'AND a.`deleted` = 0'; + + //retrieve datas list + $this->getList($this->context->language->id); + + foreach ($this->_list as $k => $v) { + $currency = $this->cldr->getCurrency($this->_list[$k]['iso_code']); + + $this->_list[$k]['name'] = ucfirst($currency['name']); + $this->_list[$k]['sign'] = $currency['symbol']; + $this->_list[$k]['iso_code'] .= ' / ' . $currency['iso_code']; + } + + $helper = new HelperList(); + $this->setHelperDisplay($helper); + + return $helper->generateList($this->_list, $this->fields_list); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, Context::getContext()->shop->id); + } + + public function renderForm() + { + $currency = null; + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Currencies', array(), 'Admin.Global'), + 'icon' => 'icon-money', + ), + 'input' => array( + array( + 'type' => 'select', + 'col' => '4', + 'label' => $this->trans('Currency', array(), 'Admin.Global'), + 'name' => 'iso_code', + 'required' => true, + 'hint' => $this->trans('ISO code (e.g. USD for Dollars, EUR for Euros, etc.).', array(), 'Admin.International.Help'), + 'options' => array( + 'query' => $this->cldr->getAllCurrencies(), + 'name' => 'name', + 'id' => 'code', + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Exchange rate', array(), 'Admin.International.Feature'), + 'name' => 'conversion_rate', + 'maxlength' => 11, + 'required' => true, + 'col' => '2', + 'hint' => $this->trans('Exchange rates are calculated from one unit of your shop\'s default currency. For example, if the default currency is euros and your chosen currency is dollars, type "1.20" (1€ = $1.20).', array(), 'Admin.International.Help'), + ), + array( + 'type' => 'hidden', + 'label' => $this->trans('Enable', array(), 'Admin.Actions'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + //form preselect : define the default currency or object value + if (Tools::getValue('id_currency')) { + $currency = new Currency((int) Tools::getValue('id_currency')); + if ($currency) { + $this->fields_value = array( + 'iso_code' => $currency->iso_code, + ); + } + } else { + $this->fields_value = array( + 'iso_code' => $this->cldr->getCurrency()['code'], + ); + } + + $this->context->smarty->assign('status', $currency ? $currency->active : 0); + $this->context->smarty->assign('isForm', true); + + return parent::renderForm(); + } + + protected function checkDeletion($object) + { + if (Validate::isLoadedObject($object)) { + if ($object->id == Configuration::get('PS_CURRENCY_DEFAULT')) { + $this->errors[] = $this->trans('You cannot delete the default currency', array(), 'Admin.International.Notification'); + } else { + return true; + } + } else { + $this->errors[] = $this->trans('An error occurred while deleting the object.', array(), 'Admin.Notifications.Error') . ' + ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + + return false; + } + + protected function checkDisableStatus($object) + { + if (Validate::isLoadedObject($object)) { + if ($object->active && $object->id == Configuration::get('PS_CURRENCY_DEFAULT')) { + $this->errors[] = $this->trans('You cannot disable the default currency', array(), 'Admin.International.Notification'); + } else { + return true; + } + } else { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') . ' + ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + + return false; + } + + /** + * @see AdminController::processDelete() + */ + public function processDelete() + { + $object = $this->loadObject(); + if (!$this->checkDeletion($object)) { + return false; + } + + return parent::processDelete(); + } + + protected function processBulkDelete() + { + if (is_array($this->boxes) && !empty($this->boxes)) { + foreach ($this->boxes as $id_currency) { + $object = new Currency((int) $id_currency); + if (!$this->checkDeletion($object)) { + return false; + } + } + } + + return parent::processBulkDelete(); + } + + /** + * @see AdminController::processStatus() + */ + public function processStatus() + { + $object = $this->loadObject(); + if (!$this->checkDisableStatus($object)) { + return false; + } + + return parent::processStatus(); + } + + protected function processBulkDisableSelection() + { + if (is_array($this->boxes) && !empty($this->boxes)) { + foreach ($this->boxes as $id_currency) { + $object = new Currency((int) $id_currency); + if (!$this->checkDisableStatus($object)) { + return false; + } + } + } + + return parent::processBulkDisableSelection(); + } + + /** + * Update currency exchange rates. + */ + public function processExchangeRates() + { + if (!$this->errors = Currency::refreshCurrencies()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=6&token=' . $this->token); + } + } + + /** + * @see AdminController::initProcess() + */ + public function initProcess() + { + if (Tools::isSubmit('SubmitExchangesRates')) { + if ($this->access('edit')) { + $this->action = 'exchangeRates'; + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } + if (Tools::isSubmit('submitAddcurrency') && !Tools::getValue('id_currency') && Currency::exists(Tools::getValue('iso_code'))) { + $this->errors[] = $this->trans('This currency already exists.', array(), 'Admin.International.Notification'); + } + if (Tools::isSubmit('submitAddcurrency') && (float) Tools::getValue('conversion_rate') <= 0) { + $this->errors[] = $this->trans('The currency conversion rate cannot be equal to 0.', array(), 'Admin.International.Notification'); + } + parent::initProcess(); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_currency'] = array( + 'href' => self::$currentIndex . '&addcurrency&token=' . $this->token, + 'desc' => $this->trans('Add new currency', array(), 'Admin.International.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function ajaxProcessCronjobLiveExchangeRate() + { + $moduleManagerBuilder = ModuleManagerBuilder::getInstance(); + $moduleManager = $moduleManagerBuilder->build(); + + if (!$moduleManager->isInstalled('cronjobs')) { + die(json_encode(array())); + } + + $enable = (int) Tools::getValue('enable'); + $config = Configuration::get('PS_ACTIVE_CRONJOB_EXCHANGE_RATE', null, null, $this->context->shop->id); + $cronJobUrl = 'http://' . ShopUrl::getMainShopDomain($this->context->shop->id) . __PS_BASE_URI__ . basename(_PS_ADMIN_DIR_) . '/cron_currency_rates.php?secure_key=' . md5(_COOKIE_KEY_ . Configuration::get('PS_SHOP_NAME')); + + if ($config && $enable == 0) { + Configuration::updateValue('PS_ACTIVE_CRONJOB_EXCHANGE_RATE', 0, false, null, $this->context->shop->id); + Db::getInstance()->execute('DELETE FROM ' . _DB_PREFIX_ . 'cronjobs WHERE `id_cronjob` = \'' . (int) $config . '\''); + } + + //The cronjob is not defined, create it + if ($enable == 1 && (!$config || $config == 0)) { + $cronJobs = new CronJobs(); + $cronJobs->addOneShotTask( + $cronJobUrl, + $this->trans('Live exchange Rate for %shop_name%', array('%shop_name%' => Configuration::get('PS_SHOP_NAME')), 'Admin.International.Feature') + ); + + Configuration::updateValue('PS_ACTIVE_CRONJOB_EXCHANGE_RATE', Db::getInstance()->Insert_ID(), false, null, $this->context->shop->id); + } else { + $cronJob = Db::getInstance()->executeS('SELECT * FROM ' . _DB_PREFIX_ . 'cronjobs WHERE `id_cronjob` = \'' . (int) $config . '\''); + + //if cronjob do not exsit anymore OR cronjob dis disabled => disable conf + if (!$cronJob || $cronJob[0]['active'] == 0) { + Configuration::updateValue('PS_ACTIVE_CRONJOB_EXCHANGE_RATE', 0, false, null, $this->context->shop->id); + } + } + + die(json_encode(array())); + } +} diff --git a/controllers/admin/AdminCustomerThreadsController.php b/controllers/admin/AdminCustomerThreadsController.php new file mode 100644 index 00000000..8c17bbf5 --- /dev/null +++ b/controllers/admin/AdminCustomerThreadsController.php @@ -0,0 +1,1199 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property CustomerThread $object + */ +class AdminCustomerThreadsControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->context = Context::getContext(); + $this->table = 'customer_thread'; + $this->className = 'CustomerThread'; + $this->lang = false; + + $contact_array = array(); + $contacts = Contact::getContacts($this->context->language->id); + + foreach ($contacts as $contact) { + $contact_array[$contact['id_contact']] = $contact['name']; + } + + $language_array = array(); + $languages = Language::getLanguages(); + foreach ($languages as $language) { + $language_array[$language['id_lang']] = $language['name']; + } + + parent::__construct(); + + $icon_array = array( + 'open' => array('class' => 'icon-circle text-success', 'alt' => $this->trans('Open', array(), 'Admin.Catalog.Feature')), + 'closed' => array('class' => 'icon-circle text-danger', 'alt' => $this->trans('Closed', array(), 'Admin.Catalog.Feature')), + 'pending1' => array('class' => 'icon-circle text-warning', 'alt' => $this->trans('Pending 1', array(), 'Admin.Catalog.Feature')), + 'pending2' => array('class' => 'icon-circle text-warning', 'alt' => $this->trans('Pending 2', array(), 'Admin.Catalog.Feature')), + ); + + $status_array = array(); + foreach ($icon_array as $k => $v) { + $status_array[$k] = $v['alt']; + } + + $this->fields_list = array( + 'id_customer_thread' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'customer' => array( + 'title' => $this->trans('Customer', array(), 'Admin.Global'), + 'filter_key' => 'customer', + 'tmpTableFilter' => true, + ), + 'email' => array( + 'title' => $this->trans('Email', array(), 'Admin.Global'), + 'filter_key' => 'a!email', + ), + 'contact' => array( + 'title' => $this->trans('Type', array(), 'Admin.Catalog.Feature'), + 'type' => 'select', + 'list' => $contact_array, + 'filter_key' => 'cl!id_contact', + 'filter_type' => 'int', + ), + 'language' => array( + 'title' => $this->trans('Language', array(), 'Admin.Global'), + 'type' => 'select', + 'list' => $language_array, + 'filter_key' => 'l!id_lang', + 'filter_type' => 'int', + ), + 'status' => array( + 'title' => $this->trans('Status', array(), 'Admin.Global'), + 'type' => 'select', + 'list' => $status_array, + 'icon' => $icon_array, + 'align' => 'center', + 'filter_key' => 'a!status', + 'filter_type' => 'string', + ), + 'employee' => array( + 'title' => $this->trans('Employee', array(), 'Admin.Global'), + 'filter_key' => 'employee', + 'tmpTableFilter' => true, + ), + 'messages' => array( + 'title' => $this->trans('Messages', array(), 'Admin.Catalog.Feature'), + 'filter_key' => 'messages', + 'tmpTableFilter' => true, + 'maxlength' => 40, + ), + 'private' => array( + 'title' => $this->trans('Private', array(), 'Admin.Catalog.Feature'), + 'type' => 'select', + 'filter_key' => 'private', + 'align' => 'center', + 'cast' => 'intval', + 'callback' => 'printOptinIcon', + 'list' => array( + '0' => $this->trans('No', array(), 'Admin.Global'), + '1' => $this->trans('Yes', array(), 'Admin.Global'), + ), + ), + 'date_upd' => array( + 'title' => $this->trans('Last message', array(), 'Admin.Catalog.Feature'), + 'havingFilter' => true, + 'type' => 'datetime', + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->shopLinkType = 'shop'; + + $this->fields_options = array( + 'contact' => array( + 'title' => $this->trans('Contact options', array(), 'Admin.Catalog.Feature'), + 'fields' => array( + 'PS_CUSTOMER_SERVICE_FILE_UPLOAD' => array( + 'title' => $this->trans('Allow file uploading', array(), 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Allow customers to upload files using the contact page.', array(), 'Admin.Catalog.Help'), + 'type' => 'bool', + ), + 'PS_CUSTOMER_SERVICE_SIGNATURE' => array( + 'title' => $this->trans('Default message', array(), 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Please fill out the message fields that appear by default when you answer a thread on the customer service page.', array(), 'Admin.Catalog.Help'), + 'type' => 'textareaLang', + 'lang' => true, + ), + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ), + 'general' => array( + 'title' => $this->trans('Customer service options', array(), 'Admin.Catalog.Feature'), + 'fields' => array( + 'PS_SAV_IMAP_URL' => array( + 'title' => $this->trans('IMAP URL', array(), 'Admin.Catalog.Feature'), + 'hint' => $this->trans('URL for your IMAP server (ie.: mail.server.com).', array(), 'Admin.Catalog.Help'), + 'type' => 'text', + 'validation' => 'isValidImapUrl', + ), + 'PS_SAV_IMAP_PORT' => array( + 'title' => $this->trans('IMAP port', array(), 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Port to use to connect to your IMAP server.', array(), 'Admin.Catalog.Help'), + 'type' => 'text', + 'defaultValue' => 143, + ), + 'PS_SAV_IMAP_USER' => array( + 'title' => $this->trans('IMAP user', array(), 'Admin.Catalog.Feature'), + 'hint' => $this->trans('User to use to connect to your IMAP server.', array(), 'Admin.Catalog.Help'), + 'type' => 'text', + ), + 'PS_SAV_IMAP_PWD' => array( + 'title' => $this->trans('IMAP password', array(), 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Password to use to connect your IMAP server.', array(), 'Admin.Catalog.Help'), + 'type' => 'text', + ), + 'PS_SAV_IMAP_DELETE_MSG' => array( + 'title' => $this->trans('Delete messages', array(), 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Delete messages after synchronization. If you do not enable this option, the synchronization will take more time.', array(), 'Admin.Catalog.Help'), + 'type' => 'bool', + ), + 'PS_SAV_IMAP_CREATE_THREADS' => array( + 'title' => $this->trans('Create new threads', array(), 'Admin.Catalog.Feature'), + 'hint' => $this->trans('Create new threads for unrecognized emails.', array(), 'Admin.Catalog.Help'), + 'type' => 'bool', + ), + 'PS_SAV_IMAP_OPT_POP3' => array( + 'title' => $this->trans('IMAP options', array(), 'Admin.Catalog.Feature') . ' (/pop3)', + 'hint' => $this->trans('Use POP3 instead of IMAP.', array(), 'Admin.Catalog.Help'), + 'type' => 'bool', + ), + 'PS_SAV_IMAP_OPT_NORSH' => array( + 'title' => $this->trans('IMAP options', array(), 'Admin.Catalog.Feature') . ' (/norsh)', + 'type' => 'bool', + 'hint' => $this->trans('Do not use RSH or SSH to establish a preauthenticated IMAP sessions.', array(), 'Admin.Catalog.Help'), + ), + 'PS_SAV_IMAP_OPT_SSL' => array( + 'title' => $this->trans('IMAP options', array(), 'Admin.Catalog.Feature') . ' (/ssl)', + 'type' => 'bool', + 'hint' => $this->trans('Use the Secure Socket Layer (TLS/SSL) to encrypt the session.', array(), 'Admin.Catalog.Help'), + ), + 'PS_SAV_IMAP_OPT_VALIDATE-CERT' => array( + 'title' => $this->trans('IMAP options', array(), 'Admin.Catalog.Feature') . ' (/validate-cert)', + 'type' => 'bool', + 'hint' => $this->trans('Validate certificates from the TLS/SSL server.', array(), 'Admin.Catalog.Help'), + ), + 'PS_SAV_IMAP_OPT_NOVALIDATE-CERT' => array( + 'title' => $this->trans('IMAP options', array(), 'Admin.Catalog.Feature') . ' (/novalidate-cert)', + 'type' => 'bool', + 'hint' => $this->trans('Do not validate certificates from the TLS/SSL server. This is only needed if a server uses self-signed certificates.', array(), 'Admin.Catalog.Help'), + ), + 'PS_SAV_IMAP_OPT_TLS' => array( + 'title' => $this->trans('IMAP options', array(), 'Admin.Catalog.Feature') . ' (/tls)', + 'type' => 'bool', + 'hint' => $this->trans('Force use of start-TLS to encrypt the session, and reject connection to servers that do not support it.', array(), 'Admin.Catalog.Help'), + ), + 'PS_SAV_IMAP_OPT_NOTLS' => array( + 'title' => $this->trans('IMAP options', array(), 'Admin.Catalog.Feature') . ' (/notls)', + 'type' => 'bool', + 'hint' => $this->trans('Do not use start-TLS to encrypt the session, even with servers that support it.', array(), 'Admin.Catalog.Help'), + ), + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ), + ); + } + + public function renderList() + { + // Check the new IMAP messages before rendering the list + $this->renderProcessSyncImap(); + + $this->addRowAction('view'); + $this->addRowAction('delete'); + + $this->_select = ' + CONCAT(c.`firstname`," ",c.`lastname`) as customer, cl.`name` as contact, l.`name` as language, group_concat(message) as messages, cm.private, + ( + SELECT IFNULL(CONCAT(LEFT(e.`firstname`, 1),". ",e.`lastname`), "--") + FROM `' . _DB_PREFIX_ . 'customer_message` cm2 + INNER JOIN ' . _DB_PREFIX_ . 'employee e + ON e.`id_employee` = cm2.`id_employee` + WHERE cm2.id_employee > 0 + AND cm2.`id_customer_thread` = a.`id_customer_thread` + ORDER BY cm2.`date_add` DESC LIMIT 1 + ) as employee'; + + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'customer` c + ON c.`id_customer` = a.`id_customer` + LEFT JOIN `' . _DB_PREFIX_ . 'customer_message` cm + ON cm.`id_customer_thread` = a.`id_customer_thread` + LEFT JOIN `' . _DB_PREFIX_ . 'lang` l + ON l.`id_lang` = a.`id_lang` + LEFT JOIN `' . _DB_PREFIX_ . 'contact_lang` cl + ON (cl.`id_contact` = a.`id_contact` AND cl.`id_lang` = ' . (int) $this->context->language->id . ')'; + + if ($id_order = Tools::getValue('id_order')) { + $this->_where .= ' AND id_order = ' . (int) $id_order; + } + + $this->_group = 'GROUP BY cm.id_customer_thread'; + $this->_orderBy = 'id_customer_thread'; + $this->_orderWay = 'DESC'; + + $contacts = CustomerThread::getContacts(); + + $categories = Contact::getCategoriesContacts(); + + $params = array( + $this->trans('Total threads', array(), 'Admin.Catalog.Feature') => $all = CustomerThread::getTotalCustomerThreads(), + $this->trans('Threads pending', array(), 'Admin.Catalog.Feature') => $pending = CustomerThread::getTotalCustomerThreads('status LIKE "%pending%"'), + $this->trans('Total number of customer messages', array(), 'Admin.Catalog.Feature') => CustomerMessage::getTotalCustomerMessages('id_employee = 0'), + $this->trans('Total number of employee messages', array(), 'Admin.Catalog.Feature') => CustomerMessage::getTotalCustomerMessages('id_employee != 0'), + $this->trans('Unread threads', array(), 'Admin.Catalog.Feature') => $unread = CustomerThread::getTotalCustomerThreads('status = "open"'), + $this->trans('Closed threads', array(), 'Admin.Catalog.Feature') => $all - ($unread + $pending), + ); + + $this->tpl_list_vars = array( + 'contacts' => $contacts, + 'categories' => $categories, + 'params' => $params, + ); + + return parent::renderList(); + } + + public function initToolbar() + { + parent::initToolbar(); + unset($this->toolbar_btn['new']); + } + + public function printOptinIcon($value, $customer) + { + return $value ? '' : ''; + } + + public function postProcess() + { + if ($id_customer_thread = (int) Tools::getValue('id_customer_thread')) { + if (($id_contact = (int) Tools::getValue('id_contact'))) { + $result = Db::getInstance()->execute(' + UPDATE ' . _DB_PREFIX_ . 'customer_thread + SET id_contact = ' . $id_contact . ' + WHERE id_customer_thread = ' . $id_customer_thread + ); + if ($result) { + $this->object->id_contact = $id_contact; + } + } + if ($id_status = (int) Tools::getValue('setstatus')) { + $status_array = array(1 => 'open', 2 => 'closed', 3 => 'pending1', 4 => 'pending2'); + $result = Db::getInstance()->execute(' + UPDATE ' . _DB_PREFIX_ . 'customer_thread + SET status = "' . $status_array[$id_status] . '" + WHERE id_customer_thread = ' . $id_customer_thread . ' LIMIT 1 + '); + if ($result) { + $this->object->status = $status_array[$id_status]; + } + } + if (isset($_POST['id_employee_forward'])) { + $messages = Db::getInstance()->getRow(' + SELECT ct.*, cm.*, cl.name subject, CONCAT(e.firstname, \' \', e.lastname) employee_name, + CONCAT(c.firstname, \' \', c.lastname) customer_name, c.firstname + FROM ' . _DB_PREFIX_ . 'customer_thread ct + LEFT JOIN ' . _DB_PREFIX_ . 'customer_message cm + ON (ct.id_customer_thread = cm.id_customer_thread) + LEFT JOIN ' . _DB_PREFIX_ . 'contact_lang cl + ON (cl.id_contact = ct.id_contact AND cl.id_lang = ' . (int) $this->context->language->id . ') + LEFT OUTER JOIN ' . _DB_PREFIX_ . 'employee e + ON e.id_employee = cm.id_employee + LEFT OUTER JOIN ' . _DB_PREFIX_ . 'customer c + ON (c.email = ct.email) + WHERE ct.id_customer_thread = ' . (int) Tools::getValue('id_customer_thread') . ' + ORDER BY cm.date_add DESC + '); + $output = $this->displayMessage($messages, true, (int) Tools::getValue('id_employee_forward')); + $cm = new CustomerMessage(); + $cm->id_employee = (int) $this->context->employee->id; + $cm->id_customer_thread = (int) Tools::getValue('id_customer_thread'); + $cm->ip_address = (int) ip2long(Tools::getRemoteAddr()); + $current_employee = $this->context->employee; + $id_employee = (int) Tools::getValue('id_employee_forward'); + $employee = new Employee($id_employee); + $email = Tools::getValue('email'); + $message = Tools::getValue('message_forward'); + if (($error = $cm->validateField('message', $message, null, array(), true)) !== true) { + $this->errors[] = $error; + } elseif ($id_employee && $employee && Validate::isLoadedObject($employee)) { + $params = array( + '{messages}' => stripslashes($output), + '{employee}' => $current_employee->firstname . ' ' . $current_employee->lastname, + '{comment}' => stripslashes(Tools::nl2br($_POST['message_forward'])), + '{firstname}' => $employee->firstname, + '{lastname}' => $employee->lastname, + ); + + if (Mail::Send( + $this->context->language->id, + 'forward_msg', + $this->trans( + 'Fwd: Customer message', + array(), + 'Emails.Subject', + $this->context->language->locale + ), + $params, + $employee->email, + $employee->firstname . ' ' . $employee->lastname, + $current_employee->email, + $current_employee->firstname . ' ' . $current_employee->lastname, + null, null, _PS_MAIL_DIR_, true)) { + $cm->private = 1; + $cm->message = $this->trans('Message forwarded to', array(), 'Admin.Catalog.Feature') . ' ' . $employee->firstname . ' ' . $employee->lastname . "\n" . $this->trans('Comment:') . ' ' . $message; + $cm->add(); + } + } elseif ($email && Validate::isEmail($email)) { + $params = array( + '{messages}' => Tools::nl2br(stripslashes($output)), + '{employee}' => $current_employee->firstname . ' ' . $current_employee->lastname, + '{comment}' => stripslashes($_POST['message_forward']), + '{firstname}' => '', + '{lastname}' => '', + ); + + if (Mail::Send( + $this->context->language->id, + 'forward_msg', + $this->trans( + 'Fwd: Customer message', + array(), + 'Emails.Subject', + $this->context->language->locale + ), + $params, $email, null, + $current_employee->email, $current_employee->firstname . ' ' . $current_employee->lastname, + null, null, _PS_MAIL_DIR_, true)) { + $cm->message = $this->trans('Message forwarded to', array(), 'Admin.Catalog.Feature') . ' ' . $email . "\n" . $this->trans('Comment:') . ' ' . $message; + $cm->add(); + } + } else { + $this->errors[] = '
' . $this->trans('The email address is invalid.', array(), 'Admin.Notifications.Error') . '
'; + } + } + if (Tools::isSubmit('submitReply')) { + $ct = new CustomerThread($id_customer_thread); + + ShopUrl::cacheMainDomainForShop((int) $ct->id_shop); + + $cm = new CustomerMessage(); + $cm->id_employee = (int) $this->context->employee->id; + $cm->id_customer_thread = $ct->id; + $cm->ip_address = (int) ip2long(Tools::getRemoteAddr()); + $cm->message = Tools::getValue('reply_message'); + if (($error = $cm->validateField('message', $cm->message, null, array(), true)) !== true) { + $this->errors[] = $error; + } elseif (isset($_FILES) && !empty($_FILES['joinFile']['name']) && $_FILES['joinFile']['error'] != 0) { + $this->errors[] = $this->trans('An error occurred during the file upload process.', array(), 'Admin.Notifications.Error'); + } elseif ($cm->add()) { + $file_attachment = null; + if (!empty($_FILES['joinFile']['name'])) { + $file_attachment['content'] = file_get_contents($_FILES['joinFile']['tmp_name']); + $file_attachment['name'] = $_FILES['joinFile']['name']; + $file_attachment['mime'] = $_FILES['joinFile']['type']; + } + $customer = new Customer($ct->id_customer); + $params = array( + '{reply}' => Tools::nl2br(Tools::getValue('reply_message')), + '{link}' => Tools::url( + $this->context->link->getPageLink('contact', true, null, null, false, $ct->id_shop), + 'id_customer_thread=' . (int) $ct->id . '&token=' . $ct->token + ), + '{firstname}' => $customer->firstname, + '{lastname}' => $customer->lastname, + ); + //#ct == id_customer_thread #tc == token of thread <== used in the synchronization imap + $contact = new Contact((int) $ct->id_contact, (int) $ct->id_lang); + + if (Validate::isLoadedObject($contact)) { + $from_name = $contact->name; + $from_email = $contact->email; + } else { + $from_name = null; + $from_email = null; + } + + $language = new Language((int) $ct->id_lang); + + if (Mail::Send( + (int) $ct->id_lang, + 'reply_msg', + $this->trans( + 'An answer to your message is available #ct%thread_id% #tc%thread_token%', + array( + '%thread_id%' => $ct->id, + '%thread_token%' => $ct->token, + ), + 'Emails.Subject', + $language->locale + ), + $params, Tools::getValue('msg_email'), null, $from_email, $from_name, $file_attachment, null, + _PS_MAIL_DIR_, true, $ct->id_shop)) { + $ct->status = 'closed'; + $ct->update(); + } + Tools::redirectAdmin( + self::$currentIndex . '&id_customer_thread=' . (int) $id_customer_thread . '&viewcustomer_thread&token=' . Tools::getValue('token') + ); + } else { + $this->errors[] = $this->trans('An error occurred. Your message was not sent. Please contact your system administrator.', array(), 'Admin.Orderscustomers.Notification'); + } + } + } + + return parent::postProcess(); + } + + public function initContent() + { + if (isset($_GET['filename']) && file_exists(_PS_UPLOAD_DIR_ . $_GET['filename']) && Validate::isFileName($_GET['filename'])) { + AdminCustomerThreadsController::openUploadedFile(); + } + + return parent::initContent(); + } + + protected function openUploadedFile() + { + $filename = $_GET['filename']; + + $extensions = array( + '.txt' => 'text/plain', + '.rtf' => 'application/rtf', + '.doc' => 'application/msword', + '.docx' => 'application/msword', + '.pdf' => 'application/pdf', + '.zip' => 'multipart/x-zip', + '.png' => 'image/png', + '.jpeg' => 'image/jpeg', + '.gif' => 'image/gif', + '.jpg' => 'image/jpeg', + ); + + $extension = false; + foreach ($extensions as $key => $val) { + if (substr(Tools::strtolower($filename), -4) == $key || substr(Tools::strtolower($filename), -5) == $key) { + $extension = $val; + break; + } + } + + if (!$extension || !Validate::isFileName($filename)) { + die(Tools::displayError()); + } + + if (ob_get_level() && ob_get_length() > 0) { + ob_end_clean(); + } + header('Content-Type: ' . $extension); + header('Content-Disposition:attachment;filename="' . $filename . '"'); + readfile(_PS_UPLOAD_DIR_ . $filename); + die; + } + + public function renderKpis() + { + $time = time(); + $kpis = array(); + + /* The data generation is located in AdminStatsControllerCore */ + + $helper = new HelperKpi(); + $helper->id = 'box-pending-messages'; + $helper->icon = 'icon-envelope'; + $helper->color = 'color1'; + $helper->href = $this->context->link->getAdminLink('AdminCustomerThreads'); + $helper->title = $this->trans('Pending Discussion Threads', array(), 'Admin.Catalog.Feature'); + if (ConfigurationKPI::get('PENDING_MESSAGES') !== false) { + $helper->value = ConfigurationKPI::get('PENDING_MESSAGES'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=pending_messages'; + $helper->refresh = (bool) (ConfigurationKPI::get('PENDING_MESSAGES_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-age'; + $helper->icon = 'icon-time'; + $helper->color = 'color2'; + $helper->title = $this->trans('Average Response Time', array(), 'Admin.Catalog.Feature'); + $helper->subtitle = $this->trans('30 days', array(), 'Admin.Global'); + if (ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME') !== false) { + $helper->value = ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=avg_msg_response_time'; + $helper->refresh = (bool) (ConfigurationKPI::get('AVG_MSG_RESPONSE_TIME_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-messages-per-thread'; + $helper->icon = 'icon-copy'; + $helper->color = 'color3'; + $helper->title = $this->trans('Messages per Thread', array(), 'Admin.Catalog.Feature'); + $helper->subtitle = $this->trans('30 day', array(), 'Admin.Global'); + if (ConfigurationKPI::get('MESSAGES_PER_THREAD') !== false) { + $helper->value = ConfigurationKPI::get('MESSAGES_PER_THREAD'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=messages_per_thread'; + $helper->refresh = (bool) (ConfigurationKPI::get('MESSAGES_PER_THREAD_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpiRow(); + $helper->kpis = $kpis; + + return $helper->generate(); + } + + public function renderView() + { + if (!$id_customer_thread = (int) Tools::getValue('id_customer_thread')) { + return; + } + + $this->context = Context::getContext(); + if (!($thread = $this->loadObject())) { + return; + } + $this->context->cookie->{'customer_threadFilter_cl!id_contact'} = $thread->id_contact; + + $employees = Employee::getEmployees(); + + $messages = CustomerThread::getMessageCustomerThreads($id_customer_thread); + + foreach ($messages as $key => $mess) { + if ($mess['id_employee']) { + $employee = new Employee($mess['id_employee']); + $messages[$key]['employee_image'] = $employee->getImage(); + } + if (isset($mess['file_name']) && $mess['file_name'] != '') { + $messages[$key]['file_name'] = _THEME_PROD_PIC_DIR_ . $mess['file_name']; + } else { + unset($messages[$key]['file_name']); + } + + if ($mess['id_product']) { + $product = new Product((int) $mess['id_product'], false, $this->context->language->id); + if (Validate::isLoadedObject($product)) { + $messages[$key]['product_name'] = $product->name; + $messages[$key]['product_link'] = $this->context->link->getAdminLink('AdminProducts') . '&updateproduct&id_product=' . (int) $product->id; + } + } + } + + $next_thread = CustomerThread::getNextThread((int) $thread->id); + + $contacts = Contact::getContacts($this->context->language->id); + + $actions = array(); + + if ($next_thread) { + $next_thread = array( + 'href' => self::$currentIndex . '&id_customer_thread=' . (int) $next_thread . '&viewcustomer_thread&token=' . $this->token, + 'name' => $this->trans('Reply to the next unanswered message in this thread', array(), 'Admin.Catalog.Feature'), + ); + } + + if ($thread->status != 'closed') { + $actions['closed'] = array( + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=2&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Mark as "handled"', array(), 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 2, + ); + } else { + $actions['open'] = array( + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=1&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Re-open', array(), 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 1, + ); + } + + if ($thread->status != 'pending1') { + $actions['pending1'] = array( + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=3&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Mark as "pending 1" (will be answered later)', array(), 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 3, + ); + } else { + $actions['pending1'] = array( + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=1&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Disable pending status', array(), 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 1, + ); + } + + if ($thread->status != 'pending2') { + $actions['pending2'] = array( + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=4&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Mark as "pending 2" (will be answered later)', array(), 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 4, + ); + } else { + $actions['pending2'] = array( + 'href' => self::$currentIndex . '&viewcustomer_thread&setstatus=1&id_customer_thread=' . (int) Tools::getValue('id_customer_thread') . '&viewmsg&token=' . $this->token, + 'label' => $this->trans('Disable pending status', array(), 'Admin.Catalog.Feature'), + 'name' => 'setstatus', + 'value' => 1, + ); + } + + if ($thread->id_customer) { + $customer = new Customer($thread->id_customer); + $orders = Order::getCustomerOrders($customer->id); + if ($orders && count($orders)) { + $total_ok = 0; + $orders_ok = array(); + foreach ($orders as $key => $order) { + if ($order['valid']) { + $orders_ok[] = $order; + $total_ok += $order['total_paid_real'] / $order['conversion_rate']; + } + $orders[$key]['date_add'] = Tools::displayDate($order['date_add']); + $orders[$key]['total_paid_real'] = Tools::displayPrice($order['total_paid_real'], new Currency((int) $order['id_currency'])); + } + } + + $products = $customer->getBoughtProducts(); + if ($products && count($products)) { + foreach ($products as $key => $product) { + $products[$key]['date_add'] = Tools::displayDate($product['date_add'], null, true); + } + } + } + $timeline_items = $this->getTimeline($messages, $thread->id_order); + $first_message = $messages[0]; + + if (!$messages[0]['id_employee']) { + unset($messages[0]); + } + + $contact = ''; + foreach ($contacts as $c) { + if ($c['id_contact'] == $thread->id_contact) { + $contact = $c['name']; + } + } + + $this->tpl_view_vars = array( + 'id_customer_thread' => $id_customer_thread, + 'thread' => $thread, + 'actions' => $actions, + 'employees' => $employees, + 'current_employee' => $this->context->employee, + 'messages' => $messages, + 'first_message' => $first_message, + 'contact' => $contact, + 'next_thread' => $next_thread, + 'orders' => isset($orders) ? $orders : false, + 'customer' => isset($customer) ? $customer : false, + 'products' => isset($products) ? $products : false, + 'total_ok' => isset($total_ok) ? Tools::displayPrice($total_ok, $this->context->currency) : false, + 'orders_ok' => isset($orders_ok) ? $orders_ok : false, + 'count_ok' => isset($orders_ok) ? count($orders_ok) : false, + 'PS_CUSTOMER_SERVICE_SIGNATURE' => str_replace('\r\n', "\n", Configuration::get('PS_CUSTOMER_SERVICE_SIGNATURE', (int) $thread->id_lang)), + 'timeline_items' => $timeline_items, + ); + + if ($next_thread) { + $this->tpl_view_vars['next_thread'] = $next_thread; + } + + return parent::renderView(); + } + + public function getTimeline($messages, $id_order) + { + $timeline = array(); + foreach ($messages as $message) { + $product = new Product((int) $message['id_product'], false, $this->context->language->id); + $link_product = $this->context->link->getAdminLink('AdminOrders') . '&vieworder&id_order=' . (int) $product->id; + + $content = ''; + if (!$message['private']) { + $content .= $this->trans('Message to: ', array(), 'Admin.Catalog.Feature') . ' ' . (!$message['id_employee'] ? $message['subject'] : $message['customer_name']) . '
'; + } + if (Validate::isLoadedObject($product)) { + $content .= '
' . $this->trans('Product: ', array(), 'Admin.Catalog.Feature') . '' . $product->name . '

'; + } + $content .= Tools::safeOutput($message['message']); + + $timeline[$message['date_add']][] = array( + 'arrow' => 'left', + 'background_color' => '', + 'icon' => 'icon-envelope', + 'content' => $content, + 'date' => $message['date_add'], + ); + } + + $order = new Order((int) $id_order); + if (Validate::isLoadedObject($order)) { + $order_history = $order->getHistory($this->context->language->id); + foreach ($order_history as $history) { + $link_order = $this->context->link->getAdminLink('AdminOrders') . '&vieworder&id_order=' . (int) $order->id; + + $content = '' . $this->trans('Order', array(), 'Admin.Global') . ' #' . (int) $order->id . '

'; + + $content .= '' . $this->trans('Status:', array(), 'Admin.Catalog.Feature') . ' ' . $history['ostate_name'] . ''; + + $timeline[$history['date_add']][] = array( + 'arrow' => 'right', + 'alt' => true, + 'background_color' => $history['color'], + 'icon' => 'icon-credit-card', + 'content' => $content, + 'date' => $history['date_add'], + 'see_more_link' => $link_order, + ); + } + } + krsort($timeline); + + return $timeline; + } + + protected function displayMessage($message, $email = false, $id_employee = null) + { + $tpl = $this->createTemplate('message.tpl'); + + $contacts = Contact::getContacts($this->context->language->id); + foreach ($contacts as $contact) { + $contact_array[$contact['id_contact']] = array('id_contact' => $contact['id_contact'], 'name' => $contact['name']); + } + $contacts = $contact_array; + + if (!$email) { + if (!empty($message['id_product']) && empty($message['employee_name'])) { + $id_order_product = Order::getIdOrderProduct((int) $message['id_customer'], (int) $message['id_product']); + } + } + $message['date_add'] = Tools::displayDate($message['date_add'], null, true); + $message['user_agent'] = strip_tags($message['user_agent']); + $message['message'] = preg_replace( + '/(https?:\/\/[a-z0-9#%&_=\(\)\.\? \+\-@\/]{6,1000})([\s\n<])/Uui', + '\1\2', + html_entity_decode($message['message'], + ENT_QUOTES, 'UTF-8') + ); + + $is_valid_order_id = true; + $order = new Order((int) $message['id_order']); + + if (!Validate::isLoadedObject($order)) { + $is_valid_order_id = false; + } + + $tpl->assign(array( + 'thread_url' => Tools::getAdminUrl(basename(_PS_ADMIN_DIR_) . '/' . + $this->context->link->getAdminLink('AdminCustomerThreads') . '&id_customer_thread=' + . (int) $message['id_customer_thread'] . '&viewcustomer_thread=1'), + 'link' => Context::getContext()->link, + 'current' => self::$currentIndex, + 'token' => $this->token, + 'message' => $message, + 'id_order_product' => isset($id_order_product) ? $id_order_product : null, + 'email' => $email, + 'id_employee' => $id_employee, + 'PS_SHOP_NAME' => Configuration::get('PS_SHOP_NAME'), + 'file_name' => file_exists(_PS_UPLOAD_DIR_ . $message['file_name']), + 'contacts' => $contacts, + 'is_valid_order_id' => $is_valid_order_id, + )); + + return $tpl->fetch(); + } + + protected function displayButton($content) + { + return '

' . $content . '

'; + } + + public function renderOptions() + { + if (Configuration::get('PS_SAV_IMAP_URL') + && Configuration::get('PS_SAV_IMAP_PORT') + && Configuration::get('PS_SAV_IMAP_USER') + && Configuration::get('PS_SAV_IMAP_PWD')) { + $this->tpl_option_vars['use_sync'] = true; + } else { + $this->tpl_option_vars['use_sync'] = false; + } + + return parent::renderOptions(); + } + + /** + * AdminController::getList() override. + * + * @see AdminController::getList() + * + * @param int $id_lang + * @param string|null $order_by + * @param string|null $order_way + * @param int $start + * @param int|null $limit + * @param int|bool $id_lang_shop + * + * @throws PrestaShopException + */ + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + $nb_items = count($this->_list); + for ($i = 0; $i < $nb_items; ++$i) { + if (isset($this->_list[$i]['messages'])) { + $this->_list[$i]['messages'] = Tools::htmlentitiesDecodeUTF8($this->_list[$i]['messages']); + } + } + } + + public function updateOptionPsSavImapOpt($value) + { + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error')); + } + + if (!$this->errors && $value) { + Configuration::updateValue('PS_SAV_IMAP_OPT', implode('', $value)); + } + } + + public function ajaxProcessMarkAsRead() + { + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error')); + } + + $id_thread = Tools::getValue('id_thread'); + $messages = CustomerThread::getMessageCustomerThreads($id_thread); + if (count($messages)) { + Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'customer_message` set `read` = 1 WHERE `id_employee` = ' . (int) $this->context->employee->id . ' AND `id_customer_thread` = ' . (int) $id_thread); + } + } + + /** + * Call the IMAP synchronization during an AJAX process. + * + * @throws PrestaShopException + */ + public function ajaxProcessSyncImap() + { + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error')); + } + + if (Tools::isSubmit('syncImapMail')) { + die(json_encode($this->syncImap())); + } + } + + /** + * Call the IMAP synchronization during the render process. + */ + public function renderProcessSyncImap() + { + // To avoid an error if the IMAP isn't configured, we check the configuration here, like during + // the synchronization. All parameters will exists. + if (!(Configuration::get('PS_SAV_IMAP_URL') + || Configuration::get('PS_SAV_IMAP_PORT') + || Configuration::get('PS_SAV_IMAP_USER') + || Configuration::get('PS_SAV_IMAP_PWD'))) { + return; + } + + // Executes the IMAP synchronization. + $sync_errors = $this->syncImap(); + + // Show the errors. + if (isset($sync_errors['hasError']) && $sync_errors['hasError']) { + if (isset($sync_errors['errors'])) { + foreach ($sync_errors['errors'] as &$error) { + $this->displayWarning($error); + } + } + } + } + + /** + * Imap synchronization method. + * + * @return array errors list + */ + public function syncImap() + { + if (!($url = Configuration::get('PS_SAV_IMAP_URL')) + || !($port = Configuration::get('PS_SAV_IMAP_PORT')) + || !($user = Configuration::get('PS_SAV_IMAP_USER')) + || !($password = Configuration::get('PS_SAV_IMAP_PWD'))) { + return array('hasError' => true, 'errors' => array('IMAP configuration is not correct')); + } + + $conf = Configuration::getMultiple(array( + 'PS_SAV_IMAP_OPT_POP3', 'PS_SAV_IMAP_OPT_NORSH', 'PS_SAV_IMAP_OPT_SSL', + 'PS_SAV_IMAP_OPT_VALIDATE-CERT', 'PS_SAV_IMAP_OPT_NOVALIDATE-CERT', + 'PS_SAV_IMAP_OPT_TLS', 'PS_SAV_IMAP_OPT_NOTLS', )); + + $conf_str = ''; + if ($conf['PS_SAV_IMAP_OPT_POP3']) { + $conf_str .= '/pop3'; + } + if ($conf['PS_SAV_IMAP_OPT_NORSH']) { + $conf_str .= '/norsh'; + } + if ($conf['PS_SAV_IMAP_OPT_SSL']) { + $conf_str .= '/ssl'; + } + if ($conf['PS_SAV_IMAP_OPT_VALIDATE-CERT']) { + $conf_str .= '/validate-cert'; + } + if ($conf['PS_SAV_IMAP_OPT_NOVALIDATE-CERT']) { + $conf_str .= '/novalidate-cert'; + } + if ($conf['PS_SAV_IMAP_OPT_TLS']) { + $conf_str .= '/tls'; + } + if ($conf['PS_SAV_IMAP_OPT_NOTLS']) { + $conf_str .= '/notls'; + } + + if (!function_exists('imap_open')) { + return array('hasError' => true, 'errors' => array('imap is not installed on this server')); + } + + $mbox = @imap_open('{' . $url . ':' . $port . $conf_str . '}', $user, $password); + + //checks if there is no error when connecting imap server + $errors = imap_errors(); + if (is_array($errors)) { + $errors = array_unique($errors); + } + $str_errors = ''; + $str_error_delete = ''; + + if (count($errors) && is_array($errors)) { + $str_errors = ''; + foreach ($errors as $error) { + $str_errors .= $error . ', '; + } + $str_errors = rtrim(trim($str_errors), ','); + } + //checks if imap connexion is active + if (!$mbox) { + return array('hasError' => true, 'errors' => array('Cannot connect to the mailbox :
' . ($str_errors))); + } + + //Returns information about the current mailbox. Returns FALSE on failure. + $check = imap_check($mbox); + if (!$check) { + return array('hasError' => true, 'errors' => array('Fail to get information about the current mailbox')); + } + + if ($check->Nmsgs == 0) { + return array('hasError' => true, 'errors' => array('NO message to sync')); + } + + $result = imap_fetch_overview($mbox, "1:{$check->Nmsgs}", 0); + $message_errors = array(); + foreach ($result as $overview) { + //check if message exist in database + if (isset($overview->subject)) { + $subject = $overview->subject; + } else { + $subject = ''; + } + //Creating an md5 to check if message has been allready processed + $md5 = md5($overview->date . $overview->from . $subject . $overview->msgno); + $exist = Db::getInstance()->getValue( + 'SELECT `md5_header` + FROM `' . _DB_PREFIX_ . 'customer_message_sync_imap` + WHERE `md5_header` = \'' . pSQL($md5) . '\''); + if ($exist) { + if (Configuration::get('PS_SAV_IMAP_DELETE_MSG')) { + if (!imap_delete($mbox, $overview->msgno)) { + $str_error_delete = ', Fail to delete message'; + } + } + } else { + //check if subject has id_order + preg_match('/\#ct([0-9]*)/', $subject, $matches1); + preg_match('/\#tc([0-9-a-z-A-Z]*)/', $subject, $matches2); + $match_found = false; + if (isset($matches1[1]) && isset($matches2[1])) { + $match_found = true; + } + + $new_ct = (Configuration::get('PS_SAV_IMAP_CREATE_THREADS') && !$match_found && (strpos($subject, '[no_sync]') == false)); + + $fetch_succeed = true; + if ($match_found || $new_ct) { + if ($new_ct) { + // parse from attribute and fix it if needed + $from_parsed = array(); + if (!isset($overview->from) + || (!preg_match('/<(' . Tools::cleanNonUnicodeSupport('[a-z\p{L}0-9!#$%&\'*+\/=?^`{}|~_-]+[.a-z\p{L}0-9!#$%&\'*+\/=?^`{}|~_-]*@[a-z\p{L}0-9]+[._a-z\p{L}0-9-]*\.[a-z0-9]+') . ')>/', $overview->from, $from_parsed) + && !Validate::isEmail($overview->from))) { + $message_errors[] = $this->trans('Cannot create message in a new thread.', array(), 'Admin.Orderscustomers.Notification'); + continue; + } + + // fix email format: from "Mr Sanders " to "sanders@blueforest.com" + $from = $overview->from; + if (isset($from_parsed[1])) { + $from = $from_parsed[1]; + } + + // we want to assign unrecognized mails to the right contact category + $contacts = Contact::getContacts($this->context->language->id); + if (!$contacts) { + continue; + } + + foreach ($contacts as $contact) { + if (isset($overview->to) && strpos($overview->to, $contact['email']) !== false) { + $id_contact = $contact['id_contact']; + } + } + + if (!isset($id_contact)) { // if not use the default contact category + $id_contact = $contacts[0]['id_contact']; + } + + $customer = new Customer(); + $client = $customer->getByEmail($from); //check if we already have a customer with this email + $ct = new CustomerThread(); + if (isset($client->id)) { //if mail is owned by a customer assign to him + $ct->id_customer = $client->id; + } + $ct->email = $from; + $ct->id_contact = $id_contact; + $ct->id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $ct->id_shop = $this->context->shop->id; //new customer threads for unrecognized mails are not shown without shop id + $ct->status = 'open'; + $ct->token = Tools::passwdGen(12); + $ct->add(); + } else { + $ct = new CustomerThread((int) $matches1[1]); + } //check if order exist in database + + if (Validate::isLoadedObject($ct) && ((isset($matches2[1]) && $ct->token == $matches2[1]) || $new_ct)) { + $structure = imap_bodystruct($mbox, $overview->msgno, '1'); + if ($structure->type == 0) { + $message = imap_fetchbody($mbox, $overview->msgno, '1'); + } elseif ($structure->type == 1) { + $structure = imap_bodystruct($mbox, $overview->msgno, '1.1'); + $message = imap_fetchbody($mbox, $overview->msgno, '1.1'); + } else { + continue; + } + + switch ($structure->encoding) { + case 3: + $message = imap_base64($message); + break; + case 4: + $message = imap_qprint($message); + break; + } + $message = iconv($this->getEncoding($structure), 'utf-8', $message); + $message = nl2br($message); + if (!$message || strlen($message) == 0) { + $message_errors[] = $this->trans('The message body is empty, cannot import it.', array(), 'Admin.Orderscustomers.Notification'); + $fetch_succeed = false; + continue; + } + $cm = new CustomerMessage(); + $cm->id_customer_thread = $ct->id; + if (empty($message) || !Validate::isCleanHtml($message)) { + $str_errors .= $this->trans('Invalid message content for subject: %s', array($subject), 'Admin.Orderscustomers.Notification'); + } else { + try { + $cm->message = $message; + $cm->add(); + } catch (PrestaShopException $pse) { + $message_errors[] = $this->trans('The message content is not valid, cannot import it.', array(), 'Admin.Orderscustomers.Notification'); + $fetch_succeed = false; + continue; + } + } + } + } + if ($fetch_succeed) { + Db::getInstance()->execute('INSERT INTO `' . _DB_PREFIX_ . 'customer_message_sync_imap` (`md5_header`) VALUES (\'' . pSQL($md5) . '\')'); + } + } + } + imap_expunge($mbox); + imap_close($mbox); + if (sizeof($message_errors) > 0) { + if (($more_error = $str_errors . $str_error_delete) && strlen($more_error) > 0) { + $message_errors = array_merge(array($more_error), $message_errors); + } + + return array('hasError' => true, 'errors' => $message_errors); + } + if ($str_errors . $str_error_delete) { + return array('hasError' => true, 'errors' => array($str_errors . $str_error_delete)); + } else { + return array('hasError' => false, 'errors' => ''); + } + } + + protected function getEncoding($structure) + { + foreach ($structure->parameters as $parameter) { + if ($parameter->attribute == 'CHARSET') { + return $parameter->value; + } + } + + return 'utf-8'; + } +} diff --git a/controllers/admin/AdminCustomersController.php b/controllers/admin/AdminCustomersController.php new file mode 100644 index 00000000..b35410a3 --- /dev/null +++ b/controllers/admin/AdminCustomersController.php @@ -0,0 +1,1111 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Customer $object + */ +class AdminCustomersControllerCore extends AdminController +{ + protected $delete_mode; + + protected $_defaultOrderBy = 'date_add'; + protected $_defaultOrderWay = 'DESC'; + protected $can_add_customer = true; + protected static $meaning_status = array(); + + public function __construct() + { + $this->bootstrap = true; + $this->required_database = true; + $this->table = 'customer'; + $this->className = 'Customer'; + $this->lang = false; + $this->deleted = true; + $this->explicitSelect = true; + + $this->allow_export = true; + + parent::__construct(); + + $this->required_fields = array( + array( + 'name' => 'optin', + 'label' => $this->trans('Partner offers', array(), 'Admin.Orderscustomers.Feature'), + ), + ); + + $this->addRowAction('edit'); + $this->addRowAction('view'); + $this->addRowAction('delete'); + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + ), + ); + + $this->default_form_language = $this->context->language->id; + + $titles_array = array(); + $genders = Gender::getGenders($this->context->language->id); + foreach ($genders as $gender) { + /* @var Gender $gender */ + $titles_array[$gender->id_gender] = $gender->name; + } + + $this->_join = 'LEFT JOIN ' . _DB_PREFIX_ . 'gender_lang gl ON (a.id_gender = gl.id_gender AND gl.id_lang = ' . (int) $this->context->language->id . ')'; + $this->_use_found_rows = false; + $this->fields_list = array( + 'id_customer' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + ), + 'title' => array( + 'title' => $this->trans('Social title', array(), 'Admin.Global'), + 'filter_key' => 'a!id_gender', + 'type' => 'select', + 'list' => $titles_array, + 'filter_type' => 'int', + 'order_key' => 'gl!name', + ), + 'firstname' => array( + 'title' => $this->trans('First name', array(), 'Admin.Global'), + 'maxlength' => 30, + ), + 'lastname' => array( + 'title' => $this->trans('Last name', array(), 'Admin.Global'), + 'maxlength' => 30, + ), + 'email' => array( + 'title' => $this->trans('Email address', array(), 'Admin.Global'), + 'maxlength' => 50, + ), + ); + + if (Configuration::get('PS_B2B_ENABLE')) { + $this->fields_list = array_merge($this->fields_list, array( + 'company' => array( + 'title' => $this->trans('Company', array(), 'Admin.Global'), + ), + )); + } + + $this->fields_list = array_merge($this->fields_list, array( + 'total_spent' => array( + 'title' => $this->trans('Sales', array(), 'Admin.Global'), + 'type' => 'price', + 'search' => false, + 'havingFilter' => true, + 'align' => 'text-right', + 'badge_success' => true, + ), + 'active' => array( + 'title' => $this->trans('Enabled', array(), 'Admin.Global'), + 'align' => 'text-center', + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + 'filter_key' => 'a!active', + ), + 'newsletter' => array( + 'title' => $this->trans('Newsletter', array(), 'Admin.Global'), + 'align' => 'text-center', + 'callback' => 'printNewsIcon', + ), + 'optin' => array( + 'title' => $this->trans('Partner offers', array(), 'Admin.Orderscustomers.Feature'), + 'align' => 'text-center', + 'callback' => 'printOptinIcon', + ), + 'date_add' => array( + 'title' => $this->trans('Registration', array(), 'Admin.Orderscustomers.Feature'), + 'type' => 'date', + 'align' => 'text-right', + ), + 'connect' => array( + 'title' => $this->trans('Last visit', array(), 'Admin.Orderscustomers.Feature'), + 'type' => 'datetime', + 'search' => false, + 'havingFilter' => true, + ), + )); + + $this->shopLinkType = 'shop'; + $this->shopShareDatas = Shop::SHARE_CUSTOMER; + + $this->_select = ' + a.date_add, gl.name as title, ( + SELECT SUM(total_paid_real / conversion_rate) + FROM ' . _DB_PREFIX_ . 'orders o + WHERE o.id_customer = a.id_customer + ' . Shop::addSqlRestriction(Shop::SHARE_ORDER, 'o') . ' + AND o.valid = 1 + ) as total_spent, ( + SELECT c.date_add FROM ' . _DB_PREFIX_ . 'guest g + LEFT JOIN ' . _DB_PREFIX_ . 'connections c ON c.id_guest = g.id_guest + WHERE g.id_customer = a.id_customer + ORDER BY c.date_add DESC + LIMIT 1 + ) as connect'; + + // Check if we can add a customer + if (Shop::isFeatureActive() && (Shop::getContext() == Shop::CONTEXT_ALL || Shop::getContext() == Shop::CONTEXT_GROUP)) { + $this->can_add_customer = false; + } + + self::$meaning_status = array( + 'open' => $this->trans('Open', array(), 'Admin.Orderscustomers.Feature'), + 'closed' => $this->trans('Closed', array(), 'Admin.Orderscustomers.Feature'), + 'pending1' => $this->trans('Pending 1', array(), 'Admin.Orderscustomers.Feature'), + 'pending2' => $this->trans('Pending 2', array(), 'Admin.Orderscustomers.Feature'), + ); + } + + public function postProcess() + { + if (!$this->can_add_customer && $this->display == 'add') { + $this->redirect_after = $this->context->link->getAdminLink('AdminCustomers'); + } + + parent::postProcess(); + } + + public function initContent() + { + if ($this->action == 'select_delete') { + $this->context->smarty->assign(array( + 'delete_form' => true, + 'url_delete' => htmlentities($_SERVER['REQUEST_URI']), + 'boxes' => $this->boxes, + )); + } + + if (!$this->can_add_customer && !$this->display) { + $this->informations[] = $this->trans('You have to select a shop if you want to create a customer.', array(), 'Admin.Orderscustomers.Notification'); + } + + parent::initContent(); + } + + public function initToolbar() + { + parent::initToolbar(); + + if (!$this->can_add_customer) { + unset($this->toolbar_btn['new']); + } elseif (!$this->display && $this->can_import) { + $this->toolbar_btn['import'] = array( + 'href' => $this->context->link->getAdminLink('AdminImport', true) . '&import_type=customers', + 'desc' => $this->trans('Import', array(), 'Admin.Actions'), + ); + } + } + + public function getList($id_lang, $orderBy = null, $orderWay = null, $start = 0, $limit = null, $id_lang_shop = null) + { + parent::getList($id_lang, $orderBy, $orderWay, $start, $limit, $id_lang_shop); + + if ($this->_list) { + foreach ($this->_list as &$row) { + $row['badge_success'] = $row['total_spent'] > 0; + } + } + } + + public function initToolbarTitle() + { + parent::initToolbarTitle(); + + switch ($this->display) { + case '': + case 'list': + array_pop($this->toolbar_title); + $this->toolbar_title[] = $this->trans('Manage your Customers', array(), 'Admin.Orderscustomers.Feature'); + break; + case 'view': + /** @var Customer $customer */ + if (($customer = $this->loadObject(true)) && Validate::isLoadedObject($customer)) { + array_pop($this->toolbar_title); + $this->toolbar_title[] = $this->trans('Information about customer %name%', array('%name%' => Tools::substr($customer->firstname, 0, 1) . '. ' . $customer->lastname), 'Admin.Orderscustomers.Feature'); + } + break; + case 'add': + case 'edit': + array_pop($this->toolbar_title); + /** @var Customer $customer */ + if (($customer = $this->loadObject(true)) && Validate::isLoadedObject($customer)) { + $this->toolbar_title[] = $this->trans('Editing customer %name%', array('%name%' => Tools::substr($customer->firstname, 0, 1) . '. ' . $customer->lastname), 'Admin.Orderscustomers.Feature'); + } else { + $this->toolbar_title[] = $this->trans('Creating a new Customer', array(), 'Admin.Orderscustomers.Feature'); + } + break; + } + + array_pop($this->meta_title); + if (count($this->toolbar_title) > 0) { + $this->addMetaTitle($this->toolbar_title[count($this->toolbar_title) - 1]); + } + } + + public function initPageHeaderToolbar() + { + if (empty($this->display) && $this->can_add_customer) { + $this->page_header_toolbar_btn['new_customer'] = array( + 'href' => self::$currentIndex . '&addcustomer&token=' . $this->token, + 'desc' => $this->trans('Add new customer', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function initProcess() + { + parent::initProcess(); + + if (Tools::isSubmit('submitGuestToCustomer') && $this->id_object) { + if ($this->access('edit')) { + $this->action = 'guest_to_customer'; + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('changeNewsletterVal') && $this->id_object) { + if ($this->access('edit')) { + $this->action = 'change_newsletter_val'; + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('changeOptinVal') && $this->id_object) { + if ($this->access('edit')) { + $this->action = 'change_optin_val'; + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } + + // When deleting, first display a form to select the type of deletion + if ($this->action == 'delete' || $this->action == 'bulkdelete') { + if (Tools::getValue('deleteMode') == 'real' || Tools::getValue('deleteMode') == 'deleted') { + $this->delete_mode = Tools::getValue('deleteMode'); + } else { + $this->action = 'select_delete'; + } + } + } + + public function renderList() + { + if ((Tools::isSubmit('submitBulkdelete' . $this->table) || Tools::isSubmit('delete' . $this->table)) && $this->access('delete')) { + $this->tpl_list_vars = array( + 'delete_customer' => true, + 'REQUEST_URI' => $_SERVER['REQUEST_URI'], + 'POST' => $_POST, + ); + } + + return parent::renderList(); + } + + public function renderForm() + { + /** @var Customer $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + + $genders = Gender::getGenders(); + $list_genders = array(); + foreach ($genders as $key => $gender) { + /* @var Gender $gender */ + $list_genders[$key]['id'] = 'gender_' . $gender->id; + $list_genders[$key]['value'] = $gender->id; + $list_genders[$key]['label'] = $gender->name; + } + + $years = Tools::dateYears(); + $months = Tools::dateMonths(); + $days = Tools::dateDays(); + + $groups = Group::getGroups($this->default_form_language, true); + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Customer', array(), 'Admin.Global'), + 'icon' => 'icon-user', + ), + 'input' => array( + array( + 'type' => 'radio', + 'label' => $this->trans('Social title', array(), 'Admin.Global'), + 'name' => 'id_gender', + 'required' => false, + 'class' => 't', + 'values' => $list_genders, + ), + array( + 'type' => 'text', + 'label' => $this->trans('First name', array(), 'Admin.Global'), + 'name' => 'firstname', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' 0-9!<>,;?=+()@#"°{}_$%:', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Last name', array(), 'Admin.Global'), + 'name' => 'lastname', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' 0-9!<>,;?=+()@#"°{}_$%:', + ), + array( + 'type' => 'text', + 'prefix' => '', + 'label' => $this->trans('Email address', array(), 'Admin.Global'), + 'name' => 'email', + 'col' => '4', + 'required' => true, + 'autocomplete' => false, + ), + array( + 'type' => 'password', + 'label' => $this->trans('Password', array(), 'Admin.Global'), + 'name' => 'passwd', + 'required' => ($obj->id ? false : true), + 'col' => '4', + 'hint' => ($obj->id ? $this->trans('Leave this field blank if there\'s no change.', array(), 'Admin.Orderscustomers.Help') : + $this->trans('Password should be at least %length% characters long.', array('%length%' => Validate::PASSWORD_LENGTH), 'Admin.Orderscustomers.Help')), + ), + array( + 'type' => 'birthday', + 'label' => $this->trans('Birthday', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'birthday', + 'options' => array( + 'days' => $days, + 'months' => $months, + 'years' => $years, + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Enable or disable customer login.', array(), 'Admin.Orderscustomers.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Partner offers', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'optin', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'optin_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'optin_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'disabled' => (bool) !Configuration::get('PS_CUSTOMER_OPTIN'), + 'hint' => $this->trans('This customer will receive your ads via email.', array(), 'Admin.Orderscustomers.Help'), + ), + ), + ); + + // if we add a customer via fancybox (ajax), it's a customer and he doesn't need to be added to the visitor and guest groups + if (Tools::isSubmit('addcustomer') && Tools::isSubmit('submitFormAjax')) { + $visitor_group = Configuration::get('PS_UNIDENTIFIED_GROUP'); + $guest_group = Configuration::get('PS_GUEST_GROUP'); + foreach ($groups as $key => $g) { + if (in_array($g['id_group'], array($visitor_group, $guest_group))) { + unset($groups[$key]); + } + } + } + + $this->fields_form['input'] = array_merge( + $this->fields_form['input'], + array( + array( + 'type' => 'group', + 'label' => $this->trans('Group access', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'groupBox', + 'values' => $groups, + 'required' => true, + 'col' => '6', + 'hint' => $this->trans('Select all the groups that you would like to apply to this customer.', array(), 'Admin.Orderscustomers.Help'), + ), + array( + 'type' => 'select', + 'label' => $this->trans('Default customer group', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'id_default_group', + 'options' => array( + 'query' => $groups, + 'id' => 'id_group', + 'name' => 'name', + ), + 'col' => '4', + 'hint' => array( + $this->trans('This group will be the user\'s default group.', array(), 'Admin.Orderscustomers.Help'), + $this->trans('Only the discount for the selected group will be applied to this customer.', array(), 'Admin.Orderscustomers.Help'), + ), + ), + ) + ); + + // if customer is a guest customer, password hasn't to be there + if ($obj->id && ($obj->is_guest && $obj->id_default_group == Configuration::get('PS_GUEST_GROUP'))) { + foreach ($this->fields_form['input'] as $k => $field) { + if ($field['type'] == 'password') { + array_splice($this->fields_form['input'], $k, 1); + } + } + } + + if (Configuration::get('PS_B2B_ENABLE')) { + $risks = Risk::getRisks(); + + $list_risks = array(); + foreach ($risks as $key => $risk) { + /* @var Risk $risk */ + $list_risks[$key]['id_risk'] = (int) $risk->id; + $list_risks[$key]['name'] = $risk->name; + } + + $this->fields_form['input'][] = array( + 'type' => 'text', + 'label' => $this->trans('Company', array(), 'Admin.Global'), + 'name' => 'company', + ); + $this->fields_form['input'][] = array( + 'type' => 'text', + 'label' => $this->trans('SIRET', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'siret', + ); + $this->fields_form['input'][] = array( + 'type' => 'text', + 'label' => $this->trans('APE', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'ape', + ); + $this->fields_form['input'][] = array( + 'type' => 'text', + 'label' => $this->trans('Website', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'website', + ); + $this->fields_form['input'][] = array( + 'type' => 'text', + 'label' => $this->trans('Allowed outstanding amount', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'outstanding_allow_amount', + 'hint' => $this->trans('Valid characters:', array(), 'Admin.Orderscustomers.Help') . ' 0-9', + 'suffix' => $this->context->currency->sign, + ); + $this->fields_form['input'][] = array( + 'type' => 'text', + 'label' => $this->trans('Maximum number of payment days', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'max_payment_days', + 'hint' => $this->trans('Valid characters:', array(), 'Admin.Orderscustomers.Help') . ' 0-9', + ); + $this->fields_form['input'][] = array( + 'type' => 'select', + 'label' => $this->trans('Risk rating', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'id_risk', + 'required' => false, + 'class' => 't', + 'options' => array( + 'query' => $list_risks, + 'id' => 'id_risk', + 'name' => 'name', + ), + ); + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + $birthday = explode('-', $this->getFieldValue($obj, 'birthday')); + + $this->fields_value = array( + 'years' => $this->getFieldValue($obj, 'birthday') ? $birthday[0] : 0, + 'months' => $this->getFieldValue($obj, 'birthday') ? $birthday[1] : 0, + 'days' => $this->getFieldValue($obj, 'birthday') ? $birthday[2] : 0, + ); + + // Added values of object Group + if (!Validate::isUnsignedId($obj->id)) { + $customer_groups = array(); + } else { + $customer_groups = $obj->getGroups(); + } + $customer_groups_ids = array(); + if (is_array($customer_groups)) { + foreach ($customer_groups as $customer_group) { + $customer_groups_ids[] = $customer_group; + } + } + + // if empty $carrier_groups_ids : object creation : we set the default groups + if (empty($customer_groups_ids)) { + $preselected = array(Configuration::get('PS_UNIDENTIFIED_GROUP'), Configuration::get('PS_GUEST_GROUP'), Configuration::get('PS_CUSTOMER_GROUP')); + $customer_groups_ids = array_merge($customer_groups_ids, $preselected); + } + + foreach ($groups as $group) { + $this->fields_value['groupBox_' . $group['id_group']] = + Tools::getValue('groupBox_' . $group['id_group'], in_array($group['id_group'], $customer_groups_ids)); + } + + return parent::renderForm(); + } + + public function beforeAdd($customer) + { + $customer->id_shop = $this->context->shop->id; + } + + public function renderKpis() + { + $time = time(); + $kpis = array(); + + /* The data generation is located in AdminStatsControllerCore */ + + $helper = new HelperKpi(); + $helper->id = 'box-gender'; + $helper->icon = 'icon-male'; + $helper->color = 'color1'; + $helper->title = $this->trans('Customers', array(), 'Admin.Global'); + $helper->subtitle = $this->trans('All Time', array(), 'Admin.Global'); + if (ConfigurationKPI::get('CUSTOMER_MAIN_GENDER', $this->context->language->id) !== false) { + $helper->value = ConfigurationKPI::get('CUSTOMER_MAIN_GENDER', $this->context->language->id); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=customer_main_gender'; + $helper->refresh = (bool) (ConfigurationKPI::get('CUSTOMER_MAIN_GENDER_EXPIRE', $this->context->language->id) < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-age'; + $helper->icon = 'icon-calendar'; + $helper->color = 'color2'; + $helper->title = $this->trans('Average Age', array(), 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('All Time', array(), 'Admin.Global'); + if (ConfigurationKPI::get('AVG_CUSTOMER_AGE', $this->context->language->id) !== false) { + $helper->value = ConfigurationKPI::get('AVG_CUSTOMER_AGE', $this->context->language->id); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=avg_customer_age'; + $helper->refresh = (bool) (ConfigurationKPI::get('AVG_CUSTOMER_AGE_EXPIRE', $this->context->language->id) < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-orders'; + $helper->icon = 'icon-retweet'; + $helper->color = 'color3'; + $helper->title = $this->trans('Orders per Customer', array(), 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('All Time', array(), 'Admin.Global'); + if (ConfigurationKPI::get('ORDERS_PER_CUSTOMER') !== false) { + $helper->value = ConfigurationKPI::get('ORDERS_PER_CUSTOMER'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=orders_per_customer'; + $helper->refresh = (bool) (ConfigurationKPI::get('ORDERS_PER_CUSTOMER_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-newsletter'; + $helper->icon = 'icon-envelope'; + $helper->color = 'color4'; + $helper->title = $this->trans('Newsletter Registrations', array(), 'Admin.Orderscustomers.Feature'); + $helper->subtitle = $this->trans('All Time', array(), 'Admin.Global'); + if (ConfigurationKPI::get('NEWSLETTER_REGISTRATIONS') !== false) { + $helper->value = ConfigurationKPI::get('NEWSLETTER_REGISTRATIONS'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=newsletter_registrations'; + $helper->refresh = (bool) (ConfigurationKPI::get('NEWSLETTER_REGISTRATIONS_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpiRow(); + $helper->kpis = $kpis; + + return $helper->generate(); + } + + public function renderView() + { + /** @var Customer $customer */ + if (!($customer = $this->loadObject())) { + return; + } + + $this->context->customer = $customer; + $gender = new Gender($customer->id_gender, $this->context->language->id); + $gender_image = $gender->getImage(); + + $customer_stats = $customer->getStats(); + $sql = 'SELECT SUM(total_paid_real) FROM ' . _DB_PREFIX_ . 'orders WHERE id_customer = ' . (int) $customer->id . ' AND valid = 1'; + if ($total_customer = Db::getInstance()->getValue($sql)) { + $sql = 'SELECT SQL_CALC_FOUND_ROWS COUNT(*) FROM ' . _DB_PREFIX_ . 'orders WHERE valid = 1 AND id_customer != ' . (int) $customer->id . ' GROUP BY id_customer HAVING SUM(total_paid_real) > ' . (int) $total_customer; + Db::getInstance()->getValue($sql); + $count_better_customers = (int) Db::getInstance()->getValue('SELECT FOUND_ROWS()') + 1; + } else { + $count_better_customers = '-'; + } + + $orders = Order::getCustomerOrders($customer->id, true); + $total_orders = count($orders); + for ($i = 0; $i < $total_orders; ++$i) { + $orders[$i]['total_paid_real_not_formated'] = $orders[$i]['total_paid_real']; + $orders[$i]['total_paid_real'] = Tools::displayPrice($orders[$i]['total_paid_real'], new Currency((int) $orders[$i]['id_currency'])); + } + + $messages = CustomerThread::getCustomerMessages((int) $customer->id); + + $total_messages = count($messages); + for ($i = 0; $i < $total_messages; ++$i) { + $messages[$i]['message'] = substr(strip_tags(html_entity_decode($messages[$i]['message'], ENT_NOQUOTES, 'UTF-8')), 0, 75); + $messages[$i]['date_add'] = Tools::displayDate($messages[$i]['date_add'], null, true); + if (isset(self::$meaning_status[$messages[$i]['status']])) { + $messages[$i]['status'] = self::$meaning_status[$messages[$i]['status']]; + } + } + + $groups = $customer->getGroups(); + $total_groups = count($groups); + for ($i = 0; $i < $total_groups; ++$i) { + $group = new Group($groups[$i]); + $groups[$i] = array(); + $groups[$i]['id_group'] = $group->id; + $groups[$i]['name'] = $group->name[$this->default_form_language]; + } + + $total_ok = 0; + $orders_ok = array(); + $orders_ko = array(); + foreach ($orders as $order) { + if (!isset($order['order_state'])) { + $order['order_state'] = $this->trans('There is no status defined for this order.', array(), 'Admin.Orderscustomers.Notification'); + } + + if ($order['valid']) { + $orders_ok[] = $order; + $total_ok += $order['total_paid_real_not_formated'] / $order['conversion_rate']; + } else { + $orders_ko[] = $order; + } + } + + $products = $customer->getBoughtProducts(); + + $carts = Cart::getCustomerCarts($customer->id); + $total_carts = count($carts); + for ($i = 0; $i < $total_carts; ++$i) { + $cart = new Cart((int) $carts[$i]['id_cart']); + $this->context->cart = $cart; + $currency = new Currency((int) $carts[$i]['id_currency']); + $this->context->currency = $currency; + $summary = $cart->getSummaryDetails(); + $carrier = new Carrier((int) $carts[$i]['id_carrier']); + $carts[$i]['id_cart'] = sprintf('%06d', $carts[$i]['id_cart']); + $carts[$i]['date_add'] = Tools::displayDate($carts[$i]['date_add'], null, true); + $carts[$i]['total_price'] = Tools::displayPrice($summary['total_price'], $currency); + $carts[$i]['name'] = $carrier->name; + } + + $this->context->currency = Currency::getDefaultCurrency(); + + $sql = 'SELECT DISTINCT cp.id_product, c.id_cart, c.id_shop, cp.id_shop AS cp_id_shop + FROM ' . _DB_PREFIX_ . 'cart_product cp + JOIN ' . _DB_PREFIX_ . 'cart c ON (c.id_cart = cp.id_cart) + JOIN ' . _DB_PREFIX_ . 'product p ON (cp.id_product = p.id_product) + WHERE c.id_customer = ' . (int) $customer->id . ' + AND NOT EXISTS ( + SELECT 1 + FROM ' . _DB_PREFIX_ . 'orders o + JOIN ' . _DB_PREFIX_ . 'order_detail od ON (o.id_order = od.id_order) + WHERE product_id = cp.id_product AND o.valid = 1 AND o.id_customer = ' . (int) $customer->id . ' + )'; + $interested = Db::getInstance()->executeS($sql); + $total_interested = count($interested); + for ($i = 0; $i < $total_interested; ++$i) { + $product = new Product($interested[$i]['id_product'], false, $this->default_form_language, $interested[$i]['id_shop']); + if (!Validate::isLoadedObject($product)) { + continue; + } + $interested[$i]['url'] = $this->context->link->getProductLink( + $product->id, + $product->link_rewrite, + Category::getLinkRewrite($product->id_category_default, $this->default_form_language), + null, + null, + $interested[$i]['cp_id_shop'] + ); + $interested[$i]['id'] = (int) $product->id; + $interested[$i]['name'] = Tools::htmlentitiesUTF8($product->name); + } + + $emails = $customer->getLastEmails(); + + $connections = $customer->getLastConnections(); + if (!is_array($connections)) { + $connections = array(); + } + $total_connections = count($connections); + for ($i = 0; $i < $total_connections; ++$i) { + $connections[$i]['http_referer'] = $connections[$i]['http_referer'] ? preg_replace('/^www./', '', parse_url($connections[$i]['http_referer'], PHP_URL_HOST)) : $this->trans('Direct link', array(), 'Admin.Orderscustomers.Notification'); + } + + $referrers = Referrer::getReferrers($customer->id); + $total_referrers = count($referrers); + for ($i = 0; $i < $total_referrers; ++$i) { + $referrers[$i]['date_add'] = Tools::displayDate($referrers[$i]['date_add'], null, true); + } + + $customerLanguage = new Language($customer->id_lang); + $shop = new Shop($customer->id_shop); + $this->tpl_view_vars = array( + 'customer' => $customer, + 'gender' => $gender, + 'gender_image' => $gender_image, + // General information of the customer + 'registration_date' => Tools::displayDate($customer->date_add, null, true), + 'customer_stats' => $customer_stats, + 'last_visit' => Tools::displayDate($customer_stats['last_visit'], null, true), + 'count_better_customers' => $count_better_customers, + 'shop_is_feature_active' => Shop::isFeatureActive(), + 'name_shop' => $shop->name, + 'customer_birthday' => Tools::displayDate($customer->birthday), + 'last_update' => Tools::displayDate($customer->date_upd, null, true), + 'customer_exists' => Customer::customerExists($customer->email), + 'id_lang' => $customer->id_lang, + 'customerLanguage' => $customerLanguage, + // Add a Private note + 'customer_note' => Tools::htmlentitiesUTF8($customer->note), + // Messages + 'messages' => $messages, + // Groups + 'groups' => $groups, + // Orders + 'orders' => $orders, + 'orders_ok' => $orders_ok, + 'orders_ko' => $orders_ko, + 'total_ok' => Tools::displayPrice($total_ok, $this->context->currency->id), + // Products + 'products' => $products, + // Addresses + 'addresses' => $customer->getAddresses($this->default_form_language), + // Discounts + 'discounts' => CartRule::getCustomerCartRules($this->default_form_language, $customer->id, false, false), + // Carts + 'carts' => $carts, + // Interested + 'interested' => $interested, + // Emails + 'emails' => $emails, + // Connections + 'connections' => $connections, + // Referrers + 'referrers' => $referrers, + 'show_toolbar' => true, + ); + + return parent::renderView(); + } + + public function processDelete() + { + $this->_setDeletedMode(); + parent::processDelete(); + } + + protected function _setDeletedMode() + { + if ($this->delete_mode == 'real') { + $this->deleted = false; + } elseif ($this->delete_mode == 'deleted') { + $this->deleted = true; + } else { + $this->errors[] = $this->trans('Unknown delete mode:', array(), 'Admin.Orderscustomers.Notification') . ' ' . $this->deleted; + + return; + } + } + + protected function processBulkDelete() + { + $this->_setDeletedMode(); + parent::processBulkDelete(); + } + + public function processAdd() + { + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + } + // Check that the new email is not already in use + $customer_email = strval(Tools::getValue('email')); + $customer = new Customer(); + if (Validate::isEmail($customer_email)) { + $customer->getByEmail($customer_email); + } + if ($customer->id) { + $this->errors[] = $this->trans('An account already exists for this email address:', array(), 'Admin.Orderscustomers.Notification') . ' ' . $customer_email; + $this->display = 'edit'; + + return $customer; + } elseif (trim(Tools::getValue('passwd')) == '') { + $this->validateRules(); + $this->errors[] = $this->trans('Password cannot be empty.', array(), 'Admin.Orderscustomers.Notification'); + $this->display = 'edit'; + } elseif ($customer = parent::processAdd()) { + $this->context->smarty->assign('new_customer', $customer); + + return $customer; + } + + return false; + } + + public function processUpdate() + { + if (Validate::isLoadedObject($this->object)) { + $customer_email = strval(Tools::getValue('email')); + + // check if e-mail already used + if ($customer_email != $this->object->email) { + $customer = new Customer(); + if (Validate::isEmail($customer_email)) { + $customer->getByEmail($customer_email); + } + if (($customer->id) && ($customer->id != (int) $this->object->id)) { + $this->errors[] = $this->trans('An account already exists for this email address:', array(), 'Admin.Orderscustomers.Notification') . ' ' . $customer_email; + } + } + + return parent::processUpdate(); + } else { + $this->errors[] = $this->trans('An error occurred while loading the object.', array(), 'Admin.Orderscustomers.Notification') . ' + ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Orderscustomers.Notification'); + } + } + + public function processSave() + { + // Check that default group is selected + if (!is_array(Tools::getValue('groupBox')) || !in_array(Tools::getValue('id_default_group'), Tools::getValue('groupBox'))) { + $this->errors[] = $this->trans('A default customer group must be selected in group box.', array(), 'Admin.Orderscustomers.Notification'); + } + + // Check the requires fields which are settings in the BO + $customer = new Customer(); + $this->errors = array_merge($this->errors, $customer->validateFieldsRequiredDatabase()); + + return parent::processSave(); + } + + protected function afterDelete($object, $old_id) + { + $customer = new Customer($old_id); + $addresses = $customer->getAddresses($this->default_form_language); + foreach ($addresses as $k => $v) { + $address = new Address($v['id_address']); + $address->id_customer = $object->id; + $address->save(); + } + + return true; + } + + /** + * Transform a guest account into a registered customer account. + */ + public function processGuestToCustomer() + { + $customer = new Customer((int) Tools::getValue('id_customer')); + if (!Validate::isLoadedObject($customer)) { + $this->errors[] = $this->trans('This customer does not exist.', array(), 'Admin.Orderscustomers.Notification'); + } + if (Customer::customerExists($customer->email)) { + $this->errors[] = $this->trans('This customer already exists as a non-guest.', array(), 'Admin.Orderscustomers.Notification'); + } elseif ($customer->transformToCustomer(Tools::getValue('id_lang', $this->context->language->id))) { + if ($id_order = (int) Tools::getValue('id_order')) { + Tools::redirectAdmin($this->context->link->getAdminLink('AdminOrders') . '&id_order=' . $id_order . '&vieworder&conf=3'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&' . $this->identifier . '=' . $customer->id . '&viewcustomer&conf=3&token=' . $this->token); + } + } else { + $this->errors[] = $this->trans('An error occurred while updating customer information.', array(), 'Admin.Orderscustomers.Notification'); + } + } + + /** + * Toggle the newsletter flag. + */ + public function processChangeNewsletterVal() + { + $customer = new Customer($this->id_object); + if (!Validate::isLoadedObject($customer)) { + $this->errors[] = $this->trans('An error occurred while updating customer information.', array(), 'Admin.Orderscustomers.Notification'); + } + $customer->newsletter = $customer->newsletter ? 0 : 1; + if (!$customer->update()) { + $this->errors[] = $this->trans('An error occurred while updating customer information.', array(), 'Admin.Orderscustomers.Notification'); + } + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token); + } + + /** + * Toggle newsletter optin flag. + */ + public function processChangeOptinVal() + { + $customer = new Customer($this->id_object); + if (!Validate::isLoadedObject($customer)) { + $this->errors[] = $this->trans('An error occurred while updating customer information.', array(), 'Admin.Orderscustomers.Notification'); + } + $customer->optin = $customer->optin ? 0 : 1; + if (!$customer->update()) { + $this->errors[] = $this->trans('An error occurred while updating customer information.', array(), 'Admin.Orderscustomers.Notification'); + } + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token); + } + + public function printNewsIcon($value, $customer) + { + return ' + ' . ($value ? '' : '') . + ''; + } + + public function printOptinIcon($value, $customer) + { + return ' + ' . ($value ? '' : '') . + ''; + } + + /** + * @param string $token + * @param int $id + * @param string $name + * + * @return mixed + */ + public function displayDeleteLink($token, $id, $name = null) + { + $tpl = $this->createTemplate('helpers/list/list_action_delete.tpl'); + + $customer = new Customer($id); + $name = $customer->lastname . ' ' . $customer->firstname; + $name = '\n\n' . $this->trans('Name:', array(), 'Admin.Global') . ' ' . $name; + + $tpl->assign(array( + 'href' => self::$currentIndex . '&' . $this->identifier . '=' . $id . '&delete' . $this->table . '&token=' . ($token != null ? $token : $this->token), + 'confirm' => $this->trans('Delete the selected item?', array(), 'Admin.Notifications.Info') . $name, + 'action' => $this->trans('Delete', array(), 'Admin.Actions'), + 'id' => $id, + )); + + return $tpl->fetch(); + } + + /** + * add to $this->content the result of Customer::SearchByName + * (encoded in json). + */ + public function ajaxProcessSearchCustomers() + { + $searches = explode(' ', Tools::getValue('customer_search')); + $customers = array(); + $searches = array_unique($searches); + foreach ($searches as $search) { + if (!empty($search) && $results = Customer::searchByName($search, 50)) { + foreach ($results as $result) { + if ($result['active']) { + $result['fullname_and_email'] = $result['firstname'] . ' ' . $result['lastname'] . ' - ' . $result['email']; + $customers[$result['id_customer']] = $result; + } + } + } + } + + if (count($customers) && Tools::getValue('sf2')) { + $to_return = $customers; + } elseif (count($customers) && !Tools::getValue('sf2')) { + $to_return = array( + 'customers' => $customers, + 'found' => true, + ); + } else { + $to_return = Tools::getValue('sf2') ? array() : array('found' => false); + } + + $this->content = json_encode($to_return); + } + + /** + * Uodate the customer note. + */ + public function ajaxProcessUpdateCustomerNote() + { + if ($this->access('edit')) { + $note = Tools::htmlentitiesDecodeUTF8(Tools::getValue('note')); + $customer = new Customer((int) Tools::getValue('id_customer')); + if (!Validate::isLoadedObject($customer)) { + die('error:update'); + } + if (!empty($note) && !Validate::isCleanHtml($note)) { + die('error:validation'); + } + $customer->note = $note; + if (!$customer->update()) { + die('error:update'); + } + die('ok'); + } + } +} diff --git a/controllers/admin/AdminDashboardController.php b/controllers/admin/AdminDashboardController.php new file mode 100644 index 00000000..e7118e41 --- /dev/null +++ b/controllers/admin/AdminDashboardController.php @@ -0,0 +1,538 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; + +class AdminDashboardControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->display = 'view'; + + parent::__construct(); + + if (Tools::isSubmit('profitability_conf') || Tools::isSubmit('submitOptionsconfiguration')) { + $this->fields_options = $this->getOptionFields(); + } + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + + $this->addJqueryUI('ui.datepicker'); + $this->addJS(array( + _PS_JS_DIR_ . 'vendor/d3.v3.min.js', + __PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/js/vendor/nv.d3.min.js', + _PS_JS_DIR_ . '/admin/dashboard.js', + )); + $this->addCSS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/css/vendor/nv.d3.css'); + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_title = $this->trans('Dashboard', array(), 'Admin.Dashboard.Feature'); + $this->page_header_toolbar_btn['switch_demo'] = array( + 'desc' => $this->trans('Demo mode', array(), 'Admin.Dashboard.Feature'), + 'icon' => 'process-icon-toggle-' . (Configuration::get('PS_DASHBOARD_SIMULATION') ? 'on' : 'off'), + 'help' => $this->trans('This mode displays sample data so you can try your dashboard without real numbers.', array(), 'Admin.Dashboard.Help'), + ); + + parent::initPageHeaderToolbar(); + + // Remove the last element on this controller to match the title with the rule of the others + array_pop($this->meta_title); + } + + protected function getOptionFields() + { + $forms = array(); + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + $carriers = Carrier::getCarriers((int) $this->context->language->id, true, false, false, null, Carrier::ALL_CARRIERS); + $modules = Module::getModulesOnDisk(true); + + $forms = array( + 'payment' => array('title' => $this->trans('Average bank fees per payment method', array(), 'Admin.Dashboard.Feature'), 'id' => 'payment'), + 'carriers' => array('title' => $this->trans('Average shipping fees per shipping method', array(), 'Admin.Dashboard.Feature'), 'id' => 'carriers'), + 'other' => array('title' => $this->trans('Other settings', array(), 'Admin.Dashboard.Feature'), 'id' => 'other'), + ); + foreach ($forms as &$form) { + $form['icon'] = 'tab-preferences'; + $form['fields'] = array(); + $form['submit'] = array('title' => $this->trans('Save', array(), 'Admin.Actions')); + } + + foreach ($modules as $module) { + if (isset($module->tab) && $module->tab == 'payments_gateways' && $module->id) { + $moduleClass = Module::getInstanceByName($module->name); + if (!$moduleClass->isEnabledForShopContext()) { + continue; + } + + $forms['payment']['fields']['CONF_' . strtoupper($module->name) . '_FIXED'] = array( + 'title' => $module->displayName, + 'desc' => $this->trans( + 'Choose a fixed fee for each order placed in %currency% with %module%.', + array( + '%currency' => $currency->iso_code, + '%module%' => $module->displayName, + ), + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPrice', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => $currency->iso_code, + ); + $forms['payment']['fields']['CONF_' . strtoupper($module->name) . '_VAR'] = array( + 'title' => $module->displayName, + 'desc' => $this->trans( + 'Choose a variable fee for each order placed in %currency% with %module%. It will be applied on the total paid with taxes.', + array( + '%currency' => $currency->iso_code, + '%module%' => $module->displayName, + ), + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPercentage', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ); + + if (Currency::isMultiCurrencyActivated()) { + $forms['payment']['fields']['CONF_' . strtoupper($module->name) . '_FIXED_FOREIGN'] = array( + 'title' => $module->displayName, + 'desc' => $this->trans( + 'Choose a fixed fee for each order placed with a foreign currency with %module%.', + array( + '%module%' => $module->displayName, + ), + 'Admin.Dashboard.Help'), + 'validation' => 'isPrice', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => $currency->iso_code, + ); + $forms['payment']['fields']['CONF_' . strtoupper($module->name) . '_VAR_FOREIGN'] = array( + 'title' => $module->displayName, + 'desc' => $this->trans( + 'Choose a variable fee for each order placed with a foreign currency with %module%. It will be applied on the total paid with taxes.', + array('%module%' => $module->displayName), + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPercentage', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ); + } + } + } + + foreach ($carriers as $carrier) { + $forms['carriers']['fields']['CONF_' . strtoupper($carrier['id_reference']) . '_SHIP'] = array( + 'title' => $carrier['name'], + 'desc' => $this->trans( + 'For the carrier named %s, indicate the domestic delivery costs in percentage of the price charged to customers.', + array( + '%s' => $carrier['name'], + ), + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPercentage', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ); + $forms['carriers']['fields']['CONF_' . strtoupper($carrier['id_reference']) . '_SHIP_OVERSEAS'] = array( + 'title' => $carrier['name'], + 'desc' => $this->trans( + 'For the carrier named %s, indicate the overseas delivery costs in percentage of the price charged to customers.', + array( + '%s' => $carrier['name'], + ), + 'Admin.Dashboard.Help' + ), + 'validation' => 'isPercentage', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ); + } + + $forms['carriers']['description'] = $this->trans('Method: Indicate the percentage of your carrier margin. For example, if you charge $10 of shipping fees to your customer for each shipment, but you really pay $4 to this carrier, then you should indicate "40" in the percentage field.', array(), 'Admin.Dashboard.Help'); + + $forms['other']['fields']['CONF_AVERAGE_PRODUCT_MARGIN'] = array( + 'title' => $this->trans('Average gross margin percentage', array(), 'Admin.Dashboard.Feature'), + 'desc' => $this->trans('You should calculate this percentage as follows: ((total sales revenue) - (cost of goods sold)) / (total sales revenue) * 100. This value is only used to calculate the Dashboard approximate gross margin, if you do not specify the wholesale price for each product.', array(), 'Admin.Dashboard.Help'), + 'validation' => 'isPercentage', + 'cast' => 'intval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => '%', + ); + + $forms['other']['fields']['CONF_ORDER_FIXED'] = array( + 'title' => $this->trans('Other fees per order', array(), 'Admin.Dashboard.Feature'), + 'desc' => $this->trans('You should calculate this value by making the sum of all of your additional costs per order.', array(), 'Admin.Dashboard.Help'), + 'validation' => 'isPrice', + 'cast' => 'floatval', + 'type' => 'text', + 'defaultValue' => '0', + 'suffix' => $currency->iso_code, + ); + + Media::addJsDef(array( + 'dashboard_ajax_url' => $this->context->link->getAdminLink('AdminDashboard'), + 'read_more' => '', + )); + + return $forms; + } + + public function renderView() + { + if (Tools::isSubmit('profitability_conf')) { + return parent::renderOptions(); + } + + // $translations = array( + // 'Calendar' => $this->trans('Calendar', array(),'Admin.Global'), + // 'Day' => $this->trans('Day', array(), 'Admin.Global'), + // 'Month' => $this->trans('Month', array(), 'Admin.Global'), + // 'Year' => $this->trans('Year', array(), 'Admin.Global'), + // 'From' => $this->trans('From:', array(), 'Admin.Global'), + // 'To' => $this->trans('To:', array(), 'Admin.Global'), + // 'Save' => $this->trans('Save', array(), 'Admin.Global') + // ); + + $testStatsDateUpdate = $this->context->cookie->__get('stats_date_update'); + if (!empty($testStatsDateUpdate) && $this->context->cookie->__get('stats_date_update') < strtotime(date('Y-m-d'))) { + switch ($this->context->employee->preselect_date_range) { + case 'day': + $date_from = date('Y-m-d'); + $date_to = date('Y-m-d'); + break; + case 'prev-day': + $date_from = date('Y-m-d', strtotime('-1 day')); + $date_to = date('Y-m-d', strtotime('-1 day')); + break; + case 'month': + default: + $date_from = date('Y-m-01'); + $date_to = date('Y-m-d'); + break; + case 'prev-month': + $date_from = date('Y-m-01', strtotime('-1 month')); + $date_to = date('Y-m-t', strtotime('-1 month')); + break; + case 'year': + $date_from = date('Y-01-01'); + $date_to = date('Y-m-d'); + break; + case 'prev-year': + $date_from = date('Y-m-01', strtotime('-1 year')); + $date_to = date('Y-12-t', strtotime('-1 year')); + break; + } + $this->context->employee->stats_date_from = $date_from; + $this->context->employee->stats_date_to = $date_to; + $this->context->employee->update(); + $this->context->cookie->__set('stats_date_update', strtotime(date('Y-m-d'))); + $this->context->cookie->write(); + } + + $calendar_helper = new HelperCalendar(); + + $calendar_helper->setDateFrom(Tools::getValue('date_from', $this->context->employee->stats_date_from)); + $calendar_helper->setDateTo(Tools::getValue('date_to', $this->context->employee->stats_date_to)); + + $stats_compare_from = $this->context->employee->stats_compare_from; + $stats_compare_to = $this->context->employee->stats_compare_to; + + if (is_null($stats_compare_from) || $stats_compare_from == '0000-00-00') { + $stats_compare_from = null; + } + + if (is_null($stats_compare_to) || $stats_compare_to == '0000-00-00') { + $stats_compare_to = null; + } + + $calendar_helper->setCompareDateFrom($stats_compare_from); + $calendar_helper->setCompareDateTo($stats_compare_to); + $calendar_helper->setCompareOption(Tools::getValue('compare_date_option', $this->context->employee->stats_compare_option)); + + $params = array( + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, + ); + + $moduleManagerBuilder = ModuleManagerBuilder::getInstance(); + $moduleManager = $moduleManagerBuilder->build(); + + $this->tpl_view_vars = array( + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, + 'hookDashboardZoneOne' => Hook::exec('dashboardZoneOne', $params), + 'hookDashboardZoneTwo' => Hook::exec('dashboardZoneTwo', $params), + //'translations' => $translations, + 'action' => '#', + 'warning' => $this->getWarningDomainName(), + 'new_version_url' => Tools::getCurrentUrlProtocolPrefix() . _PS_API_DOMAIN_ . '/version/check_version.php?v=' . _PS_VERSION_ . '&lang=' . $this->context->language->iso_code . '&autoupgrade=' . (int) ($moduleManager->isInstalled('autoupgrade') && $moduleManager->isEnabled('autoupgrade')) . '&hosted_mode=' . (int) defined('_PS_HOST_MODE_'), + 'dashboard_use_push' => Configuration::get('PS_DASHBOARD_USE_PUSH'), + 'calendar' => $calendar_helper->generate(), + 'PS_DASHBOARD_SIMULATION' => Configuration::get('PS_DASHBOARD_SIMULATION'), + 'datepickerFrom' => Tools::getValue('datepickerFrom', $this->context->employee->stats_date_from), + 'datepickerTo' => Tools::getValue('datepickerTo', $this->context->employee->stats_date_to), + 'preselect_date_range' => Tools::getValue('preselectDateRange', $this->context->employee->preselect_date_range), + ); + + return parent::renderView(); + } + + public function postProcess() + { + if (Tools::isSubmit('submitDateRealTime')) { + if ($use_realtime = (int) Tools::getValue('submitDateRealTime')) { + $this->context->employee->stats_date_from = date('Y-m-d'); + $this->context->employee->stats_date_to = date('Y-m-d'); + $this->context->employee->stats_compare_option = HelperCalendar::DEFAULT_COMPARE_OPTION; + $this->context->employee->stats_compare_from = null; + $this->context->employee->stats_compare_to = null; + $this->context->employee->update(); + } + Configuration::updateValue('PS_DASHBOARD_USE_PUSH', $use_realtime); + } + + if (Tools::isSubmit('submitDateRange')) { + if (!Validate::isDate(Tools::getValue('date_from')) + || !Validate::isDate(Tools::getValue('date_to'))) { + $this->errors[] = $this->trans('The selected date range is not valid.', array(), 'Admin.Notifications.Error'); + } + + if (Tools::getValue('datepicker_compare')) { + if (!Validate::isDate(Tools::getValue('compare_date_from')) + || !Validate::isDate(Tools::getValue('compare_date_to'))) { + $this->errors[] = $this->trans('The selected date range is not valid.', array(), 'Admin.Notifications.Error'); + } + } + + if (!count($this->errors)) { + $this->context->employee->stats_date_from = Tools::getValue('date_from'); + $this->context->employee->stats_date_to = Tools::getValue('date_to'); + $this->context->employee->preselect_date_range = Tools::getValue('preselectDateRange'); + + if (Tools::getValue('datepicker_compare')) { + $this->context->employee->stats_compare_from = Tools::getValue('compare_date_from'); + $this->context->employee->stats_compare_to = Tools::getValue('compare_date_to'); + $this->context->employee->stats_compare_option = Tools::getValue('compare_date_option'); + } else { + $this->context->employee->stats_compare_from = null; + $this->context->employee->stats_compare_to = null; + $this->context->employee->stats_compare_option = HelperCalendar::DEFAULT_COMPARE_OPTION; + } + + $this->context->employee->update(); + } + } + + parent::postProcess(); + } + + protected function getWarningDomainName() + { + $warning = false; + if (Shop::isFeatureActive()) { + return; + } + + $shop = Context::getContext()->shop; + if ($_SERVER['HTTP_HOST'] != $shop->domain && $_SERVER['HTTP_HOST'] != $shop->domain_ssl && Tools::getValue('ajax') == false && !defined('_PS_HOST_MODE_')) { + $warning = $this->trans('You are currently connected under the following domain name:', array(), 'Admin.Dashboard.Notification') . ' ' . $_SERVER['HTTP_HOST'] . '
'; + if (Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE')) { + $warning .= $this->trans( + 'This is different from the shop domain name set in the Multistore settings: "%s".', + array( + '%s' => $shop->domain, + ), + 'Admin.Dashboard.Notification' + ) . $this->trans( + 'If this is your main domain, please {link}change it now{/link}.', + array( + '{link}' => '', + '{/link}' => '', + ), + 'Admin.Dashboard.Notification' + ); + } else { + $warning .= $this->trans('This is different from the domain name set in the "SEO & URLs" tab.', array(), 'Admin.Dashboard.Notification') . ' + ' . $this->trans( + 'If this is your main domain, please {link}change it now{/link}.', + array( + '{link}' => '', + '{/link}' => '', + ), + 'Admin.Dashboard.Notification' + ); + } + } + + return $warning; + } + + public function ajaxProcessRefreshDashboard() + { + $id_module = null; + if ($module = Tools::getValue('module')) { + $module_obj = Module::getInstanceByName($module); + if (Validate::isLoadedObject($module_obj)) { + $id_module = $module_obj->id; + } + } + + $params = array( + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, + 'compare_from' => $this->context->employee->stats_compare_from, + 'compare_to' => $this->context->employee->stats_compare_to, + 'dashboard_use_push' => (int) Tools::getValue('dashboard_use_push'), + 'extra' => (int) Tools::getValue('extra'), + ); + + die(json_encode(Hook::exec('dashboardData', $params, $id_module, true, true, (int) Tools::getValue('dashboard_use_push')))); + } + + public function ajaxProcessSetSimulationMode() + { + Configuration::updateValue('PS_DASHBOARD_SIMULATION', (int) Tools::getValue('PS_DASHBOARD_SIMULATION')); + die('k' . Configuration::get('PS_DASHBOARD_SIMULATION') . 'k'); + } + + public function ajaxProcessGetBlogRss() + { + $return = array('has_errors' => false, 'rss' => array()); + if (!$this->isFresh('/config/xml/blog-' . $this->context->language->iso_code . '.xml', 86400)) { + if (!$this->refresh('/config/xml/blog-' . $this->context->language->iso_code . '.xml', _PS_API_URL_ . '/rss/blog/blog-' . $this->context->language->iso_code . '.xml')) { + $return['has_errors'] = true; + } + } + + if (!$return['has_errors']) { + $rss = @simplexml_load_file(_PS_ROOT_DIR_ . '/config/xml/blog-' . $this->context->language->iso_code . '.xml'); + if (!$rss) { + $return['has_errors'] = true; + } + $articles_limit = 2; + if ($rss) { + foreach ($rss->channel->item as $item) { + if ($articles_limit > 0 && Validate::isCleanHtml((string) $item->title) && Validate::isCleanHtml((string) $item->description) + && isset($item->link) && isset($item->title)) { + if (in_array($this->context->mode, array(Context::MODE_HOST, Context::MODE_HOST_CONTRIB))) { + $utm_content = 'cloud'; + } else { + $utm_content = 'download'; + } + + $shop_default_country_id = (int) Configuration::get('PS_COUNTRY_DEFAULT'); + $shop_default_iso_country = (string) Tools::strtoupper(Country::getIsoById($shop_default_country_id)); + $analytics_params = array('utm_source' => 'back-office', + 'utm_medium' => 'rss', + 'utm_campaign' => 'back-office-' . $shop_default_iso_country, + 'utm_content' => $utm_content, + ); + $url_query = parse_url($item->link, PHP_URL_QUERY); + parse_str($url_query, $link_query_params); + + if ($link_query_params) { + $full_url_params = array_merge($link_query_params, $analytics_params); + $base_url = explode('?', (string) $item->link); + $base_url = (string) $base_url[0]; + $article_link = $base_url . '?' . http_build_query($full_url_params); + } else { + $article_link = (string) $item->link . '?' . http_build_query($analytics_params); + } + + $return['rss'][] = array( + 'date' => Tools::displayDate(date('Y-m-d', strtotime((string) $item->pubDate))), + 'title' => (string) Tools::htmlentitiesUTF8($item->title), + 'short_desc' => Tools::truncateString(strip_tags((string) $item->description), 150), + 'link' => (string) $article_link, + ); + } else { + break; + } + --$articles_limit; + } + } + } + die(json_encode($return)); + } + + public function ajaxProcessSaveDashConfig() + { + $return = array('has_errors' => false, 'errors' => array()); + $module = Tools::getValue('module'); + $hook = Tools::getValue('hook'); + $configs = Tools::getValue('configs'); + + $params = array( + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, + ); + + if (Validate::isModuleName($module) && $module_obj = Module::getInstanceByName($module)) { + if (Validate::isLoadedObject($module_obj) && method_exists($module_obj, 'validateDashConfig')) { + $return['errors'] = $module_obj->validateDashConfig($configs); + } + if (!count($return['errors'])) { + if (Validate::isLoadedObject($module_obj) && method_exists($module_obj, 'saveDashConfig')) { + $return['has_errors'] = $module_obj->saveDashConfig($configs); + } elseif (is_array($configs) && count($configs)) { + foreach ($configs as $name => $value) { + if (Validate::isConfigName($name)) { + Configuration::updateValue($name, $value); + } + } + } + } else { + $return['has_errors'] = true; + } + } + + if (Validate::isHookName($hook) && method_exists($module_obj, $hook)) { + $return['widget_html'] = $module_obj->$hook($params); + } + + die(json_encode($return)); + } +} diff --git a/controllers/admin/AdminEmployeesController.php b/controllers/admin/AdminEmployeesController.php new file mode 100644 index 00000000..41ee64a5 --- /dev/null +++ b/controllers/admin/AdminEmployeesController.php @@ -0,0 +1,670 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Employee $object + */ +class AdminEmployeesControllerCore extends AdminController +{ + /** @var array profiles list */ + protected $profiles_array = array(); + + /** @var array tabs list */ + protected $tabs_list = array(); + + protected $restrict_edition = false; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'employee'; + $this->className = 'Employee'; + $this->lang = false; + + parent::__construct(); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->addRowActionSkipList('delete', array((int) $this->context->employee->id)); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + /* + check if there are more than one superAdmin + if it's the case then we can delete a superAdmin + */ + $super_admin = Employee::countProfile(_PS_ADMIN_PROFILE_, true); + if ($super_admin == 1) { + $super_admin_array = Employee::getEmployeesByProfile(_PS_ADMIN_PROFILE_, true); + $super_admin_id = array(); + foreach ($super_admin_array as $val) { + $super_admin_id[] = $val['id_employee']; + } + $this->addRowActionSkipList('delete', $super_admin_id); + } + + $profiles = Profile::getProfiles($this->context->language->id); + if (!$profiles) { + $this->errors[] = $this->trans('No profile.', array(), 'Admin.Notifications.Error'); + } else { + foreach ($profiles as $profile) { + $this->profiles_array[$profile['name']] = $profile['name']; + } + } + + $this->fields_list = array( + 'id_employee' => array('title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'firstname' => array( + 'title' => $this->trans('First name', array(), 'Admin.Global'), + 'maxlength' => 30, + ), + 'lastname' => array( + 'title' => $this->trans('Last name', array(), 'Admin.Global'), + 'maxlength' => 30, + ), + 'email' => array( + 'title' => $this->trans('Email address', array(), 'Admin.Global'), + 'maxlength' => 50, + ), + 'profile' => array( + 'title' => $this->trans('Profile', array(), 'Admin.Advparameters.Feature'), + 'type' => 'select', + 'list' => $this->profiles_array, + 'filter_key' => 'pl!name', + 'class' => 'fixed-width-lg', + ), + 'active' => array( + 'title' => $this->trans('Active', array(), 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'class' => 'fixed-width-sm', + ), + ); + + $this->fields_options = array( + 'general' => array( + 'title' => $this->trans('Employee options', array(), 'Admin.Advparameters.Feature'), + 'fields' => array( + 'PS_PASSWD_TIME_BACK' => array( + 'title' => $this->trans('Password regeneration', array(), 'Admin.Advparameters.Feature'), + 'hint' => $this->trans('Security: Minimum time to wait between two password changes.', array(), 'Admin.Advparameters.Feature'), + 'cast' => 'intval', + 'type' => 'text', + 'suffix' => ' ' . $this->trans('minutes', array(), 'Admin.Advparameters.Feature'), + 'visibility' => Shop::CONTEXT_ALL, + ), + 'PS_BO_ALLOW_EMPLOYEE_FORM_LANG' => array( + 'title' => $this->trans('Memorize the language used in Admin panel forms', array(), 'Admin.Advparameters.Feature'), + 'hint' => $this->trans('Allow employees to select a specific language for the Admin panel form.', array(), 'Admin.Advparameters.Feature'), + 'cast' => 'intval', + 'type' => 'select', + 'identifier' => 'value', + 'list' => array( + '0' => array('value' => 0, 'name' => $this->trans('No', array(), 'Admin.Global')), + '1' => array('value' => 1, 'name' => $this->trans('Yes', array(), 'Admin.Global'), + ), + ), 'visibility' => Shop::CONTEXT_ALL, ), + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ), + ); + + $home_tab = Tab::getInstanceFromClassName('AdminDashboard', $this->context->language->id); + $this->tabs_list[$home_tab->id] = array( + 'name' => $home_tab->name, + 'id_tab' => $home_tab->id, + 'children' => array(array( + 'id_tab' => $home_tab->id, + 'name' => $home_tab->name, + )), + ); + foreach (Tab::getTabs($this->context->language->id, 0) as $tab) { + if (Tab::checkTabRights($tab['id_tab'])) { + $this->tabs_list[$tab['id_tab']] = $tab; + foreach (Tab::getTabs($this->context->language->id, $tab['id_tab']) as $children) { + if (Tab::checkTabRights($children['id_tab'])) { + foreach (Tab::getTabs($this->context->language->id, $children['id_tab']) as $subchild) { + if (Tab::checkTabRights($subchild['id_tab'])) { + $this->tabs_list[$tab['id_tab']]['children'][] = $subchild; + } + } + } + } + } + } + + // An employee can edit its own profile + if ($this->context->employee->id == Tools::getValue('id_employee')) { + $this->tabAccess['view'] = '1'; + $this->restrict_edition = true; + $this->tabAccess['edit'] = '1'; + } + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/js/vendor/jquery-passy.js'); + $this->addjQueryPlugin('validate'); + $this->addJS(_PS_JS_DIR_ . 'jquery/plugins/validate/localization/messages_' . $this->context->language->iso_code . '.js'); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_employee'] = array( + 'href' => self::$currentIndex . '&addemployee&token=' . $this->token, + 'desc' => $this->trans('Add new employee', array(), 'Admin.Advparameters.Feature'), + 'icon' => 'process-icon-new', + ); + } + + if ($this->display == 'edit') { + $obj = $this->loadObject(true); + if (Validate::isLoadedObject($obj)) { + /* @var Employee $obj */ + array_pop($this->toolbar_title); + $this->toolbar_title[] = $this->trans('Edit: %lastname% %firstname%', + array( + '%lastname%' => $obj->lastname, + '%firstname%' => $obj->firstname, + ), + 'Admin.Advparameters.Feature' + ); + $this->page_header_toolbar_title = implode(' ' . Configuration::get('PS_NAVIGATION_PIPE') . ' ', + $this->toolbar_title); + } + } + } + + public function renderList() + { + $this->_select = 'pl.`name` AS profile'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'profile` p ON a.`id_profile` = p.`id_profile` + LEFT JOIN `' . _DB_PREFIX_ . 'profile_lang` pl ON (pl.`id_profile` = p.`id_profile` AND pl.`id_lang` = ' + . (int) $this->context->language->id . ')'; + $this->_use_found_rows = false; + + return parent::renderList(); + } + + public function renderForm() + { + /** @var Employee $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + + $available_profiles = Profile::getProfiles($this->context->language->id); + + if ($obj->id_profile == _PS_ADMIN_PROFILE_ && $this->context->employee->id_profile != _PS_ADMIN_PROFILE_) { + $this->errors[] = $this->trans('You cannot edit the SuperAdmin profile.', array(), 'Admin.Advparameters.Notification'); + + return parent::renderForm(); + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Employees', array(), 'Admin.Advparameters.Feature'), + 'icon' => 'icon-user', + ), + 'input' => array( + array( + 'type' => 'text', + 'class' => 'fixed-width-xl', + 'label' => $this->trans('First name', array(), 'Admin.Global'), + 'name' => 'firstname', + 'required' => true, + ), + array( + 'type' => 'text', + 'class' => 'fixed-width-xl', + 'label' => $this->trans('Last name', array(), 'Admin.Global'), + 'name' => 'lastname', + 'required' => true, + ), + array( + 'type' => 'html', + 'name' => 'employee_avatar', + 'html_content' => '
+
' . $this->trans( + 'Your avatar in PrestaShop 1.7.x is your profile picture on %url%. To change your avatar, log in to PrestaShop.com with your email %email% and follow the on-screen instructions.', + array( + '%url%' => 'PrestaShop.com', + '%email%' => $obj->email, + ), + 'Admin.Advparameters.Help' + ) . ' +
', + ), + array( + 'type' => 'text', + 'class' => 'fixed-width-xxl', + 'prefix' => '', + 'label' => $this->trans('Email address', array(), 'Admin.Global'), + 'name' => 'email', + 'required' => true, + 'autocomplete' => false, + ), + ), + ); + + if ($this->restrict_edition) { + $this->fields_form['input'][] = array( + 'type' => 'change-password', + 'label' => $this->trans('Password', array(), 'Admin.Global'), + 'name' => 'passwd', + ); + + if (Tab::checkTabRights(Tab::getIdFromClassName('AdminModulesController'))) { + $this->fields_form['input'][] = array( + 'type' => 'prestashop_addons', + 'label' => 'PrestaShop Addons', + 'name' => 'prestashop_addons', + ); + } + } else { + $this->fields_form['input'][] = array( + 'type' => 'password', + 'label' => $this->trans('Password', array(), 'Admin.Global'), + 'hint' => $this->trans('Password should be at least %num% characters long.', array('%num%' => Validate::ADMIN_PASSWORD_LENGTH), 'Admin.Advparameters.Help'), + 'name' => 'passwd', + ); + } + + $this->fields_form['input'] = array_merge($this->fields_form['input'], array( + array( + 'type' => 'switch', + 'label' => $this->trans('Subscribe to PrestaShop newsletter', array(), 'Admin.Advparameters.Feature'), + 'name' => 'optin', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'optin_on', + 'value' => 1, + 'label' => $this->trans('Yes', array(), 'Admin.Global'), + ), + array( + 'id' => 'optin_off', + 'value' => 0, + 'label' => $this->trans('No', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('PrestaShop can provide you with guidance on a regular basis by sending you tips on how to optimize the management of your store which will help you grow your business. If you do not wish to receive these tips, you can disable this option.', array(), 'Admin.Advparameters.Help'), + ), + array( + 'type' => 'default_tab', + 'label' => $this->trans('Default page', array(), 'Admin.Advparameters.Feature'), + 'name' => 'default_tab', + 'hint' => $this->trans('This page will be displayed just after login.', array(), 'Admin.Advparameters.Help'), + 'options' => $this->tabs_list, + ), + array( + 'type' => 'select', + 'label' => $this->trans('Language', array(), 'Admin.Global'), + 'name' => 'id_lang', + //'required' => true, + 'options' => array( + 'query' => Language::getLanguages(false), + 'id' => 'id_lang', + 'name' => 'name', + ), + ), + )); + + if ((int) $this->access('edit') && !$this->restrict_edition) { + $this->fields_form['input'][] = array( + 'type' => 'switch', + 'label' => $this->trans('Active', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Allow or disallow this employee to log in to the Admin panel.', array(), 'Admin.Advparameters.Help'), + ); + + // if employee is not SuperAdmin (id_profile = 1), don't make it possible to select the admin profile + if ($this->context->employee->id_profile != _PS_ADMIN_PROFILE_) { + foreach ($available_profiles as $i => $profile) { + if ($available_profiles[$i]['id_profile'] == _PS_ADMIN_PROFILE_) { + unset($available_profiles[$i]); + break; + } + } + } + $this->fields_form['input'][] = array( + 'type' => 'select', + 'label' => $this->trans('Permission profile', array(), 'Admin.Advparameters.Feature'), + 'name' => 'id_profile', + 'required' => true, + 'options' => array( + 'query' => $available_profiles, + 'id' => 'id_profile', + 'name' => 'name', + 'default' => array( + 'value' => '', + 'label' => $this->trans('-- Choose --', array(), 'Admin.Advparameters.Help'), + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->context->smarty->assign('_PS_ADMIN_PROFILE_', (int) _PS_ADMIN_PROFILE_); + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'hint' => $this->trans('Select the shops the employee is allowed to access.', array(), 'Admin.Advparameters.Help'), + 'name' => 'checkBoxShopAsso', + ); + } + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + $this->fields_value['passwd'] = false; + $this->fields_value['bo_theme_css'] = $obj->bo_theme . '|' . $obj->bo_css; + + if (empty($obj->id)) { + $this->fields_value['id_lang'] = $this->context->language->id; + } + + return parent::renderForm(); + } + + protected function _childValidation() + { + if (!($obj = $this->loadObject(true))) { + return false; + } + + if (Tools::getValue('id_profile') == _PS_ADMIN_PROFILE_ && $this->context->employee->id_profile != _PS_ADMIN_PROFILE_) { + $this->errors[] = $this->trans('The provided profile is invalid', array(), 'Admin.Advparameters.Notification'); + } + + $email = $this->getFieldValue($obj, 'email'); + if (Validate::isEmail($email) && Employee::employeeExists($email) && (!Tools::getValue('id_employee') + || ($employee = new Employee((int) Tools::getValue('id_employee'))) && $employee->email != $email)) { + $this->errors[] = $this->trans('An account already exists for this email address:', array(), 'Admin.Orderscustomers.Notification') . ' ' . $email; + } + } + + public function processDelete() + { + if (!$this->canModifyEmployee()) { + return false; + } + + return parent::processDelete(); + } + + public function processStatus() + { + if (!$this->canModifyEmployee()) { + return false; + } + + parent::processStatus(); + } + + protected function processBulkDelete() + { + if (is_array($this->boxes) && !empty($this->boxes)) { + foreach ($this->boxes as $id_employee) { + if ((int) $this->context->employee->id == (int) $id_employee) { + $this->restrict_edition = true; + + return $this->canModifyEmployee(); + } + } + } + + return parent::processBulkDelete(); + } + + protected function canModifyEmployee() + { + if ($this->restrict_edition) { + $this->errors[] = $this->trans('You cannot disable or delete your own account.', array(), 'Admin.Advparameters.Notification'); + + return false; + } + + $employee = new Employee(Tools::getValue('id_employee')); + if ($employee->isLastAdmin()) { + $this->errors[] = $this->trans('You cannot disable or delete the administrator account.', array(), 'Admin.Advparameters.Notification'); + + return false; + } + + // It is not possible to delete an employee if he manages warehouses + $warehouses = Warehouse::getWarehousesByEmployee((int) Tools::getValue('id_employee')); + if (Tools::isSubmit('deleteemployee') && count($warehouses) > 0) { + $this->errors[] = $this->trans('You cannot delete this account because it manages warehouses. Check your warehouses first.', array(), 'Admin.Advparameters.Notification'); + + return false; + } + + return true; + } + + public function processSave() + { + $employee = new Employee((int) Tools::getValue('id_employee')); + + // If the employee is editing its own account + if ($this->restrict_edition) { + $current_password = trim(Tools::getValue('old_passwd')); + if (Tools::getValue('passwd') && (empty($current_password) || !Validate::isPasswdAdmin($current_password) || !$employee->getByEmail($employee->email, $current_password))) { + $this->errors[] = $this->trans('Your current password is invalid.', array(), 'Admin.Advparameters.Notification'); + } elseif (Tools::getValue('passwd') && (!Tools::getValue('passwd2') || Tools::getValue('passwd') !== Tools::getValue('passwd2'))) { + $this->errors[] = $this->trans('The confirmation password does not match.', array(), 'Admin.Advparameters.Notification'); + } + + $_POST['id_profile'] = $_GET['id_profile'] = $employee->id_profile; + $_POST['active'] = $_GET['active'] = $employee->active; + + // Unset set shops + foreach ($_POST as $postkey => $postvalue) { + if (strstr($postkey, 'checkBoxShopAsso_' . $this->table) !== false) { + unset($_POST[$postkey]); + } + } + foreach ($_GET as $postkey => $postvalue) { + if (strstr($postkey, 'checkBoxShopAsso_' . $this->table) !== false) { + unset($_GET[$postkey]); + } + } + + // Add current shops associated to the employee + $result = Shop::getShopById((int) $employee->id, $this->identifier, $this->table); + foreach ($result as $row) { + $key = 'checkBoxShopAsso_' . $this->table; + if (!isset($_POST[$key])) { + $_POST[$key] = array(); + } + if (!isset($_GET[$key])) { + $_GET[$key] = array(); + } + $_POST[$key][$row['id_shop']] = 1; + $_GET[$key][$row['id_shop']] = 1; + } + } else { + $_POST['id_last_order'] = $employee->getLastElementsForNotify('order'); + $_POST['id_last_customer_message'] = $employee->getLastElementsForNotify('customer_message'); + $_POST['id_last_customer'] = $employee->getLastElementsForNotify('customer'); + } + + //if profile is super admin, manually fill checkBoxShopAsso_employee because in the form they are disabled. + if ($_POST['id_profile'] == _PS_ADMIN_PROFILE_) { + $result = Db::getInstance()->executeS('SELECT id_shop FROM ' . _DB_PREFIX_ . 'shop'); + foreach ($result as $row) { + $key = 'checkBoxShopAsso_' . $this->table; + if (!isset($_POST[$key])) { + $_POST[$key] = array(); + } + if (!isset($_GET[$key])) { + $_GET[$key] = array(); + } + $_POST[$key][$row['id_shop']] = 1; + $_GET[$key][$row['id_shop']] = 1; + } + } + + if ($employee->isLastAdmin()) { + if (Tools::getValue('id_profile') != (int) _PS_ADMIN_PROFILE_) { + $this->errors[] = $this->trans('You should have at least one employee in the administrator group.', array(), 'Admin.Advparameters.Notification'); + + return false; + } + + if (Tools::getValue('active') == 0) { + $this->errors[] = $this->trans('You cannot disable or delete the administrator account.', array(), 'Admin.Advparameters.Notification'); + + return false; + } + } + + $assos = $this->getSelectedAssoShop($this->table); + if (!$assos && $this->table = 'employee') { + if (Shop::isFeatureActive() && _PS_ADMIN_PROFILE_ != $_POST['id_profile']) { + $this->errors[] = $this->trans('The employee must be associated with at least one shop.', array(), 'Admin.Advparameters.Notification'); + } + } + + if (count($this->errors)) { + return false; + } + + return parent::processSave(); + } + + public function validateRules($class_name = false) + { + $employee = new Employee((int) Tools::getValue('id_employee')); + + if (!Validate::isLoadedObject($employee) && !Validate::isPasswd(Tools::getValue('passwd'), Validate::ADMIN_PASSWORD_LENGTH)) { + return !($this->errors[] = $this->trans('The password must be at least %length% characters long.', array('%length%' => Validate::ADMIN_PASSWORD_LENGTH), 'Admin.Advparameters.Notification')); + } + + return parent::validateRules($class_name); + } + + public function postProcess() + { + /* PrestaShop demo mode */ + if ((Tools::isSubmit('submitBulkdeleteemployee') || Tools::isSubmit('submitBulkdisableSelectionemployee') || Tools::isSubmit('deleteemployee') || Tools::isSubmit('status') || Tools::isSubmit('statusemployee') || Tools::isSubmit('submitAddemployee')) && _PS_MODE_DEMO_) { + $this->errors[] = $this->trans('This functionality has been disabled.', array(), 'Admin.Notifications.Error'); + + return; + } + + return parent::postProcess(); + } + + public function initContent() + { + if ($this->context->employee->id == Tools::getValue('id_employee')) { + $this->display = 'edit'; + } + + return parent::initContent(); + } + + /** + * @param Employee $object + * + * @return bool + */ + protected function afterUpdate($object) + { + $res = parent::afterUpdate($object); + // Update cookie if needed + if (Tools::getValue('id_employee') == $this->context->employee->id && Tools::getValue('passwd') + && $object->passwd != $this->context->employee->passwd) { + $this->context->cookie->passwd = $this->context->employee->passwd = $object->passwd; + } + + return $res; + } + + protected function ajaxProcessFormLanguage() + { + $this->context->cookie->employee_form_lang = (int) Tools::getValue('form_language_id'); + if (!$this->context->cookie->write()) { + die('Error while updating cookie.'); + } + die('Form language updated.'); + } + + protected function ajaxProcessToggleMenu() + { + $this->context->cookie->collapse_menu = (int) Tools::getValue('collapse'); + $this->context->cookie->write(); + } + + public function ajaxProcessGetTabByIdProfile() + { + $id_profile = Tools::getValue('id_profile'); + $tabs = Tab::getTabByIdProfile(0, $id_profile); + $this->tabs_list = array(); + foreach ($tabs as $tab) { + if (Tab::checkTabRights($tab['id_tab'])) { + $this->tabs_list[$tab['id_tab']] = $tab; + foreach (Tab::getTabByIdProfile($tab['id_tab'], $id_profile) as $children) { + if (Tab::checkTabRights($children['id_tab'])) { + $this->tabs_list[$tab['id_tab']]['children'][] = $children; + } + } + } + } + die(json_encode($this->tabs_list)); + } +} diff --git a/controllers/admin/AdminFeaturesController.php b/controllers/admin/AdminFeaturesController.php new file mode 100644 index 00000000..fcb4602e --- /dev/null +++ b/controllers/admin/AdminFeaturesController.php @@ -0,0 +1,637 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Feature $object + */ +class AdminFeaturesControllerCore extends AdminController +{ + public $bootstrap = true; + protected $position_identifier = 'id_feature'; + protected $feature_name; + + public function __construct() + { + $this->table = 'feature'; + $this->className = 'Feature'; + $this->list_id = 'feature'; + $this->identifier = 'id_feature'; + $this->lang = true; + + parent::__construct(); + + $this->fields_list = array( + 'id_feature' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + 'width' => 'auto', + 'filter_key' => 'b!name', + ), + 'value' => array( + 'title' => $this->trans('Values', array(), 'Admin.Global'), + 'orderby' => false, + 'search' => false, + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'position' => array( + 'title' => $this->trans('Position', array(), 'Admin.Global'), + 'filter_key' => 'a!position', + 'align' => 'center', + 'class' => 'fixed-width-xs', + 'position' => 'position', + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + ), + ); + } + + /** + * AdminController::renderList() override. + * + * @see AdminController::renderList() + */ + public function renderList() + { + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + return parent::renderList(); + } + + /** + * Change object type to feature value (use when processing a feature value). + */ + protected function setTypeValue() + { + $this->table = 'feature_value'; + $this->className = 'FeatureValue'; + $this->identifier = 'id_feature_value'; + } + + /** + * Change object type to feature (use when processing a feature). + */ + protected function setTypeFeature() + { + $this->table = 'feature'; + $this->className = 'Feature'; + $this->identifier = 'id_feature'; + } + + public function renderView() + { + if (($id = Tools::getValue('id_feature'))) { + $this->setTypeValue(); + $this->list_id = 'feature_value'; + $this->lang = true; + + // Action for list + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + if (!Validate::isLoadedObject($obj = new Feature((int) $id))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + + return; + } + + $this->feature_name = $obj->name; + $this->toolbar_title = $this->feature_name[$this->context->employee->id_lang]; + $this->fields_list = array( + 'id_feature_value' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'value' => array( + 'title' => $this->trans('Value', array(), 'Admin.Global'), + ), + ); + + $this->_where = sprintf('AND `id_feature` = %d', (int) $id); + self::$currentIndex = self::$currentIndex . '&id_feature=' . (int) $id . '&viewfeature'; + $this->processFilter(); + + return parent::renderList(); + } + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $this->toolbar_title = $this->trans('Add a new feature', array(), 'Admin.Catalog.Feature'); + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Feature', array(), 'Admin.Catalog.Feature'), + 'icon' => 'icon-info-sign', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'size' => 33, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + 'required' => true, + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + return parent::renderForm(); + } + + public function initPageHeaderToolbar() + { + if (Feature::isFeatureActive()) { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_feature'] = array( + 'href' => self::$currentIndex . '&addfeature&token=' . $this->token, + 'desc' => $this->trans('Add new feature', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + + $this->page_header_toolbar_btn['new_feature_value'] = array( + 'href' => self::$currentIndex . '&addfeature_value&id_feature=' . (int) Tools::getValue('id_feature') . '&token=' . $this->token, + 'desc' => $this->trans('Add new feature value', array(), 'Admin.Catalog.Help'), + 'icon' => 'process-icon-new', + ); + } + } + + if ($this->display == 'view') { + $this->page_header_toolbar_btn['new_feature_value'] = array( + 'href' => self::$currentIndex . '&addfeature_value&id_feature=' . (int) Tools::getValue('id_feature') . '&token=' . $this->token, + 'desc' => $this->trans('Add new feature value', array(), 'Admin.Catalog.Help'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + /** + * AdminController::initToolbar() override. + * + * @see AdminController::initToolbar() + */ + public function initToolbar() + { + switch ($this->display) { + case 'editFeatureValue': + case 'add': + case 'edit': + $this->toolbar_btn['save'] = array( + 'href' => '#', + 'desc' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + if ($this->display == 'editFeatureValue') { + $this->toolbar_btn['save-and-stay'] = array( + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save and add another value', array(), 'Admin.Catalog.Help'), + 'force_desc' => true, + ); + } + + // Default cancel button - like old back link + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + + $this->toolbar_btn['back'] = array( + 'href' => $back, + 'desc' => $this->trans('Back to the list', array(), 'Admin.Catalog.Help'), + ); + break; + case 'view': + $this->toolbar_btn['newAttributes'] = array( + 'href' => self::$currentIndex . '&addfeature_value&id_feature=' . (int) Tools::getValue('id_feature') . '&token=' . $this->token, + 'desc' => $this->trans('Add new feature values', array(), 'Admin.Catalog.Help'), + ); + $this->toolbar_btn['back'] = array( + 'href' => self::$currentIndex . '&token=' . $this->token, + 'desc' => $this->trans('Back to the list', array(), 'Admin.Catalog.Help'), + ); + break; + default: + parent::initToolbar(); + } + } + + public function initToolbarTitle() + { + $bread_extended = $this->breadcrumbs; + + switch ($this->display) { + case 'edit': + $bread_extended[] = $this->trans('Edit New Feature', array(), 'Admin.Catalog.Feature'); + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + break; + + case 'add': + $bread_extended[] = $this->trans('Add New Feature', array(), 'Admin.Catalog.Feature'); + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + break; + + case 'view': + $bread_extended[] = $this->feature_name[$this->context->employee->id_lang]; + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + break; + + case 'editFeatureValue': + if (Tools::getValue('id_feature_value')) { + if (($id = Tools::getValue('id_feature'))) { + if (Validate::isLoadedObject($obj = new Feature((int) $id))) { + $bread_extended[] = '' . $obj->name[$this->context->employee->id_lang] . ''; + } + + if (Validate::isLoadedObject($obj = new FeatureValue((int) Tools::getValue('id_feature_value')))) { + $bread_extended[] = $this->trans('Edit: %value%', array('%value%' => $obj->value[$this->context->employee->id_lang]), 'Admin.Catalog.Feature'); + } + } else { + $bread_extended[] = $this->trans('Edit Value', array(), 'Admin.Catalog.Feature'); + } + } else { + $bread_extended[] = $this->trans('Add New Value', array(), 'Admin.Catalog.Feature'); + } + + if (count($bread_extended) > 0) { + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + } + break; + } + + $this->toolbar_title = $bread_extended; + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function initFormFeatureValue() + { + $this->setTypeValue(); + + $this->fields_form[0]['form'] = array( + 'legend' => array( + 'title' => $this->trans('Feature value', array(), 'Admin.Catalog.Feature'), + 'icon' => 'icon-info-sign', + ), + 'input' => array( + array( + 'type' => 'select', + 'label' => $this->trans('Feature', array(), 'Admin.Catalog.Feature'), + 'name' => 'id_feature', + 'options' => array( + 'query' => Feature::getFeatures($this->context->language->id), + 'id' => 'id_feature', + 'name' => 'name', + ), + 'required' => true, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Value', array(), 'Admin.Global'), + 'name' => 'value', + 'lang' => true, + 'size' => 33, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + 'required' => true, + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + 'buttons' => array( + 'save-and-stay' => array( + 'title' => $this->trans('Save then add another value', array(), 'Admin.Catalog.Feature'), + 'name' => 'submitAdd' . $this->table . 'AndStay', + 'type' => 'submit', + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-save', + ), + ), + ); + + $this->fields_value['id_feature'] = (int) Tools::getValue('id_feature'); + + // Create Object FeatureValue + $feature_value = new FeatureValue(Tools::getValue('id_feature_value')); + + $this->tpl_vars = array( + 'feature_value' => $feature_value, + ); + + $this->getlanguages(); + $helper = new HelperForm(); + $helper->show_cancel_button = true; + + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + if (!Validate::isCleanHtml($back)) { + die(Tools::displayError()); + } + + $helper->back_url = $back; + $helper->currentIndex = self::$currentIndex; + $helper->token = $this->token; + $helper->table = $this->table; + $helper->identifier = $this->identifier; + $helper->override_folder = 'feature_value/'; + $helper->id = $feature_value->id; + $helper->toolbar_scroll = false; + $helper->tpl_vars = $this->tpl_vars; + $helper->languages = $this->_languages; + $helper->default_form_language = $this->default_form_language; + $helper->allow_employee_form_lang = $this->allow_employee_form_lang; + $helper->fields_value = $this->getFieldsValue($feature_value); + $helper->toolbar_btn = $this->toolbar_btn; + $helper->title = $this->trans('Add a new feature value', array(), 'Admin.Catalog.Feature'); + $this->content .= $helper->generateForm($this->fields_form); + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + if (Feature::isFeatureActive()) { + if ($this->display == 'edit' || $this->display == 'add') { + if (!$this->loadObject(true)) { + return; + } + $this->content .= $this->renderForm(); + } elseif ($this->display == 'view') { + // Some controllers use the view action without an object + if ($this->className) { + $this->loadObject(true); + } + $this->content .= $this->renderView(); + } elseif ($this->display == 'editFeatureValue') { + if (!$this->object = new FeatureValue((int) Tools::getValue('id_feature_value'))) { + return; + } + $this->content .= $this->initFormFeatureValue(); + } elseif (!$this->ajax) { + // If a feature value was saved, we need to reset the values to display the list + $this->setTypeFeature(); + $this->content .= $this->renderList(); + } + } else { + $adminPerformanceUrl = $this->context->link->getAdminLink('AdminPerformance'); + $url = '' . $this->trans('Performance', array(), 'Admin.Global') . ''; + $this->displayWarning($this->trans('This feature has been disabled. You can activate it here: %url%.', array('%url%' => $url), 'Admin.Catalog.Notification')); + } + + $this->context->smarty->assign(array( + 'content' => $this->content, + )); + } + + public function initProcess() + { + // Are we working on feature values? + if (Tools::getValue('id_feature_value') + || Tools::isSubmit('deletefeature_value') + || Tools::isSubmit('submitAddfeature_value') + || Tools::isSubmit('addfeature_value') + || Tools::isSubmit('updatefeature_value') + || Tools::isSubmit('submitBulkdeletefeature_value')) { + $this->setTypeValue(); + } + + if (Tools::getIsset('viewfeature')) { + $this->list_id = 'feature_value'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + } else { + $this->list_id = 'feature'; + $this->_defaultOrderBy = 'position'; + $this->_defaultOrderWay = 'ASC'; + } + + parent::initProcess(); + } + + public function postProcess() + { + if (!Feature::isFeatureActive()) { + return; + } + + if ($this->table == 'feature_value' && ($this->action == 'save' || $this->action == 'delete' || $this->action == 'bulkDelete')) { + Hook::exec('displayFeatureValuePostProcess', + array('errors' => &$this->errors)); + } // send errors as reference to allow displayFeatureValuePostProcess to stop saving process + else { + Hook::exec('displayFeaturePostProcess', + array('errors' => &$this->errors)); + } // send errors as reference to allow displayFeaturePostProcess to stop saving process + + parent::postProcess(); + + if ($this->table == 'feature_value' && ($this->display == 'edit' || $this->display == 'add')) { + $this->display = 'editFeatureValue'; + } + } + + /** + * Override processAdd to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processAdd() + */ + public function processAdd() + { + $object = parent::processAdd(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + if ($this->table == 'feature_value' && ($this->display == 'edit' || $this->display == 'add')) { + $this->redirect_after = self::$currentIndex . '&addfeature_value&id_feature=' . (int) Tools::getValue('id_feature') . '&token=' . $this->token; + } else { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } + } elseif (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && count($this->errors)) { + $this->display = 'editFeatureValue'; + } + + return $object; + } + + /** + * Override processUpdate to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processUpdate() + */ + public function processUpdate() + { + $object = parent::processUpdate(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } + + return $object; + } + + /** + * Call the right method for creating or updating object. + * + * @return mixed + */ + public function processSave() + { + if ($this->table == 'feature') { + $id_feature = (int) Tools::getValue('id_feature'); + // Adding last position to the feature if not exist + if ($id_feature <= 0) { + $sql = 'SELECT `position`+1 + FROM `' . _DB_PREFIX_ . 'feature` + ORDER BY position DESC'; + // set the position of the new feature in $_POST for postProcess() method + $_POST['position'] = Db::getInstance()->getValue($sql); + } + // clean \n\r characters + foreach ($_POST as $key => $value) { + if (preg_match('/^name_/Ui', $key)) { + $_POST[$key] = str_replace('\n', '', str_replace('\r', '', $value)); + } + } + } + + return parent::processSave(); + } + + /** + * AdminController::getList() override. + * + * @see AdminController::getList() + * + * @param int $id_lang + * @param string|null $order_by + * @param string|null $order_way + * @param int $start + * @param int|null $limit + * @param int|bool $id_lang_shop + * + * @throws PrestaShopException + */ + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + if ($this->table == 'feature_value') { + $this->_where .= ' AND (a.custom = 0 OR a.custom IS NULL)'; + } + + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + if ($this->table == 'feature') { + $nb_items = count($this->_list); + for ($i = 0; $i < $nb_items; ++$i) { + $item = &$this->_list[$i]; + + $query = new DbQuery(); + $query->select('COUNT(fv.id_feature_value) as count_values'); + $query->from('feature_value', 'fv'); + $query->where('fv.id_feature =' . (int) $item['id_feature']); + $query->where('(fv.custom=0 OR fv.custom IS NULL)'); + $res = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query); + $item['value'] = (int) $res; + unset($query); + } + } + } + + public function ajaxProcessUpdatePositions() + { + if ($this->access('edit')) { + $way = (int) Tools::getValue('way'); + $id_feature = (int) Tools::getValue('id'); + $positions = Tools::getValue('feature'); + + $new_positions = array(); + foreach ($positions as $v) { + if (!empty($v)) { + $new_positions[] = $v; + } + } + + foreach ($new_positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_feature) { + if ($feature = new Feature((int) $pos[2])) { + if (isset($position) && $feature->updatePosition($way, $position, $id_feature)) { + echo 'ok position ' . (int) $position . ' for feature ' . (int) $pos[1] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update feature ' . (int) $id_feature . ' to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "This feature (' . (int) $id_feature . ') can t be loaded"}'; + } + + break; + } + } + } + } +} diff --git a/controllers/admin/AdminGendersController.php b/controllers/admin/AdminGendersController.php new file mode 100644 index 00000000..1a398181 --- /dev/null +++ b/controllers/admin/AdminGendersController.php @@ -0,0 +1,232 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Gender $object + */ +class AdminGendersControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'gender'; + $this->className = 'Gender'; + $this->lang = true; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + parent::__construct(); + + if (!Tools::getValue('realedit')) { + $this->deleted = false; + } + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->default_image_height = 16; + $this->default_image_width = 16; + + $this->fieldImageSettings = array( + 'name' => 'image', + 'dir' => 'genders', + ); + + $this->fields_list = array( + 'id_gender' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Social title', array(), 'Admin.Shopparameters.Feature'), + 'filter_key' => 'b!name', + ), + 'type' => array( + 'title' => $this->trans('Gender', array(), 'Admin.Global'), + 'orderby' => false, + 'type' => 'select', + 'list' => array( + 0 => $this->trans('Male', array(), 'Admin.Shopparameters.Feature'), + 1 => $this->trans('Female', array(), 'Admin.Shopparameters.Feature'), + 2 => $this->trans('Neutral', array(), 'Admin.Shopparameters.Feature'), + ), + 'filter_key' => 'a!type', + 'callback' => 'displayGenderType', + 'callback_object' => $this, + ), + 'image' => array( + 'title' => $this->trans('Image', array(), 'Admin.Global'), + 'align' => 'center', + 'image' => 'genders', + 'orderby' => false, + 'search' => false, + ), + ); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_gender'] = array( + 'href' => self::$currentIndex . '&addgender&token=' . $this->token, + 'desc' => $this->trans('Add new social title', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Social titles', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-male', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Social title', array(), 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'col' => 4, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Shopparameters.Help') . ' 0-9!<>,;?=+()@#"�{}_$%:', + 'required' => true, + ), + array( + 'type' => 'radio', + 'label' => $this->trans('Gender', array(), 'Admin.Global'), + 'name' => 'type', + 'required' => false, + 'class' => 't', + 'values' => array( + array( + 'id' => 'type_male', + 'value' => 0, + 'label' => $this->trans('Male', array(), 'Admin.Shopparameters.Feature'), + ), + array( + 'id' => 'type_female', + 'value' => 1, + 'label' => $this->trans('Female', array(), 'Admin.Shopparameters.Feature'), + ), + array( + 'id' => 'type_neutral', + 'value' => 2, + 'label' => $this->trans('Neutral', array(), 'Admin.Shopparameters.Feature'), + ), + ), + ), + array( + 'type' => 'file', + 'label' => $this->trans('Image', array(), 'Admin.Global'), + 'name' => 'image', + 'col' => 6, + 'value' => true, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Image width', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'img_width', + 'col' => 2, + 'hint' => $this->trans('Image width in pixels. Enter "0" to use the original size.', array(), 'Admin.Shopparameters.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Image height', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'img_height', + 'col' => 2, + 'hint' => $this->trans('Image height in pixels. Enter "0" to use the original size.', array(), 'Admin.Shopparameters.Help'), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + /** @var Gender $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_value = array( + 'img_width' => $this->default_image_width, + 'img_height' => $this->default_image_height, + 'image' => $obj->getImage(), + ); + + return parent::renderForm(); + } + + public function displayGenderType($value, $tr) + { + return $this->fields_list['type']['list'][$value]; + } + + protected function postImage($id) + { + if (isset($this->fieldImageSettings['name']) && isset($this->fieldImageSettings['dir'])) { + if (!Validate::isInt(Tools::getValue('img_width')) || !Validate::isInt(Tools::getValue('img_height'))) { + $this->errors[] = $this->trans('Width and height must be numeric values.', array(), 'Admin.Shopparameters.Notification'); + } else { + if ((int) Tools::getValue('img_width') > 0 && (int) Tools::getValue('img_height') > 0) { + $width = (int) Tools::getValue('img_width'); + $height = (int) Tools::getValue('img_height'); + } else { + $width = null; + $height = null; + } + + return $this->uploadImage($id, $this->fieldImageSettings['name'], $this->fieldImageSettings['dir'] . '/', false, $width, $height); + } + } + + return !count($this->errors) ? true : false; + } + + protected function afterImageUpload() + { + parent::afterImageUpload(); + + if (($id_gender = (int) Tools::getValue('id_gender')) && + isset($_FILES) && count($_FILES) && file_exists(_PS_GENDERS_DIR_ . $id_gender . '.jpg')) { + $current_file = _PS_TMP_IMG_DIR_ . 'gender_mini_' . $id_gender . '_' . $this->context->shop->id . '.jpg'; + + if (file_exists($current_file)) { + unlink($current_file); + } + } + + return true; + } +} diff --git a/controllers/admin/AdminGroupsController.php b/controllers/admin/AdminGroupsController.php new file mode 100644 index 00000000..b4235c43 --- /dev/null +++ b/controllers/admin/AdminGroupsController.php @@ -0,0 +1,645 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Group $object + */ +class AdminGroupsControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'group'; + $this->className = 'Group'; + $this->list_id = 'group'; + $this->lang = true; + + parent::__construct(); + + $this->addRowAction('edit'); + $this->addRowAction('view'); + $this->addRowAction('delete'); + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $groups_to_keep = array( + Configuration::get('PS_UNIDENTIFIED_GROUP'), + Configuration::get('PS_GUEST_GROUP'), + Configuration::get('PS_CUSTOMER_GROUP'), + ); + + $this->fields_list = array( + 'id_group' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Group name', array(), 'Admin.Shopparameters.Feature'), + 'filter_key' => 'b!name', + ), + 'reduction' => array( + 'title' => $this->trans('Discount (%)', array(), 'Admin.Shopparameters.Feature'), + 'align' => 'right', + 'type' => 'percent', + ), + 'nb' => array( + 'title' => $this->trans('Members', array(), 'Admin.Shopparameters.Feature'), + 'align' => 'center', + 'havingFilter' => true, + ), + 'show_prices' => array( + 'title' => $this->trans('Show prices', array(), 'Admin.Shopparameters.Feature'), + 'align' => 'center', + 'type' => 'bool', + 'orderby' => false, + ), + 'date_add' => array( + 'title' => $this->trans('Creation date', array(), 'Admin.Shopparameters.Feature'), + 'type' => 'date', + 'align' => 'right', + ), + ); + + $this->addRowActionSkipList('delete', $groups_to_keep); + + $this->_select .= '(SELECT COUNT(jcg.`id_customer`) + FROM `' . _DB_PREFIX_ . 'customer_group` jcg + LEFT JOIN `' . _DB_PREFIX_ . 'customer` jc ON (jc.`id_customer` = jcg.`id_customer`) + WHERE jc.`deleted` != 1 + ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER) . ' + AND jcg.`id_group` = a.`id_group`) AS nb'; + $this->_use_found_rows = false; + + $groups = Group::getGroups(Context::getContext()->language->id, true); + + if (Group::isFeatureActive()) { + $this->fields_options = array( + 'general' => array( + 'title' => $this->trans('Default groups options', array(), 'Admin.Shopparameters.Feature'), + 'fields' => array( + 'PS_UNIDENTIFIED_GROUP' => array( + 'title' => $this->trans('Visitors group', array(), 'Admin.Shopparameters.Feature'), + 'desc' => $this->trans('The group defined for your un-identified visitors.', array(), 'Admin.Shopparameters.Help'), + 'cast' => 'intval', + 'type' => 'select', + 'list' => $groups, + 'identifier' => 'id_group', + ), + 'PS_GUEST_GROUP' => array( + 'title' => $this->trans('Guests group', array(), 'Admin.Shopparameters.Feature'), + 'desc' => $this->trans('The group defined for your identified guest customers (used in guest checkout).', array(), 'Admin.Shopparameters.Help'), + 'cast' => 'intval', + 'type' => 'select', + 'list' => $groups, + 'identifier' => 'id_group', + ), + 'PS_CUSTOMER_GROUP' => array( + 'title' => $this->trans('Customers group', array(), 'Admin.Shopparameters.Feature'), + 'desc' => $this->trans('The group defined for your identified registered customers.', array(), 'Admin.Shopparameters.Help'), + 'cast' => 'intval', + 'type' => 'select', + 'list' => $groups, + 'identifier' => 'id_group', + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ), + ); + } + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryPlugin('fancybox'); + $this->addJqueryUi('ui.sortable'); + } + + public function initToolbar() + { + if ($this->display == 'add' || $this->display == 'edit') { + $this->toolbar_btn['save-and-stay'] = array( + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save, then add a category reduction.', array(), 'Admin.Shopparameters.Feature'), + 'force_desc' => true, + ); + } + parent::initToolbar(); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_group'] = array( + 'href' => self::$currentIndex . '&addgroup&token=' . $this->token, + 'desc' => $this->trans('Add new group', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function initProcess() + { + $this->id_object = Tools::getValue('id_' . $this->table); + + if (Tools::isSubmit('changeShowPricesVal') && $this->id_object) { + $this->action = 'change_show_prices_val'; + } + + if (Tools::getIsset('viewgroup')) { + $this->list_id = 'customer_group'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + } else { + $this->list_id = 'group'; + } + + parent::initProcess(); + } + + public function renderView() + { + $this->context = Context::getContext(); + if (!($group = $this->loadObject(true))) { + return; + } + + $this->tpl_view_vars = array( + 'group' => $group, + 'language' => $this->context->language, + 'customerList' => $this->renderCustomersList($group), + 'categorieReductions' => $this->formatCategoryDiscountList($group->id), + ); + + return parent::renderView(); + } + + protected function renderCustomersList($group) + { + $genders = array(0 => '?'); + $genders_icon = array('default' => 'unknown.gif'); + foreach (Gender::getGenders() as $gender) { + /* @var Gender $gender */ + $genders_icon[$gender->id] = '../genders/' . (int) $gender->id . '.jpg'; + $genders[$gender->id] = $gender->name; + } + $this->table = 'customer_group'; + $this->lang = false; + $this->list_id = 'customer_group'; + $this->actions = array(); + $this->addRowAction('edit'); + $this->identifier = 'id_customer'; + $this->bulk_actions = false; + $this->list_no_link = true; + $this->explicitSelect = true; + + $this->fields_list = (array( + 'id_customer' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'filter_key' => 'c!id_customer', + 'class' => 'fixed-width-xs', + ), + 'id_gender' => array( + 'title' => $this->trans('Social title', array(), 'Admin.Global'), + 'icon' => $genders_icon, + 'list' => $genders, + ), + 'firstname' => array( + 'title' => $this->trans('First name', array(), 'Admin.Global'), + 'maxlength' => 30, + ), + 'lastname' => array( + 'title' => $this->trans('Last name', array(), 'Admin.Global'), + 'maxlength' => 30, + ), + 'email' => array( + 'title' => $this->trans('Email address', array(), 'Admin.Global'), + 'filter_key' => 'c!email', + 'orderby' => true, + 'maxlength' => 50, + ), + 'birthday' => array( + 'title' => $this->trans('Date of birth', array(), 'Admin.Global'), + 'type' => 'date', + 'class' => 'fixed-width-md', + 'align' => 'center', + ), + 'date_add' => array( + 'title' => $this->trans('Registration date', array(), 'Admin.Shopparameters.Feature'), + 'type' => 'date', + 'class' => 'fixed-width-md', + 'align' => 'center', + ), + 'active' => array( + 'title' => $this->trans('Enabled', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-sm', + 'type' => 'bool', + 'search' => false, + 'orderby' => false, + 'filter_key' => 'c!active', + 'callback' => 'printOptinIcon', + ), + )); + $this->_select = 'c.*, a.id_group'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON (a.`id_customer` = c.`id_customer`)'; + $this->_where = 'AND a.`id_group` = ' . (int) $group->id . ' AND c.`deleted` != 1'; + $this->_where .= Shop::addSqlRestriction(Shop::SHARE_CUSTOMER, 'c'); + self::$currentIndex = self::$currentIndex . '&id_group=' . (int) $group->id . '&viewgroup'; + + $this->processFilter(); + + return parent::renderList(); + } + + public function printOptinIcon($value, $customer) + { + return $value ? '' : ''; + } + + public function renderForm() + { + if (!($group = $this->loadObject(true))) { + return; + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Customer group', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-group', + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'lang' => true, + 'col' => 4, + 'hint' => $this->trans('Forbidden characters:', array(), 'Admin.Notifications.Info') . ' 0-9!&lt;&gt;,;?=+()@#"�{}_$%:', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Discount', array(), 'Admin.Global'), + 'name' => 'reduction', + 'suffix' => '%', + 'col' => 1, + 'hint' => $this->trans('Automatically apply this value as a discount on all products for members of this customer group.', array(), 'Admin.Shopparameters.Help'), + ), + array( + 'type' => 'select', + 'label' => $this->trans('Price display method', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'price_display_method', + 'col' => 2, + 'hint' => $this->trans('How prices are displayed in the order summary for this customer group.', array(), 'Admin.Shopparameters.Help'), + 'options' => array( + 'query' => array( + array( + 'id_method' => PS_TAX_EXC, + 'name' => $this->trans('Tax excluded', array(), 'Admin.Global'), + ), + array( + 'id_method' => PS_TAX_INC, + 'name' => $this->trans('Tax included', array(), 'Admin.Global'), + ), + ), + 'id' => 'id_method', + 'name' => 'name', + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Show prices', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'show_prices', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'show_prices_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'show_prices_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Customers in this group can view prices.', array(), 'Admin.Shopparameters.Help'), + ), + array( + 'type' => 'group_discount_category', + 'label' => $this->trans('Category discount', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'reduction', + 'values' => ($group->id ? $this->formatCategoryDiscountList((int) $group->id) : array()), + ), + array( + 'type' => 'modules', + 'label' => $this->trans('Modules authorization', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'auth_modules', + 'values' => $this->formatModuleListAuth($group->id), + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + if (Tools::getIsset('addgroup')) { + $this->fields_value['price_display_method'] = Configuration::get('PRICE_DISPLAY_METHOD'); + } + + $this->fields_value['reduction'] = isset($group->reduction) ? $group->reduction : 0; + + $tree = new HelperTreeCategories('categories-tree'); + $this->tpl_form_vars['categoryTreeView'] = $tree->setRootCategory((int) Category::getRootCategory()->id)->render(); + + return parent::renderForm(); + } + + protected function formatCategoryDiscountList($id_group) + { + $group_reductions = GroupReduction::getGroupReductions((int) $id_group, $this->context->language->id); + $category_reductions = array(); + $category_reduction = Tools::getValue('category_reduction'); + + foreach ($group_reductions as $category) { + if (is_array($category_reduction) && array_key_exists($category['id_category'], $category_reduction)) { + $category['reduction'] = $category_reduction[$category['id_category']]; + } + + $category_reductions[(int) $category['id_category']] = array( + 'path' => Tools::getPath(Context::getContext()->link->getAdminLink('AdminCategories'), (int) $category['id_category']), + 'reduction' => (float) $category['reduction'] * 100, + 'id_category' => (int) $category['id_category'], + ); + } + + if (is_array($category_reduction)) { + foreach ($category_reduction as $key => $val) { + if (!array_key_exists($key, $category_reductions)) { + $category_reductions[(int) $key] = array( + 'path' => Tools::getPath(Context::getContext()->link->getAdminLink('AdminCategories'), $key), + 'reduction' => (float) $val * 100, + 'id_category' => (int) $key, + ); + } + } + } + + return $category_reductions; + } + + public function formatModuleListAuth($id_group) + { + $modules = Module::getModulesInstalled(); + $authorized_modules = ''; + + $auth_modules = array(); + $unauth_modules = array(); + + $shops = Shop::getContextListShopID(); + + if ($id_group) { + $authorized_modules = Module::getAuthorizedModules($id_group, $shops); + } + + if (is_array($authorized_modules)) { + foreach ($modules as $module) { + $authorized = false; + foreach ($authorized_modules as $auth_module) { + if ($module['id_module'] == $auth_module['id_module']) { + $authorized = true; + } + } + + if ($authorized) { + $auth_modules[] = $module; + } else { + $unauth_modules[] = $module; + } + } + } else { + $auth_modules = $modules; + } + $auth_modules_tmp = array(); + foreach ($auth_modules as $key => $val) { + if ($module = Module::getInstanceById($val['id_module'])) { + $auth_modules_tmp[] = $module; + } + } + + $auth_modules = $auth_modules_tmp; + + $unauth_modules_tmp = array(); + foreach ($unauth_modules as $key => $val) { + if (($tmp_obj = Module::getInstanceById($val['id_module']))) { + $unauth_modules_tmp[] = $tmp_obj; + } + } + + $unauth_modules = $unauth_modules_tmp; + + return array('unauth_modules' => $unauth_modules, 'auth_modules' => $auth_modules); + } + + public function processSave() + { + if (!$this->validateDiscount(Tools::getValue('reduction'))) { + $this->errors[] = $this->trans('The discount value is incorrect (must be a percentage).', array(), 'Admin.Shopparameters.Notification'); + } else { + $this->updateCategoryReduction(); + $object = parent::processSave(); + $this->updateRestrictions(); + + return $object; + } + } + + protected function validateDiscount($reduction) + { + if (!Validate::isPrice($reduction) || $reduction > 100 || $reduction < 0) { + return false; + } else { + return true; + } + } + + public function ajaxProcessAddCategoryReduction() + { + $category_reduction = Tools::getValue('category_reduction'); + $id_category = Tools::getValue('id_category'); //no cast validation is done with Validate::isUnsignedId($id_category) + + $result = array(); + if (!Validate::isUnsignedId($id_category)) { + $result['errors'][] = $this->trans('Wrong category ID.', array(), 'Admin.Shopparameters.Notification'); + $result['hasError'] = true; + } elseif (!$this->validateDiscount($category_reduction)) { + $result['errors'][] = $this->trans('The discount value is incorrect (must be a percentage).', array(), 'Admin.Shopparameters.Notification'); + $result['hasError'] = true; + } else { + $result['id_category'] = (int) $id_category; + $result['catPath'] = Tools::getPath(self::$currentIndex . '?tab=AdminCategories', (int) $id_category); + $result['discount'] = $category_reduction; + $result['hasError'] = false; + } + die(json_encode($result)); + } + + /** + * Update (or create) restrictions for modules by group. + */ + protected function updateRestrictions() + { + $id_group = Tools::getValue('id_group'); + $auth_modules = Tools::getValue('modulesBoxAuth'); + $return = true; + if ($id_group) { + $shops = Shop::getContextListShopID(); + if (is_array($auth_modules)) { + $return &= Group::addModulesRestrictions($id_group, $auth_modules, $shops); + } + } + + // update module list by hook cache + Cache::clean(Hook::MODULE_LIST_BY_HOOK_KEY . '*'); + + return $return; + } + + protected function updateCategoryReduction() + { + $category_reduction = Tools::getValue('category_reduction'); + Db::getInstance()->execute(' + DELETE FROM `' . _DB_PREFIX_ . 'group_reduction` + WHERE `id_group` = ' . (int) Tools::getValue('id_group') + ); + Db::getInstance()->execute(' + DELETE FROM `' . _DB_PREFIX_ . 'product_group_reduction_cache` + WHERE `id_group` = ' . (int) Tools::getValue('id_group') + ); + if (is_array($category_reduction) && count($category_reduction)) { + if (!Configuration::getGlobalValue('PS_GROUP_FEATURE_ACTIVE')) { + Configuration::updateGlobalValue('PS_GROUP_FEATURE_ACTIVE', 1); + } + foreach ($category_reduction as $cat => $reduction) { + if (!Validate::isUnsignedId($cat) || !$this->validateDiscount($reduction)) { + $this->errors[] = $this->trans('The discount value is incorrect.', array(), 'Admin.Shopparameters.Notification'); + } else { + $category = new Category((int) $cat); + $category->addGroupsIfNoExist((int) Tools::getValue('id_group')); + $group_reduction = new GroupReduction(); + $group_reduction->id_group = (int) Tools::getValue('id_group'); + $group_reduction->reduction = (float) ($reduction / 100); + $group_reduction->id_category = (int) $cat; + if (!$group_reduction->save()) { + $this->errors[] = $this->trans('You cannot save group reductions.', array(), 'Admin.Shopparameters.Notification'); + } + } + } + } + } + + /** + * Toggle show prices flag. + */ + public function processChangeShowPricesVal() + { + $group = new Group($this->id_object); + if (!Validate::isLoadedObject($group)) { + $this->errors[] = $this->trans('An error occurred while updating this group.', array(), 'Admin.Shopparameters.Notification'); + } + $update = Db::getInstance()->execute('UPDATE `' . _DB_PREFIX_ . 'group` SET show_prices = ' . ($group->show_prices ? 0 : 1) . ' WHERE `id_group` = ' . (int) $group->id); + if (!$update) { + $this->errors[] = $this->trans('An error occurred while updating this group.', array(), 'Admin.Shopparameters.Notification'); + } + Tools::clearSmartyCache(); + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token); + } + + public function renderList() + { + $unidentified = new Group(Configuration::get('PS_UNIDENTIFIED_GROUP')); + $guest = new Group(Configuration::get('PS_GUEST_GROUP')); + $default = new Group(Configuration::get('PS_CUSTOMER_GROUP')); + + $unidentified_group_information = $this->trans('%group_name% - All persons without a customer account or customers that are not logged in.', array('%group_name%' => '' . $unidentified->name[$this->context->language->id] . ''), 'Admin.Shopparameters.Help'); + $guest_group_information = $this->trans('%group_name% - All persons who placed an order through Guest Checkout.', array('%group_name%' => '' . $guest->name[$this->context->language->id] . ''), 'Admin.Shopparameters.Help'); + $default_group_information = $this->trans('%group_name% - All persons who created an account on this site.', array('%group_name%' => '' . $default->name[$this->context->language->id] . ''), 'Admin.Shopparameters.Help'); + + $this->displayInformation($this->trans('PrestaShop has three default customer groups:', array(), 'Admin.Shopparameters.Help')); + $this->displayInformation($unidentified_group_information); + $this->displayInformation($guest_group_information); + $this->displayInformation($default_group_information); + + return parent::renderList(); + } + + public function displayEditLink($token, $id) + { + $tpl = $this->createTemplate('helpers/list/list_action_edit.tpl'); + if (!array_key_exists('Edit', self::$cache_lang)) { + self::$cache_lang['Edit'] = $this->trans('Edit', array(), 'Admin.Actions'); + } + + $href = self::$currentIndex . '&' . $this->identifier . '=' . $id . '&update' . $this->table . '&token=' . ($token != null ? $token : $this->token); + + if ($this->display == 'view') { + $href = Context::getContext()->link->getAdminLink('AdminCustomers') . '&id_customer=' . (int) $id . '&updatecustomer&back=' . urlencode($href); + } + + $tpl->assign(array( + 'href' => $href, + 'action' => self::$cache_lang['Edit'], + 'id' => $id, + )); + + return $tpl->fetch(); + } +} diff --git a/controllers/admin/AdminImagesController.php b/controllers/admin/AdminImagesController.php new file mode 100644 index 00000000..40fa87b1 --- /dev/null +++ b/controllers/admin/AdminImagesController.php @@ -0,0 +1,748 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property ImageType $object + */ +class AdminImagesControllerCore extends AdminController +{ + protected $start_time = 0; + protected $max_execution_time = 7200; + protected $display_move; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'image_type'; + $this->className = 'ImageType'; + $this->lang = false; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->fields_list = array( + 'id_image_type' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global')), + 'width' => array('title' => $this->trans('Width', array(), 'Admin.Global'), 'suffix' => ' px'), + 'height' => array('title' => $this->trans('Height', array(), 'Admin.Global'), 'suffix' => ' px'), + 'products' => array('title' => $this->trans('Products', array(), 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false), + 'categories' => array('title' => $this->trans('Categories', array(), 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false), + 'manufacturers' => array('title' => $this->trans('Brands', array(), 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false), + 'suppliers' => array('title' => $this->trans('Suppliers', array(), 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false), + 'stores' => array('title' => $this->trans('Stores', array(), 'Admin.Global'), 'align' => 'center', 'type' => 'bool', 'callback' => 'printEntityActiveIcon', 'orderby' => false), + ); + + // No need to display the old image system migration tool except if product images are in _PS_PROD_IMG_DIR_ + $this->display_move = false; + $dir = _PS_PROD_IMG_DIR_; + if (is_dir($dir)) { + if ($dh = opendir($dir)) { + while (($file = readdir($dh)) !== false && $this->display_move == false) { + if (!is_dir($dir . DIRECTORY_SEPARATOR . $file) && $file[0] != '.' && is_numeric($file[0])) { + $this->display_move = true; + } + } + closedir($dh); + } + } + + $this->fields_options = array( + 'images' => array( + 'title' => $this->trans('Images generation options', array(), 'Admin.Design.Feature'), + 'icon' => 'icon-picture', + 'top' => '', + 'bottom' => '', + 'description' => $this->trans('JPEG images have a small file size and standard quality. PNG images have a larger file size, a higher quality and support transparency. Note that in all cases the image files will have the .jpg extension.', array(), 'Admin.Design.Help') . ' +

' . $this->trans('WARNING: This feature may not be compatible with your theme, or with some of your modules. In particular, PNG mode is not compatible with the Watermark module. If you encounter any issues, turn it off by selecting "Use JPEG".', array(), 'Admin.Design.Help'), + 'fields' => array( + 'PS_IMAGE_QUALITY' => array( + 'title' => $this->trans('Image format', array(), 'Admin.Design.Feature'), + 'show' => true, + 'required' => true, + 'type' => 'radio', + 'choices' => array('jpg' => $this->trans('Use JPEG.', array(), 'Admin.Design.Feature'), 'png' => $this->trans('Use PNG only if the base image is in PNG format.', array(), 'Admin.Design.Feature'), 'png_all' => $this->trans('Use PNG for all images.', array(), 'Admin.Design.Feature')), + ), + 'PS_JPEG_QUALITY' => array( + 'title' => $this->trans('JPEG compression', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('Ranges from 0 (worst quality, smallest file) to 100 (best quality, biggest file).', array(), 'Admin.Design.Help') . ' ' . $this->trans('Recommended: 90.', array(), 'Admin.Design.Help'), + 'validation' => 'isUnsignedId', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + ), + 'PS_PNG_QUALITY' => array( + 'title' => $this->trans('PNG compression', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('PNG compression is lossless: unlike JPG, you do not lose image quality with a high compression ratio. However, photographs will compress very badly.', array(), 'Admin.Design.Help') . ' ' . $this->trans('Ranges from 0 (biggest file) to 9 (smallest file, slowest decompression).', array(), 'Admin.Design.Help') . ' ' . $this->trans('Recommended: 7.', array(), 'Admin.Design.Help'), + 'validation' => 'isUnsignedId', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + ), + 'PS_IMAGE_GENERATION_METHOD' => array( + 'title' => $this->trans('Generate images based on one side of the source image', array(), 'Admin.Design.Feature'), + 'validation' => 'isUnsignedId', + 'required' => false, + 'cast' => 'intval', + 'type' => 'select', + 'list' => array( + array( + 'id' => '0', + 'name' => $this->trans('Automatic (longest side)', array(), 'Admin.Design.Feature'), + ), + array( + 'id' => '1', + 'name' => $this->trans('Width', array(), 'Admin.Global'), + ), + array( + 'id' => '2', + 'name' => $this->trans('Height', array(), 'Admin.Global'), + ), + ), + 'identifier' => 'id', + 'visibility' => Shop::CONTEXT_ALL, + ), + 'PS_PRODUCT_PICTURE_MAX_SIZE' => array( + 'title' => $this->trans('Maximum file size of product customization pictures', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('The maximum file size of pictures that customers can upload to customize a product (in bytes).', array(), 'Admin.Design.Help'), + 'validation' => 'isUnsignedInt', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + 'suffix' => $this->trans('bytes', array(), 'Admin.Design.Feature'), + 'visibility' => Shop::CONTEXT_ALL, + ), + 'PS_PRODUCT_PICTURE_WIDTH' => array( + 'title' => $this->trans('Product picture width', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('Width of product customization pictures that customers can upload (in pixels).', array(), 'Admin.Design.Help'), + 'validation' => 'isUnsignedInt', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + 'width' => 'px', + 'suffix' => $this->trans('pixels', array(), 'Admin.Design.Feature'), + 'visibility' => Shop::CONTEXT_ALL, + ), + 'PS_PRODUCT_PICTURE_HEIGHT' => array( + 'title' => $this->trans('Product picture height', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('Height of product customization pictures that customers can upload (in pixels).', array(), 'Admin.Design.Help'), + 'validation' => 'isUnsignedInt', + 'required' => true, + 'cast' => 'intval', + 'type' => 'text', + 'height' => 'px', + 'suffix' => $this->trans('pixels', array(), 'Admin.Design.Feature'), + 'visibility' => Shop::CONTEXT_ALL, + ), + 'PS_HIGHT_DPI' => array( + 'type' => 'bool', + 'title' => $this->trans('Generate high resolution images', array(), 'Admin.Design.Feature'), + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This will generate an additional file for each image (thus doubling your total amount of images). Resolution of these images will be twice higher.', array(), 'Admin.Design.Help'), + 'desc' => $this->trans('Enable to optimize the display of your images on high pixel density screens.', array(), 'Admin.Design.Help'), + 'visibility' => Shop::CONTEXT_ALL, + ), + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ), + ); + + if ($this->display_move) { + $this->fields_options['product_images']['fields']['PS_LEGACY_IMAGES'] = array( + 'title' => $this->trans('Use the legacy image filesystem', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('This should be set to yes unless you successfully moved images in "Images" page under the "Preferences" menu.', array(), 'Admin.Design.Help'), + 'validation' => 'isBool', + 'cast' => 'intval', + 'required' => false, + 'type' => 'bool', + 'visibility' => Shop::CONTEXT_ALL, + ); + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Image type', array(), 'Admin.Design.Feature'), + 'icon' => 'icon-picture', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name for the image type', array(), 'Admin.Design.Feature'), + 'name' => 'name', + 'required' => true, + 'hint' => $this->trans('Letters, underscores and hyphens only (e.g. "small_custom", "cart_medium", "large", "thickbox_extra-large").', array(), 'Admin.Design.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Width', array(), 'Admin.Global'), + 'name' => 'width', + 'required' => true, + 'maxlength' => 5, + 'suffix' => $this->trans('pixels', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('Maximum image width in pixels.', array(), 'Admin.Design.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Height', array(), 'Admin.Global'), + 'name' => 'height', + 'required' => true, + 'maxlength' => 5, + 'suffix' => $this->trans('pixels', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('Maximum image height in pixels.', array(), 'Admin.Design.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Products', array(), 'Admin.Global'), + 'name' => 'products', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Product images.', array(), 'Admin.Design.Help'), + 'values' => array( + array( + 'id' => 'products_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'products_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Categories', array(), 'Admin.Global'), + 'name' => 'categories', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Category images.', array(), 'Admin.Design.Help'), + 'values' => array( + array( + 'id' => 'categories_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'categories_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Brands', array(), 'Admin.Global'), + 'name' => 'manufacturers', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Brand images.', array(), 'Admin.Design.Help'), + 'values' => array( + array( + 'id' => 'manufacturers_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'manufacturers_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Suppliers', array(), 'Admin.Global'), + 'name' => 'suppliers', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Supplier images.', array(), 'Admin.Design.Help'), + 'values' => array( + array( + 'id' => 'suppliers_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'suppliers_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Stores', array(), 'Admin.Global'), + 'name' => 'stores', + 'required' => false, + 'is_bool' => true, + 'hint' => $this->trans('This type will be used for Store images.', array(), 'Admin.Design.Help'), + 'values' => array( + array( + 'id' => 'stores_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'stores_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + } + + public function postProcess() + { + // When moving images, if duplicate images were found they are moved to a folder named duplicates/ + if (file_exists(_PS_PROD_IMG_DIR_ . 'duplicates/')) { + $this->warnings[] = $this->trans('Duplicate images were found when moving the product images. This is likely caused by unused demonstration images. Please make sure that the folder %folder% only contains demonstration images, and then delete it.', array('%folder%' => _PS_PROD_IMG_DIR_ . 'duplicates/'), 'Admin.Design.Notification'); + } + + if (Tools::isSubmit('submitRegenerate' . $this->table)) { + if ($this->access('edit')) { + if ($this->_regenerateThumbnails(Tools::getValue('type'), Tools::getValue('erase'))) { + Tools::redirectAdmin(self::$currentIndex . '&conf=9' . '&token=' . $this->token); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitMoveImages' . $this->table)) { + if ($this->access('edit')) { + if ($this->_moveImagesToNewFileSystem()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=25' . '&token=' . $this->token); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitOptions' . $this->table)) { + if ($this->access('edit')) { + if ((int) Tools::getValue('PS_JPEG_QUALITY') < 0 + || (int) Tools::getValue('PS_JPEG_QUALITY') > 100) { + $this->errors[] = $this->trans('Incorrect value for the selected JPEG image compression.', array(), 'Admin.Design.Notification'); + } elseif ((int) Tools::getValue('PS_PNG_QUALITY') < 0 + || (int) Tools::getValue('PS_PNG_QUALITY') > 9) { + $this->errors[] = $this->trans('Incorrect value for the selected PNG image compression.', array(), 'Admin.Design.Notification'); + } elseif (!Configuration::updateValue('PS_IMAGE_QUALITY', Tools::getValue('PS_IMAGE_QUALITY')) + || !Configuration::updateValue('PS_JPEG_QUALITY', Tools::getValue('PS_JPEG_QUALITY')) + || !Configuration::updateValue('PS_PNG_QUALITY', Tools::getValue('PS_PNG_QUALITY'))) { + $this->errors[] = $this->trans('Unknown error.', array(), 'Admin.Notifications.Error'); + } else { + $this->confirmations[] = $this->_conf[6]; + } + + return parent::postProcess(); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } else { + return parent::postProcess(); + } + } + + public static function printEntityActiveIcon($value, $object) + { + return $value ? '' : ''; + } + + protected function _childValidation() + { + if (!Tools::getValue('id_image_type') && Validate::isImageTypeName($typeName = Tools::getValue('name')) && ImageType::typeAlreadyExists($typeName)) { + $this->errors[] = $this->trans('This name already exists.', array(), 'Admin.Design.Notification'); + } + } + + /** + * Init display for the thumbnails regeneration block. + */ + public function initRegenerate() + { + $types = array( + 'categories' => $this->trans('Categories', array(), 'Admin.Global'), + 'manufacturers' => $this->trans('Brands', array(), 'Admin.Global'), + 'suppliers' => $this->trans('Suppliers', array(), 'Admin.Global'), + 'products' => $this->trans('Products', array(), 'Admin.Global'), + 'stores' => $this->trans('Stores', array(), 'Admin.Global'), + ); + + $formats = array(); + foreach ($types as $i => $type) { + $formats[$i] = ImageType::getImagesTypes($i); + } + + $this->context->smarty->assign(array( + 'types' => $types, + 'formats' => $formats, + )); + } + + /** + * Delete resized image then regenerate new one with updated settings. + * + * @param string $dir + * @param array $type + * @param bool $product + * + * @return bool + */ + protected function _deleteOldImages($dir, $type, $product = false) + { + if (!is_dir($dir)) { + return false; + } + $toDel = scandir($dir, SCANDIR_SORT_NONE); + + foreach ($toDel as $d) { + foreach ($type as $imageType) { + if (preg_match('/^[0-9]+\-' . ($product ? '[0-9]+\-' : '') . $imageType['name'] . '\.jpg$/', $d) + || (count($type) > 1 && preg_match('/^[0-9]+\-[_a-zA-Z0-9-]*\.jpg$/', $d)) + || preg_match('/^([[:lower:]]{2})\-default\-' . $imageType['name'] . '\.jpg$/', $d)) { + if (file_exists($dir . $d)) { + unlink($dir . $d); + } + } + } + } + + // delete product images using new filesystem. + if ($product) { + $productsImages = Image::getAllImages(); + foreach ($productsImages as $image) { + $imageObj = new Image($image['id_image']); + $imageObj->id_product = $image['id_product']; + if (file_exists($dir . $imageObj->getImgFolder())) { + $toDel = scandir($dir . $imageObj->getImgFolder(), SCANDIR_SORT_NONE); + foreach ($toDel as $d) { + foreach ($type as $imageType) { + if (preg_match('/^[0-9]+\-' . $imageType['name'] . '\.jpg$/', $d) || (count($type) > 1 && preg_match('/^[0-9]+\-[_a-zA-Z0-9-]*\.jpg$/', $d))) { + if (file_exists($dir . $imageObj->getImgFolder() . $d)) { + unlink($dir . $imageObj->getImgFolder() . $d); + } + } + } + } + } + } + } + } + + /** + * Regenerate images. + * + * @param $dir + * @param $type + * @param bool $productsImages + * + * @return bool|string + */ + protected function _regenerateNewImages($dir, $type, $productsImages = false) + { + if (!is_dir($dir)) { + return false; + } + + $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI'); + + if (!$productsImages) { + $formated_medium = ImageType::getFormattedName('medium'); + foreach (scandir($dir, SCANDIR_SORT_NONE) as $image) { + if (preg_match('/^[0-9]*\.jpg$/', $image)) { + foreach ($type as $k => $imageType) { + // Customizable writing dir + $newDir = $dir; + if (!file_exists($newDir)) { + continue; + } + + if (($dir == _PS_CAT_IMG_DIR_) && ($imageType['name'] == $formated_medium) && is_file(_PS_CAT_IMG_DIR_ . str_replace('.', '_thumb.', $image))) { + $image = str_replace('.', '_thumb.', $image); + } + + if (!file_exists($newDir . substr($image, 0, -4) . '-' . stripslashes($imageType['name']) . '.jpg')) { + if (!file_exists($dir . $image) || !filesize($dir . $image)) { + $this->errors[] = $this->trans('Source file does not exist or is empty (%filepath%)', array('%filepath%' => $dir . $image), 'Admin.Design.Notification'); + } elseif (!ImageManager::resize($dir . $image, $newDir . substr(str_replace('_thumb.', '.', $image), 0, -4) . '-' . stripslashes($imageType['name']) . '.jpg', (int) $imageType['width'], (int) $imageType['height'])) { + $this->errors[] = $this->trans('Failed to resize image file (%filepath%)', array('%filepath%' => $dir . $image), 'Admin.Design.Notification'); + } + + if ($generate_hight_dpi_images) { + if (!ImageManager::resize($dir . $image, $newDir . substr($image, 0, -4) . '-' . stripslashes($imageType['name']) . '2x.jpg', (int) $imageType['width'] * 2, (int) $imageType['height'] * 2)) { + $this->errors[] = $this->trans('Failed to resize image file to high resolution (%filepath%)', array('%filepath%' => $dir . $image), 'Admin.Design.Notification'); + } + } + } + // stop 4 seconds before the timeout, just enough time to process the end of the page on a slow server + if (time() - $this->start_time > $this->max_execution_time - 4) { + return 'timeout'; + } + } + } + } + } else { + foreach (Image::getAllImages() as $image) { + $imageObj = new Image($image['id_image']); + $existing_img = $dir . $imageObj->getExistingImgPath() . '.jpg'; + if (file_exists($existing_img) && filesize($existing_img)) { + foreach ($type as $imageType) { + if (!file_exists($dir . $imageObj->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg')) { + if (!ImageManager::resize($existing_img, $dir . $imageObj->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg', (int) $imageType['width'], (int) $imageType['height'])) { + $this->errors[] = $this->trans( + 'Original image is corrupt (%filename%) for product ID %id% or bad permission on folder.', + array( + '%filename%' => $existing_img, + '%id%' => (int) $imageObj->id_product, + ), + 'Admin.Design.Notification' + ); + } + + if ($generate_hight_dpi_images) { + if (!ImageManager::resize($existing_img, $dir . $imageObj->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '2x.jpg', (int) $imageType['width'] * 2, (int) $imageType['height'] * 2)) { + $this->errors[] = $this->trans( + 'Original image is corrupt (%filename%) for product ID %id% or bad permission on folder.', + array( + '%filename%' => $existing_img, + '%id%' => (int) $imageObj->id_product, + ), + 'Admin.Design.Notification' + ); + } + } + } + } + } else { + $this->errors[] = $this->trans( + 'Original image is missing or empty (%filename%) for product ID %id%', + array( + '%filename%' => $existing_img, + '%id%' => (int) $imageObj->id_product, + ), + 'Admin.Design.Notification' + ); + } + if (time() - $this->start_time > $this->max_execution_time - 4) { // stop 4 seconds before the tiemout, just enough time to process the end of the page on a slow server + return 'timeout'; + } + } + } + + return (bool) count($this->errors); + } + + /** + * Regenerate no-pictures images. + * + * @param $dir + * @param $type + * @param $languages + * + * @return bool + */ + protected function _regenerateNoPictureImages($dir, $type, $languages) + { + $errors = false; + $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI'); + + foreach ($type as $image_type) { + foreach ($languages as $language) { + $file = $dir . $language['iso_code'] . '.jpg'; + if (!file_exists($file)) { + $file = _PS_PROD_IMG_DIR_ . Language::getIsoById((int) Configuration::get('PS_LANG_DEFAULT')) . '.jpg'; + } + if (!file_exists($dir . $language['iso_code'] . '-default-' . stripslashes($image_type['name']) . '.jpg')) { + if (!ImageManager::resize($file, $dir . $language['iso_code'] . '-default-' . stripslashes($image_type['name']) . '.jpg', (int) $image_type['width'], (int) $image_type['height'])) { + $errors = true; + } + + if ($generate_hight_dpi_images) { + if (!ImageManager::resize($file, $dir . $language['iso_code'] . '-default-' . stripslashes($image_type['name']) . '2x.jpg', (int) $image_type['width'] * 2, (int) $image_type['height'] * 2)) { + $errors = true; + } + } + } + } + } + + return $errors; + } + + /* Hook watermark optimization */ + protected function _regenerateWatermark($dir, $type = null) + { + $result = Db::getInstance()->executeS(' + SELECT m.`name` FROM `' . _DB_PREFIX_ . 'module` m + LEFT JOIN `' . _DB_PREFIX_ . 'hook_module` hm ON hm.`id_module` = m.`id_module` + LEFT JOIN `' . _DB_PREFIX_ . 'hook` h ON hm.`id_hook` = h.`id_hook` + WHERE h.`name` = \'actionWatermark\' AND m.`active` = 1'); + + if ($result && count($result)) { + $productsImages = Image::getAllImages(); + foreach ($productsImages as $image) { + $imageObj = new Image($image['id_image']); + if (file_exists($dir . $imageObj->getExistingImgPath() . '.jpg')) { + foreach ($result as $module) { + $moduleInstance = Module::getInstanceByName($module['name']); + if ($moduleInstance && is_callable(array($moduleInstance, 'hookActionWatermark'))) { + call_user_func(array($moduleInstance, 'hookActionWatermark'), array('id_image' => $imageObj->id, 'id_product' => $imageObj->id_product, 'image_type' => $type)); + } + + if (time() - $this->start_time > $this->max_execution_time - 4) { // stop 4 seconds before the tiemout, just enough time to process the end of the page on a slow server + return 'timeout'; + } + } + } + } + } + } + + protected function _regenerateThumbnails($type = 'all', $deleteOldImages = false) + { + $this->start_time = time(); + ini_set('max_execution_time', $this->max_execution_time); // ini_set may be disabled, we need the real value + $this->max_execution_time = (int) ini_get('max_execution_time'); + $languages = Language::getLanguages(false); + + $process = array( + array('type' => 'categories', 'dir' => _PS_CAT_IMG_DIR_), + array('type' => 'manufacturers', 'dir' => _PS_MANU_IMG_DIR_), + array('type' => 'suppliers', 'dir' => _PS_SUPP_IMG_DIR_), + array('type' => 'products', 'dir' => _PS_PROD_IMG_DIR_), + array('type' => 'stores', 'dir' => _PS_STORE_IMG_DIR_), + ); + + // Launching generation process + foreach ($process as $proc) { + if ($type != 'all' && $type != $proc['type']) { + continue; + } + + // Getting format generation + $formats = ImageType::getImagesTypes($proc['type']); + if ($type != 'all') { + $format = strval(Tools::getValue('format_' . $type)); + if ($format != 'all') { + foreach ($formats as $k => $form) { + if ($form['id_image_type'] != $format) { + unset($formats[$k]); + } + } + } + } + + if ($deleteOldImages) { + $this->_deleteOldImages($proc['dir'], $formats, ($proc['type'] == 'products' ? true : false)); + } + if (($return = $this->_regenerateNewImages($proc['dir'], $formats, ($proc['type'] == 'products' ? true : false))) === true) { + if (!count($this->errors)) { + $this->errors[] = $this->trans('Cannot write images for this type: %1$s. Please check the %2$s folder\'s writing permissions.', array($proc['type'], $proc['dir']), 'Admin.Design.Notification'); + } + } elseif ($return == 'timeout') { + $this->errors[] = $this->trans('Only part of the images have been regenerated. The server timed out before finishing.', array(), 'Admin.Design.Notification'); + } else { + if ($proc['type'] == 'products') { + if ($this->_regenerateWatermark($proc['dir'], $formats) == 'timeout') { + $this->errors[] = $this->trans('Server timed out. The watermark may not have been applied to all images.', array(), 'Admin.Design.Notification'); + } + } + if (!count($this->errors)) { + if ($this->_regenerateNoPictureImages($proc['dir'], $formats, $languages)) { + $this->errors[] = $this->trans('Cannot write "No picture" image to %s images folder. Please check the folder\'s writing permissions.', array($proc['type']), 'Admin.Design.Notification'); + } + } + } + } + + return count($this->errors) > 0 ? false : true; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_image_type'] = array( + 'href' => self::$currentIndex . '&addimage_type&token=' . $this->token, + 'desc' => $this->trans('Add new image type', array(), 'Admin.Design.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + /** + * Move product images to the new filesystem. + */ + protected function _moveImagesToNewFileSystem() + { + if (!Image::testFileSystem()) { + $this->errors[] = $this->trans('Error: Your server configuration is not compatible with the new image system. No images were moved.', array(), 'Admin.Design.Notification'); + } else { + ini_set('max_execution_time', $this->max_execution_time); // ini_set may be disabled, we need the real value + $this->max_execution_time = (int) ini_get('max_execution_time'); + $result = Image::moveToNewFileSystem($this->max_execution_time); + if ($result === 'timeout') { + $this->errors[] = $this->trans('Not all images have been moved. The server timed out before finishing. Click on "Move images" again to resume the moving process.', array(), 'Admin.Design.Notification'); + } elseif ($result === false) { + $this->errors[] = $this->trans('Error: Some -- or all -- images cannot be moved.', array(), 'Admin.Design.Notification'); + } + } + + return count($this->errors) > 0 ? false : true; + } + + public function initContent() + { + if ($this->display != 'edit' && $this->display != 'add') { + $this->initRegenerate(); + + $this->context->smarty->assign(array( + 'display_regenerate' => true, + 'display_move' => $this->display_move, + )); + } + + if ($this->display == 'edit') { + $this->warnings[] = $this->trans('After modification, do not forget to regenerate thumbnails', array(), 'Admin.Design.Notification'); + } + + parent::initContent(); + } +} diff --git a/controllers/admin/AdminImportController.php b/controllers/admin/AdminImportController.php new file mode 100644 index 00000000..18463540 --- /dev/null +++ b/controllers/admin/AdminImportController.php @@ -0,0 +1,4705 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +@ini_set('max_execution_time', 0); +/* No max line limit since the lines can be more than 4096. Performance impact is not significant. */ +define('MAX_LINE_SIZE', 0); + +/* Used for validatefields diying without user friendly error or not */ +define('UNFRIENDLY_ERROR', false); + +/* this value set the number of columns visible on each page */ +define('MAX_COLUMNS', 6); + +/* correct Mac error on eof */ +@ini_set('auto_detect_line_endings', '1'); + +class AdminImportControllerCore extends AdminController +{ + public static $column_mask; + + public $entities = array(); + + public $available_fields = array(); + + public $required_fields = array(); + + public static $default_values = array(); + + public static $validators = array( + 'active' => array('AdminImportController', 'getBoolean'), + 'tax_rate' => array('AdminImportController', 'getPrice'), + /* Tax excluded */ + 'price_tex' => array('AdminImportController', 'getPrice'), + /* Tax included */ + 'price_tin' => array('AdminImportController', 'getPrice'), + 'reduction_price' => array('AdminImportController', 'getPrice'), + 'reduction_percent' => array('AdminImportController', 'getPrice'), + 'wholesale_price' => array('AdminImportController', 'getPrice'), + 'ecotax' => array('AdminImportController', 'getPrice'), + 'name' => array('AdminImportController', 'createMultiLangField'), + 'description' => array('AdminImportController', 'createMultiLangField'), + 'description_short' => array('AdminImportController', 'createMultiLangField'), + 'meta_title' => array('AdminImportController', 'createMultiLangField'), + 'meta_keywords' => array('AdminImportController', 'createMultiLangField'), + 'meta_description' => array('AdminImportController', 'createMultiLangField'), + 'link_rewrite' => array('AdminImportController', 'createMultiLangField'), + 'available_now' => array('AdminImportController', 'createMultiLangField'), + 'available_later' => array('AdminImportController', 'createMultiLangField'), + 'category' => array('AdminImportController', 'split'), + 'online_only' => array('AdminImportController', 'getBoolean'), + 'accessories' => array('AdminImportController', 'split'), + 'image_alt' => array('AdminImportController', 'split'), + 'delivery_in_stock' => array('AdminImportController', 'createMultiLangField'), + 'delivery_out_stock' => array('AdminImportController', 'createMultiLangField'), + ); + + public $separator; + public $convert; + public $multiple_value_separator; + + public function __construct() + { + $this->bootstrap = true; + + parent::__construct(); + + $this->entities = array( + $this->trans('Categories', array(), 'Admin.Global'), + $this->trans('Products', array(), 'Admin.Global'), + $this->trans('Combinations', array(), 'Admin.Global'), + $this->trans('Customers', array(), 'Admin.Global'), + $this->trans('Addresses', array(), 'Admin.Global'), + $this->trans('Brands', array(), 'Admin.Global'), + $this->trans('Suppliers', array(), 'Admin.Global'), + $this->trans('Alias', array(), 'Admin.Shopparameters.Feature'), + $this->trans('Store contacts', array(), 'Admin.Advparameters.Feature'), + ); + + // @since 1.5.0 + if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) { + $this->entities = array_merge( + $this->entities, + array( + $this->trans('Supply Orders', array(), 'Admin.Advparameters.Feature'), + $this->trans('Supply Order Details', array(), 'Admin.Advparameters.Feature'), + ) + ); + } + + $this->entities = array_flip($this->entities); + + switch ((int) Tools::getValue('entity')) { + case $this->entities[$this->trans('Combinations', array(), 'Admin.Global')]: + $this->required_fields = array( + 'group', + 'attribute', + ); + + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id_product' => array('label' => $this->trans('Product ID', array(), 'Admin.Advparameters.Feature')), + 'product_reference' => array('label' => $this->trans('Product Reference', array(), 'Admin.Advparameters.Feature')), + 'group' => array( + 'label' => $this->trans('Attribute (Name:Type:Position)', array(), 'Admin.Advparameters.Feature') . '*', + ), + 'attribute' => array( + 'label' => $this->trans('Value (Value:Position)', array(), 'Admin.Advparameters.Feature') . '*', + ), + 'supplier_reference' => array('label' => $this->trans('Supplier reference', array(), 'Admin.Advparameters.Feature')), + 'reference' => array('label' => $this->trans('Reference', array(), 'Admin.Global')), + 'ean13' => array('label' => $this->trans('EAN13', array(), 'Admin.Advparameters.Feature')), + 'upc' => array('label' => $this->trans('UPC', array(), 'Admin.Advparameters.Feature')), + 'wholesale_price' => array('label' => $this->trans('Cost price', array(), 'Admin.Catalog.Feature')), + 'price' => array('label' => $this->trans('Impact on price', array(), 'Admin.Catalog.Feature')), + 'ecotax' => array('label' => $this->trans('Ecotax', array(), 'Admin.Catalog.Feature')), + 'quantity' => array('label' => $this->trans('Quantity', array(), 'Admin.Global')), + 'minimal_quantity' => array('label' => $this->trans('Minimal quantity', array(), 'Admin.Advparameters.Feature')), + 'low_stock_threshold' => array('label' => $this->trans('Low stock level', array(), 'Admin.Catalog.Feature')), + 'low_stock_alert' => array('label' => $this->trans('Send me an email when the quantity is under this level', array(), 'Admin.Catalog.Feature')), + 'weight' => array('label' => $this->trans('Impact on weight', array(), 'Admin.Catalog.Feature')), + 'default_on' => array('label' => $this->trans('Default (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature')), + 'available_date' => array('label' => $this->trans('Combination availability date', array(), 'Admin.Advparameters.Feature')), + 'image_position' => array( + 'label' => $this->trans('Choose among product images by position (1,2,3...)', array(), 'Admin.Advparameters.Feature'), + ), + 'image_url' => array('label' => $this->trans('Image URLs (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + 'image_alt' => array('label' => $this->trans('Image alt texts (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + 'shop' => array( + 'label' => $this->trans('ID / Name of shop', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', array(), 'Admin.Advparameters.Help'), + ), + 'advanced_stock_management' => array( + 'label' => $this->trans('Advanced Stock Management', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Enable Advanced Stock Management on product (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Help'), + ), + 'depends_on_stock' => array( + 'label' => $this->trans('Depends on stock', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('0 = Use quantity set in product, 1 = Use quantity from warehouse.', array(), 'Admin.Advparameters.Help'), + ), + 'warehouse' => array( + 'label' => $this->trans('Warehouse', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('ID of the warehouse to set as storage.', array(), 'Admin.Advparameters.Help'), + ), + ); + + self::$default_values = array( + 'reference' => '', + 'supplier_reference' => '', + 'ean13' => '', + 'upc' => '', + 'wholesale_price' => 0, + 'price' => 0, + 'ecotax' => 0, + 'quantity' => 0, + 'minimal_quantity' => 1, + 'low_stock_threshold' => null, + 'low_stock_alert' => false, + 'weight' => 0, + 'default_on' => null, + 'advanced_stock_management' => 0, + 'depends_on_stock' => 0, + 'available_date' => date('Y-m-d'), + ); + break; + + case $this->entities[$this->trans('Categories', array(), 'Admin.Global')]: + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id' => array('label' => $this->trans('ID', array(), 'Admin.Global')), + 'active' => array('label' => $this->trans('Active (0/1)', array(), 'Admin.Advparameters.Feature')), + 'name' => array('label' => $this->trans('Name', array(), 'Admin.Global')), + 'parent' => array('label' => $this->trans('Parent category', array(), 'Admin.Catalog.Feature')), + 'is_root_category' => array( + 'label' => $this->trans('Root category (0/1)', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('A category root is where a category tree can begin. This is used with multistore.', array(), 'Admin.Advparameters.Help'), + ), + 'description' => array('label' => $this->trans('Description', array(), 'Admin.Global')), + 'meta_title' => array('label' => $this->trans('Meta title', array(), 'Admin.Global')), + 'meta_keywords' => array('label' => $this->trans('Meta keywords', array(), 'Admin.Global')), + 'meta_description' => array('label' => $this->trans('Meta description', array(), 'Admin.Global')), + 'link_rewrite' => array('label' => $this->trans('Rewritten URL', array(), 'Admin.Shopparameters.Feature')), + 'image' => array('label' => $this->trans('Image URL', array(), 'Admin.Advparameters.Feature')), + 'shop' => array( + 'label' => $this->trans('ID / Name of shop', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', array(), 'Admin.Advparameters.Help'), + ), + ); + + self::$default_values = array( + 'active' => '1', + 'parent' => Configuration::get('PS_HOME_CATEGORY'), + 'link_rewrite' => '', + ); + break; + + case $this->entities[$this->trans('Products', array(), 'Admin.Global')]: + self::$validators['image'] = array( + 'AdminImportController', + 'split', + ); + + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id' => array('label' => $this->trans('ID', array(), 'Admin.Global')), + 'active' => array('label' => $this->trans('Active (0/1)', array(), 'Admin.Advparameters.Feature')), + 'name' => array('label' => $this->trans('Name', array(), 'Admin.Global')), + 'category' => array('label' => $this->trans('Categories (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + 'price_tex' => array('label' => $this->trans('Price tax excluded', array(), 'Admin.Advparameters.Feature')), + 'price_tin' => array('label' => $this->trans('Price tax included', array(), 'Admin.Advparameters.Feature')), + 'id_tax_rules_group' => array('label' => $this->trans('Tax rule ID', array(), 'Admin.Advparameters.Feature')), + 'wholesale_price' => array('label' => $this->trans('Cost price', array(), 'Admin.Catalog.Feature')), + 'on_sale' => array('label' => $this->trans('On sale (0/1)', array(), 'Admin.Advparameters.Feature')), + 'reduction_price' => array('label' => $this->trans('Discount amount', array(), 'Admin.Advparameters.Feature')), + 'reduction_percent' => array('label' => $this->trans('Discount percent', array(), 'Admin.Advparameters.Feature')), + 'reduction_from' => array('label' => $this->trans('Discount from (yyyy-mm-dd)', array(), 'Admin.Advparameters.Feature')), + 'reduction_to' => array('label' => $this->trans('Discount to (yyyy-mm-dd)', array(), 'Admin.Advparameters.Feature')), + 'reference' => array('label' => $this->trans('Reference #', array(), 'Admin.Advparameters.Feature')), + 'supplier_reference' => array('label' => $this->trans('Supplier reference #', array(), 'Admin.Advparameters.Feature')), + 'supplier' => array('label' => $this->trans('Supplier', array(), 'Admin.Global')), + 'manufacturer' => array('label' => $this->trans('Brand', array(), 'Admin.Global')), + 'ean13' => array('label' => $this->trans('EAN13', array(), 'Admin.Advparameters.Feature')), + 'upc' => array('label' => $this->trans('UPC', array(), 'Admin.Advparameters.Feature')), + 'ecotax' => array('label' => $this->trans('Ecotax', array(), 'Admin.Catalog.Feature')), + 'width' => array('label' => $this->trans('Width', array(), 'Admin.Global')), + 'height' => array('label' => $this->trans('Height', array(), 'Admin.Global')), + 'depth' => array('label' => $this->trans('Depth', array(), 'Admin.Global')), + 'weight' => array('label' => $this->trans('Weight', array(), 'Admin.Global')), + 'delivery_in_stock' => array( + 'label' => $this->trans( + 'Delivery time of in-stock products:', + array(), + 'Admin.Catalog.Feature' + ), + ), + 'delivery_out_stock' => array( + 'label' => $this->trans( + 'Delivery time of out-of-stock products with allowed orders:', + array(), + 'Admin.Advparameters.Feature' + ), + ), + 'quantity' => array('label' => $this->trans('Quantity', array(), 'Admin.Global')), + 'minimal_quantity' => array('label' => $this->trans('Minimal quantity', array(), 'Admin.Advparameters.Feature')), + 'low_stock_threshold' => array('label' => $this->trans('Low stock level', array(), 'Admin.Catalog.Feature')), + 'low_stock_alert' => array('label' => $this->trans('Send me an email when the quantity is under this level', array(), 'Admin.Catalog.Feature')), + 'visibility' => array('label' => $this->trans('Visibility', array(), 'Admin.Catalog.Feature')), + 'additional_shipping_cost' => array('label' => $this->trans('Additional shipping cost', array(), 'Admin.Advparameters.Feature')), + 'unity' => array('label' => $this->trans('Unit for the price per unit', array(), 'Admin.Advparameters.Feature')), + 'unit_price' => array('label' => $this->trans('Price per unit', array(), 'Admin.Advparameters.Feature')), + 'description_short' => array('label' => $this->trans('Summary', array(), 'Admin.Catalog.Feature')), + 'description' => array('label' => $this->trans('Description', array(), 'Admin.Global')), + 'tags' => array('label' => $this->trans('Tags (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + 'meta_title' => array('label' => $this->trans('Meta title', array(), 'Admin.Global')), + 'meta_keywords' => array('label' => $this->trans('Meta keywords', array(), 'Admin.Global')), + 'meta_description' => array('label' => $this->trans('Meta description', array(), 'Admin.Global')), + 'link_rewrite' => array('label' => $this->trans('Rewritten URL', array(), 'Admin.Advparameters.Feature')), + 'available_now' => array('label' => $this->trans('Label when in stock', array(), 'Admin.Catalog.Feature')), + 'available_later' => array('label' => $this->trans('Label when backorder allowed', array(), 'Admin.Advparameters.Feature')), + 'available_for_order' => array('label' => $this->trans('Available for order (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature')), + 'available_date' => array('label' => $this->trans('Product availability date', array(), 'Admin.Advparameters.Feature')), + 'date_add' => array('label' => $this->trans('Product creation date', array(), 'Admin.Advparameters.Feature')), + 'show_price' => array('label' => $this->trans('Show price (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature')), + 'image' => array('label' => $this->trans('Image URLs (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + 'image_alt' => array('label' => $this->trans('Image alt texts (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + 'delete_existing_images' => array( + 'label' => $this->trans('Delete existing images (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature'), + ), + 'features' => array('label' => $this->trans('Feature (Name:Value:Position:Customized)', array(), 'Admin.Advparameters.Feature')), + 'online_only' => array('label' => $this->trans('Available online only (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature')), + 'condition' => array('label' => $this->trans('Condition', array(), 'Admin.Catalog.Feature')), + 'customizable' => array('label' => $this->trans('Customizable (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature')), + 'uploadable_files' => array('label' => $this->trans('Uploadable files (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature')), + 'text_fields' => array('label' => $this->trans('Text fields (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature')), + 'out_of_stock' => array('label' => $this->trans('Action when out of stock', array(), 'Admin.Advparameters.Feature')), + 'is_virtual' => array('label' => $this->trans('Virtual product (0 = No, 1 = Yes)', array(), 'Admin.Advparameters.Feature')), + 'file_url' => array('label' => $this->trans('File URL', array(), 'Admin.Advparameters.Feature')), + 'nb_downloadable' => array( + 'label' => $this->trans('Number of allowed downloads', array(), 'Admin.Catalog.Feature'), + 'help' => $this->trans('Number of days this file can be accessed by customers. Set to zero for unlimited access.', array(), 'Admin.Catalog.Help'), + ), + 'date_expiration' => array('label' => $this->trans('Expiration date (yyyy-mm-dd)', array(), 'Admin.Advparameters.Feature')), + 'nb_days_accessible' => array( + 'label' => $this->trans('Number of days', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Number of days this file can be accessed by customers. Set to zero for unlimited access.', array(), 'Admin.Catalog.Help'), + ), + 'shop' => array( + 'label' => $this->trans('ID / Name of shop', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', array(), 'Admin.Advparameters.Help'), + ), + 'advanced_stock_management' => array( + 'label' => $this->trans('Advanced Stock Management', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Enable Advanced Stock Management on product (0 = No, 1 = Yes).', array(), 'Admin.Advparameters.Help'), + ), + 'depends_on_stock' => array( + 'label' => $this->trans('Depends on stock', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('0 = Use quantity set in product, 1 = Use quantity from warehouse.', array(), 'Admin.Advparameters.Help'), + ), + 'warehouse' => array( + 'label' => $this->trans('Warehouse', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('ID of the warehouse to set as storage.', array(), 'Admin.Advparameters.Help'), + ), + 'accessories' => array('label' => $this->trans('Accessories (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + ); + + self::$default_values = array( + 'id_category' => array((int) Configuration::get('PS_HOME_CATEGORY')), + 'id_category_default' => null, + 'active' => '1', + 'width' => 0.000000, + 'height' => 0.000000, + 'depth' => 0.000000, + 'weight' => 0.000000, + 'visibility' => 'both', + 'additional_shipping_cost' => 0.00, + 'unit_price' => 0, + 'quantity' => 0, + 'minimal_quantity' => 1, + 'low_stock_threshold' => null, + 'low_stock_alert' => false, + 'price' => 0, + 'id_tax_rules_group' => 0, + 'description_short' => array((int) Configuration::get('PS_LANG_DEFAULT') => ''), + 'link_rewrite' => array((int) Configuration::get('PS_LANG_DEFAULT') => ''), + 'online_only' => 0, + 'condition' => 'new', + 'available_date' => date('Y-m-d'), + 'date_add' => date('Y-m-d H:i:s'), + 'date_upd' => date('Y-m-d H:i:s'), + 'customizable' => 0, + 'uploadable_files' => 0, + 'text_fields' => 0, + 'advanced_stock_management' => 0, + 'depends_on_stock' => 0, + 'is_virtual' => 0, + ); + break; + + case $this->entities[$this->trans('Customers', array(), 'Admin.Global')]: + //Overwrite required_fields AS only email is required whereas other entities + $this->required_fields = array('email', 'passwd', 'lastname', 'firstname'); + + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id' => array('label' => $this->trans('ID', array(), 'Admin.Global')), + 'active' => array('label' => $this->trans('Active (0/1)', array(), 'Admin.Advparameters.Feature')), + 'id_gender' => array('label' => $this->trans('Titles ID (Mr = 1, Ms = 2, else 0)', array(), 'Admin.Advparameters.Feature')), + 'email' => array('label' => $this->trans('Email', array(), 'Admin.Global') . '*'), + 'passwd' => array('label' => $this->trans('Password', array(), 'Admin.Global') . '*'), + 'birthday' => array('label' => $this->trans('Birth date (yyyy-mm-dd)', array(), 'Admin.Advparameters.Feature')), + 'lastname' => array('label' => $this->trans('Last name', array(), 'Admin.Global') . '*'), + 'firstname' => array('label' => $this->trans('First name', array(), 'Admin.Global') . '*'), + 'newsletter' => array('label' => $this->trans('Newsletter (0/1)', array(), 'Admin.Advparameters.Feature')), + 'optin' => array('label' => $this->trans('Partner offers (0/1)', array(), 'Admin.Advparameters.Feature')), + 'date_add' => array('label' => $this->trans('Registration date (yyyy-mm-dd)', array(), 'Admin.Advparameters.Feature')), + 'group' => array('label' => $this->trans('Groups (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + 'id_default_group' => array('label' => $this->trans('Default group ID', array(), 'Admin.Advparameters.Feature')), + 'id_shop' => array( + 'label' => $this->trans('ID / Name of shop', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', array(), 'Admin.Advparameters.Help'), + ), + ); + + self::$default_values = array( + 'active' => '1', + 'id_shop' => Configuration::get('PS_SHOP_DEFAULT'), + ); + break; + + case $this->entities[$this->trans('Addresses', array(), 'Admin.Global')]: + //Overwrite required_fields + $this->required_fields = array( + 'alias', + 'lastname', + 'firstname', + 'address1', + 'postcode', + 'country', + 'customer_email', + 'city', + ); + + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id' => array('label' => $this->trans('ID', array(), 'Admin.Global')), + 'alias' => array('label' => $this->trans('Alias', array(), 'Admin.Shopparameters.Feature') . '*'), + 'active' => array('label' => $this->trans('Active (0/1)', array(), 'Admin.Advparameters.Feature')), + 'customer_email' => array('label' => $this->trans('Customer email', array(), 'Admin.Advparameters.Feature') . '*'), + 'id_customer' => array('label' => $this->trans('Customer ID', array(), 'Admin.Advparameters.Feature')), + 'manufacturer' => array('label' => $this->trans('Brand', array(), 'Admin.Global')), + 'supplier' => array('label' => $this->trans('Supplier', array(), 'Admin.Global')), + 'company' => array('label' => $this->trans('Company', array(), 'Admin.Global')), + 'lastname' => array('label' => $this->trans('Last name', array(), 'Admin.Global') . '*'), + 'firstname' => array('label' => $this->trans('First name ', array(), 'Admin.Global') . '*'), + 'address1' => array('label' => $this->trans('Address', array(), 'Admin.Global') . '*'), + 'address2' => array('label' => $this->trans('Address (2)', array(), 'Admin.Global')), + 'postcode' => array('label' => $this->trans('Zip/postal code', array(), 'Admin.Global') . '*'), + 'city' => array('label' => $this->trans('City', array(), 'Admin.Global') . '*'), + 'country' => array('label' => $this->trans('Country', array(), 'Admin.Global') . '*'), + 'state' => array('label' => $this->trans('State', array(), 'Admin.Global')), + 'other' => array('label' => $this->trans('Other', array(), 'Admin.Global')), + 'phone' => array('label' => $this->trans('Phone', array(), 'Admin.Global')), + 'phone_mobile' => array('label' => $this->trans('Mobile Phone', array(), 'Admin.Global')), + 'vat_number' => array('label' => $this->trans('VAT number', array(), 'Admin.Orderscustomers.Feature')), + 'dni' => array('label' => $this->trans('Identification number', array(), 'Admin.Orderscustomers.Feature')), + ); + + self::$default_values = array( + 'alias' => 'Alias', + 'postcode' => 'X', + ); + break; + case $this->entities[$this->trans('Brands', array(), 'Admin.Global')]: + case $this->entities[$this->trans('Suppliers', array(), 'Admin.Global')]: + //Overwrite validators AS name is not MultiLangField + self::$validators = array( + 'description' => array('AdminImportController', 'createMultiLangField'), + 'short_description' => array('AdminImportController', 'createMultiLangField'), + 'meta_title' => array('AdminImportController', 'createMultiLangField'), + 'meta_keywords' => array('AdminImportController', 'createMultiLangField'), + 'meta_description' => array('AdminImportController', 'createMultiLangField'), + ); + + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id' => array('label' => $this->trans('ID', array(), 'Admin.Global')), + 'active' => array('label' => $this->trans('Active (0/1)', array(), 'Admin.Advparameters.Feature')), + 'name' => array('label' => $this->trans('Name', array(), 'Admin.Global')), + 'description' => array('label' => $this->trans('Description', array(), 'Admin.Global')), + 'short_description' => array('label' => $this->trans('Short description', array(), 'Admin.Catalog.Feature')), + 'meta_title' => array('label' => $this->trans('Meta title', array(), 'Admin.Global')), + 'meta_keywords' => array('label' => $this->trans('Meta keywords', array(), 'Admin.Global')), + 'meta_description' => array('label' => $this->trans('Meta description', array(), 'Admin.Global')), + 'image' => array('label' => $this->trans('Image URL', array(), 'Admin.Advparameters.Feature')), + 'shop' => array( + 'label' => $this->trans('ID / Name of group shop', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', array(), 'Admin.Advparameters.Help'), + ), + ); + + self::$default_values = array( + 'shop' => Shop::getGroupFromShop(Configuration::get('PS_SHOP_DEFAULT')), + ); + break; + case $this->entities[$this->trans('Alias', array(), 'Admin.Shopparameters.Feature')]: + //Overwrite required_fields + $this->required_fields = array( + 'alias', + 'search', + ); + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id' => array('label' => $this->trans('ID', array(), 'Admin.Global')), + 'alias' => array('label' => $this->trans('Alias', array(), 'Admin.Shopparameters.Feature') . '*'), + 'search' => array('label' => $this->trans('Search', array(), 'Admin.Shopparameters.Feature') . '*'), + 'active' => array('label' => $this->trans('Active', array(), 'Admin.Global')), + ); + self::$default_values = array( + 'active' => '1', + ); + break; + case $this->entities[$this->trans('Store contacts', array(), 'Admin.Advparameters.Feature')]: + self::$validators['hours'] = array('AdminImportController', 'split'); + self::$validators['address1'] = array('AdminImportController', 'createMultiLangField'); + self::$validators['address2'] = array('AdminImportController', 'createMultiLangField'); + + $this->required_fields = array( + 'address1', + 'city', + 'country', + 'latitude', + 'longitude', + ); + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id' => array('label' => $this->trans('ID', array(), 'Admin.Global')), + 'active' => array('label' => $this->trans('Active (0/1)', array(), 'Admin.Advparameters.Feature')), + 'name' => array('label' => $this->trans('Name', array(), 'Admin.Global')), + 'address1' => array('label' => $this->trans('Address', array(), 'Admin.Global') . '*'), + 'address2' => array('label' => $this->trans('Address (2)', array(), 'Admin.Advparameters.Feature')), + 'postcode' => array('label' => $this->trans('Zip/postal code', array(), 'Admin.Global')), + 'state' => array('label' => $this->trans('State', array(), 'Admin.Global')), + 'city' => array('label' => $this->trans('City', array(), 'Admin.Global') . '*'), + 'country' => array('label' => $this->trans('Country', array(), 'Admin.Global') . '*'), + 'latitude' => array('label' => $this->trans('Latitude', array(), 'Admin.Advparameters.Feature') . '*'), + 'longitude' => array('label' => $this->trans('Longitude', array(), 'Admin.Advparameters.Feature') . '*'), + 'phone' => array('label' => $this->trans('Phone', array(), 'Admin.Global')), + 'fax' => array('label' => $this->trans('Fax', array(), 'Admin.Global')), + 'email' => array('label' => $this->trans('Email address', array(), 'Admin.Global')), + 'note' => array('label' => $this->trans('Note', array(), 'Admin.Advparameters.Feature')), + 'hours' => array('label' => $this->trans('Hours (x,y,z...)', array(), 'Admin.Advparameters.Feature')), + 'image' => array('label' => $this->trans('Image URL', array(), 'Admin.Advparameters.Feature')), + 'shop' => array( + 'label' => $this->trans('ID / Name of shop', array(), 'Admin.Advparameters.Feature'), + 'help' => $this->trans('Ignore this field if you don\'t use the Multistore tool. If you leave this field empty, the default shop will be used.', array(), 'Admin.Advparameters.Help'), + ), + ); + self::$default_values = array( + 'active' => '1', + ); + break; + } + + // @since 1.5.0 + if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) { + switch ((int) Tools::getValue('entity')) { + case $this->entities[$this->trans('Supply Orders', array(), 'Admin.Advparameters.Feature')]: + // required fields + $this->required_fields = array( + 'id_supplier', + 'id_warehouse', + 'reference', + 'date_delivery_expected', + ); + // available fields + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'id' => array('label' => $this->trans('ID', array(), 'Admin.Global')), + 'id_supplier' => array('label' => $this->trans('Supplier ID *', array(), 'Admin.Advparameters.Feature')), + 'id_lang' => array('label' => $this->trans('Lang ID', array(), 'Admin.Advparameters.Feature')), + 'id_warehouse' => array('label' => $this->trans('Warehouse ID *', array(), 'Admin.Advparameters.Feature')), + 'id_currency' => array('label' => $this->trans('Currency ID *', array(), 'Admin.Advparameters.Feature')), + 'reference' => array('label' => $this->trans('Supply Order Reference *', array(), 'Admin.Advparameters.Feature')), + 'date_delivery_expected' => array('label' => $this->trans('Delivery Date (Y-M-D)*', array(), 'Admin.Advparameters.Feature')), + 'discount_rate' => array('label' => $this->trans('Discount rate', array(), 'Admin.Advparameters.Feature')), + 'is_template' => array('label' => $this->trans('Template', array(), 'Admin.Advparameters.Feature')), + ); + // default values + self::$default_values = array( + 'id_lang' => (int) Configuration::get('PS_LANG_DEFAULT'), + 'id_currency' => Currency::getDefaultCurrency()->id, + 'discount_rate' => '0', + 'is_template' => '0', + ); + break; + case $this->entities[$this->trans('Supply Order Details', array(), 'Admin.Advparameters.Feature')]: + // required fields + $this->required_fields = array( + 'supply_order_reference', + 'id_product', + 'unit_price_te', + 'quantity_expected', + ); + // available fields + $this->available_fields = array( + 'no' => array('label' => $this->trans('Ignore this column', array(), 'Admin.Advparameters.Feature')), + 'supply_order_reference' => array('label' => $this->trans('Supply Order Reference *', array(), 'Admin.Advparameters.Feature')), + 'id_product' => array('label' => $this->trans('Product ID *', array(), 'Admin.Advparameters.Feature')), + 'id_product_attribute' => array('label' => $this->trans('Product Attribute ID', array(), 'Admin.Advparameters.Feature')), + 'unit_price_te' => array('label' => $this->trans('Unit Price (tax excl.)*', array(), 'Admin.Advparameters.Feature')), + 'quantity_expected' => array('label' => $this->trans('Quantity Expected *', array(), 'Admin.Advparameters.Feature')), + 'discount_rate' => array('label' => $this->trans('Discount Rate', array(), 'Admin.Advparameters.Feature')), + 'tax_rate' => array('label' => $this->trans('Tax Rate', array(), 'Admin.Advparameters.Feature')), + ); + // default values + self::$default_values = array( + 'discount_rate' => '0', + 'tax_rate' => '0', + ); + break; + } + } + + $this->separator = ($separator = Tools::substr(strval(trim(Tools::getValue('separator'))), 0, 1)) ? $separator : ';'; + $this->convert = false; + $this->multiple_value_separator = ($separator = Tools::substr(strval(trim(Tools::getValue('multiple_value_separator'))), 0, 1)) ? $separator : ','; + } + + public function setMedia($isNewTheme = false) + { + $bo_theme = ((Validate::isLoadedObject($this->context->employee) + && $this->context->employee->bo_theme) ? $this->context->employee->bo_theme : 'default'); + + if (!file_exists(_PS_BO_ALL_THEMES_DIR_ . $bo_theme . DIRECTORY_SEPARATOR + . 'template')) { + $bo_theme = 'default'; + } + + // We need to set parent media first, so that jQuery is loaded before the dependant plugins + parent::setMedia($isNewTheme); + + $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $bo_theme . '/js/jquery.iframe-transport.js'); + $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $bo_theme . '/js/jquery.fileupload.js'); + $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $bo_theme . '/js/jquery.fileupload-process.js'); + $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $bo_theme . '/js/jquery.fileupload-validate.js'); + $this->addJs(__PS_BASE_URI__ . 'js/vendor/spin.js'); + $this->addJs(__PS_BASE_URI__ . 'js/vendor/ladda.js'); + } + + public function renderForm() + { + // Import form is reworked in Symfony. + // If user tries to access legacy form directly, + // we redirect him to new form. + $symfonyImportForm = $this->context->link->getAdminLink('AdminImport'); + Tools::redirectAdmin($symfonyImportForm); + + if (!is_dir(AdminImportController::getPath())) { + return !($this->errors[] = $this->trans('The import directory doesn\'t exist. Please check your file path.', array(), 'Admin.Advparameters.Notification')); + } + + if (!is_writable(AdminImportController::getPath())) { + $this->displayWarning($this->trans('The import directory must be writable (CHMOD 755 / 777).', array(), 'Admin.Advparameters.Notification')); + } + + $files_to_import = scandir(AdminImportController::getPath(), SCANDIR_SORT_NONE); + uasort($files_to_import, array('AdminImportController', 'usortFiles')); + foreach ($files_to_import as $k => &$filename) { + //exclude . .. .svn and index.php and all hidden files + if (preg_match('/^\..*|index\.php/i', $filename) || is_dir(AdminImportController::getPath() . $filename)) { + unset($files_to_import[$k]); + } + } + unset($filename); + + $this->fields_form = array(''); + + $this->toolbar_scroll = false; + $this->toolbar_btn = array(); + + // adds fancybox + $this->addJqueryPlugin(array('fancybox')); + + $entity_selected = 0; + if (isset($this->entities[$this->l(Tools::ucfirst(Tools::getValue('import_type')))])) { + $entity_selected = $this->entities[$this->l(Tools::ucfirst(Tools::getValue('import_type')))]; + $this->context->cookie->entity_selected = (int) $entity_selected; + } elseif (isset($this->context->cookie->entity_selected)) { + $entity_selected = (int) $this->context->cookie->entity_selected; + } + + $csv_selected = ''; + if (isset($this->context->cookie->csv_selected) && + @filemtime(AdminImportController::getPath( + urldecode($this->context->cookie->csv_selected) + ))) { + $csv_selected = urldecode($this->context->cookie->csv_selected); + } else { + $this->context->cookie->csv_selected = $csv_selected; + } + + $id_lang_selected = ''; + if (isset($this->context->cookie->iso_lang_selected) && $this->context->cookie->iso_lang_selected) { + $id_lang_selected = (int) Language::getIdByIso(urldecode($this->context->cookie->iso_lang_selected)); + } + + $separator_selected = $this->separator; + if (isset($this->context->cookie->separator_selected) && $this->context->cookie->separator_selected) { + $separator_selected = urldecode($this->context->cookie->separator_selected); + } + + $multiple_value_separator_selected = $this->multiple_value_separator; + if (isset($this->context->cookie->multiple_value_separator_selected) && $this->context->cookie->multiple_value_separator_selected) { + $multiple_value_separator_selected = urldecode($this->context->cookie->multiple_value_separator_selected); + } + + //get post max size + $post_max_size = ini_get('post_max_size'); + $bytes = (int) trim($post_max_size); + $last = strtolower($post_max_size[strlen($post_max_size) - 1]); + + switch ($last) { + case 'g': + $bytes *= 1024; + // no break to fall-through + case 'm': + $bytes *= 1024; + // no break to fall-through + case 'k': + $bytes *= 1024; + } + + if (!isset($bytes) || $bytes == '') { + $bytes = 20971520; + } // 20Mb + + $this->tpl_form_vars = array( + 'post_max_size' => (int) $bytes, + 'module_confirmation' => Tools::isSubmit('import') && (isset($this->warnings) && !count($this->warnings)), + 'path_import' => AdminImportController::getPath(), + 'entities' => $this->entities, + 'entity_selected' => $entity_selected, + 'csv_selected' => $csv_selected, + 'separator_selected' => $separator_selected, + 'multiple_value_separator_selected' => $multiple_value_separator_selected, + 'files_to_import' => $files_to_import, + 'languages' => Language::getLanguages(false), + 'id_language' => ($id_lang_selected) ? $id_lang_selected : $this->context->language->id, + 'available_fields' => $this->getAvailableFields(), + 'truncateAuthorized' => (Shop::isFeatureActive() && $this->context->employee->isSuperAdmin()) || !Shop::isFeatureActive(), + 'PS_ADVANCED_STOCK_MANAGEMENT' => Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT'), + ); + + return parent::renderForm(); + } + + public function ajaxProcessuploadCsv() + { + $filename_prefix = date('YmdHis') . '-'; + + if (isset($_FILES['file']) && !empty($_FILES['file']['error'])) { + switch ($_FILES['file']['error']) { + case UPLOAD_ERR_INI_SIZE: + $_FILES['file']['error'] = $this->trans('The uploaded file exceeds the upload_max_filesize directive in php.ini. If your server configuration allows it, you may add a directive in your .htaccess.', array(), 'Admin.Advparameters.Notification'); + break; + case UPLOAD_ERR_FORM_SIZE: + $_FILES['file']['error'] = $this->trans('The uploaded file exceeds the post_max_size directive in php.ini. If your server configuration allows it, you may add a directive in your .htaccess, for example:', array(), 'Admin.Advparameters.Notification') + . '
+ php_value post_max_size 20M ' . + $this->trans('(click to open "Generators" page)', array(), 'Admin.Advparameters.Notification') . ''; + break; + break; + case UPLOAD_ERR_PARTIAL: + $_FILES['file']['error'] = $this->trans('The uploaded file was only partially uploaded.', array(), 'Admin.Advparameters.Notification'); + break; + break; + case UPLOAD_ERR_NO_FILE: + $_FILES['file']['error'] = $this->trans('No file was uploaded.', array(), 'Admin.Advparameters.Notification'); + break; + break; + } + } elseif (!preg_match('#([^\.]*?)\.(csv|xls[xt]?|o[dt]s)$#is', $_FILES['file']['name'])) { + $_FILES['file']['error'] = $this->trans('The extension of your file should be .csv.', array(), 'Admin.Advparameters.Notification'); + } elseif (!@filemtime($_FILES['file']['tmp_name']) || + !@move_uploaded_file($_FILES['file']['tmp_name'], AdminImportController::getPath() . $filename_prefix . str_replace("\0", '', $_FILES['file']['name']))) { + $_FILES['file']['error'] = $this->trans('An error occurred while uploading / copying the file.', array(), 'Admin.Advparameters.Notification'); + } else { + @chmod(AdminImportController::getPath() . $filename_prefix . $_FILES['file']['name'], 0664); + $_FILES['file']['filename'] = $filename_prefix . str_replace('\0', '', $_FILES['file']['name']); + } + + die(json_encode($_FILES)); + } + + public function renderView() + { + $this->addJS(_PS_JS_DIR_ . 'admin/import.js'); + + $handle = $this->openCsvFile(); + $nb_column = $this->getNbrColumn($handle, $this->separator); + $nb_table = ceil($nb_column / MAX_COLUMNS); + + $res = array(); + foreach ($this->required_fields as $elem) { + $res[] = '\'' . $elem . '\''; + } + + $data = array(); + for ($i = 0; $i < $nb_table; ++$i) { + $data[$i] = $this->generateContentTable($i, $nb_column, $handle, $this->separator); + } + + $this->context->cookie->entity_selected = (int) Tools::getValue('entity'); + $this->context->cookie->iso_lang_selected = urlencode(Tools::getValue('iso_lang')); + $this->context->cookie->separator_selected = urlencode($this->separator); + $this->context->cookie->multiple_value_separator_selected = urlencode($this->multiple_value_separator); + $this->context->cookie->csv_selected = urlencode(Tools::getValue('csv')); + + $this->tpl_view_vars = array( + 'import_matchs' => Db::getInstance()->executeS('SELECT * FROM ' . _DB_PREFIX_ . 'import_match', true, false), + 'fields_value' => array( + 'csv' => Tools::getValue('csv'), + 'entity' => (int) Tools::getValue('entity'), + 'iso_lang' => Tools::getValue('iso_lang'), + 'truncate' => Tools::getValue('truncate'), + 'forceIDs' => Tools::getValue('forceIDs'), + 'regenerate' => Tools::getValue('regenerate'), + 'sendemail' => Tools::getValue('sendemail'), + 'match_ref' => Tools::getValue('match_ref'), + 'separator' => $this->separator, + 'multiple_value_separator' => $this->multiple_value_separator, + ), + 'nb_table' => $nb_table, + 'nb_column' => $nb_column, + 'res' => implode(',', $res), + 'max_columns' => MAX_COLUMNS, + 'no_pre_select' => array('price_tin', 'feature'), + 'available_fields' => $this->available_fields, + 'data' => $data, + ); + + return parent::renderView(); + } + + public function initToolbar() + { + switch ($this->display) { + case 'import': + // Default cancel button - like old back link + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + + $this->toolbar_btn['cancel'] = array( + 'href' => $back, + 'desc' => $this->trans('Cancel', array(), 'Admin.Actions'), + ); + // Default save button - action dynamically handled in javascript + $this->toolbar_btn['save-import'] = array( + 'href' => '#', + 'desc' => $this->trans('Import .CSV data', array(), 'Admin.Advparameters.Feature'), + ); + break; + } + } + + protected function generateContentTable($current_table, $nb_column, $handle, $glue) + { + $html = ''; + // Header + for ($i = 0; $i < $nb_column; ++$i) { + if (MAX_COLUMNS * (int) $current_table <= $i && (int) $i < MAX_COLUMNS * ((int) $current_table + 1)) { + $html .= ''; + } + } + $html .= ''; + + AdminImportController::setLocale(); + for ($current_line = 0; $current_line < 10 && $line = fgetcsv($handle, MAX_LINE_SIZE, $glue); ++$current_line) { + /* UTF-8 conversion */ + if ($this->convert) { + $line = $this->utf8EncodeArray($line); + } + $html .= ''; + foreach ($line as $nb_c => $column) { + if ((MAX_COLUMNS * (int) $current_table <= $nb_c) && ((int) $nb_c < MAX_COLUMNS * ((int) $current_table + 1))) { + $html .= ''; + } + } + $html .= ''; + } + $html .= ''; + AdminImportController::rewindBomAware($handle); + + return $html; + } + + public function init() + { + parent::init(); + if (Tools::isSubmit('submitImportFile')) { + $this->display = 'import'; + } + } + + public function initContent() + { + if ($this->display == 'import') { + if (Tools::getValue('csv')) { + $this->content .= $this->renderView(); + } else { + $this->errors[] = $this->trans('To proceed, please upload a file first.', array(), 'Admin.Advparameters.Notification'); + $this->content .= $this->renderForm(); + } + } else { + $this->content .= $this->renderForm(); + } + + $this->context->smarty->assign(array( + 'content' => $this->content, + )); + } + + protected static function rewindBomAware($handle) + { + // A rewind wrapper that skips BOM signature wrongly + if (!is_resource($handle)) { + return false; + } + rewind($handle); + if (($bom = fread($handle, 3)) != "\xEF\xBB\xBF") { + rewind($handle); + } + } + + protected static function getBoolean($field) + { + return (bool) $field; + } + + protected static function getPrice($field) + { + $field = ((float) str_replace(',', '.', $field)); + $field = ((float) str_replace('%', '', $field)); + + return $field; + } + + protected static function split($field) + { + if (empty($field)) { + return array(); + } + + $separator = Tools::getValue('multiple_value_separator'); + if (is_null($separator) || trim($separator) == '') { + $separator = ','; + } + + $tab = ''; + $uniqid_path = false; + + // try data:// protocole. If failed, old school file on filesystem. + if (($fd = @fopen('data://text/plain;base64,' . base64_encode($field), 'rb')) === false) { + do { + $uniqid_path = _PS_UPLOAD_DIR_ . uniqid(); + } while (file_exists($uniqid_path)); + file_put_contents($uniqid_path, $field); + $fd = fopen($uniqid_path, 'r'); + } + + if ($fd === false) { + return array(); + } + + $tab = fgetcsv($fd, MAX_LINE_SIZE, $separator); + fclose($fd); + if ($uniqid_path !== false && file_exists($uniqid_path)) { + @unlink($uniqid_path); + } + + if (empty($tab) || (!is_array($tab))) { + return array(); + } + + return $tab; + } + + protected static function createMultiLangField($field) + { + $res = array(); + foreach (Language::getIDs(false) as $id_lang) { + $res[$id_lang] = $field; + } + + return $res; + } + + protected function getTypeValuesOptions($nb_c) + { + $i = 0; + $no_pre_select = array('price_tin', 'feature'); + + $options = ''; + foreach ($this->available_fields as $k => $field) { + $options .= '' . "\n"; + } + + foreach ($states as $state) { + $list .= '' . "\n"; + } + } else { + $list = 'false'; + } + + die($list); + } + + /** + * Allow the assignation of zone only if the form is displayed. + */ + protected function processBulkAffectZone() + { + $zone_to_affect = Tools::getValue('zone_to_affect'); + if ($zone_to_affect && $zone_to_affect !== 0) { + parent::processBulkAffectZone(); + } + + if (Tools::getIsset('submitBulkAffectZonestate')) { + $this->tpl_list_vars['assign_zone'] = true; + } + + return; + } +} diff --git a/controllers/admin/AdminStatsController.php b/controllers/admin/AdminStatsController.php new file mode 100644 index 00000000..9728cc99 --- /dev/null +++ b/controllers/admin/AdminStatsController.php @@ -0,0 +1,1098 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +use PrestaShop\PrestaShop\Core\Addon\Theme\Theme; +use PrestaShop\PrestaShop\Core\Addon\Theme\ThemeManagerBuilder; +use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; + +class AdminStatsControllerCore extends AdminStatsTabController +{ + public static function getVisits($unique, $date_from, $date_to, $granularity = false) + { + $visits = ($granularity == false) ? 0 : array(); + $moduleManagerBuilder = ModuleManagerBuilder::getInstance(); + $moduleManager = $moduleManagerBuilder->build(); + + /** @var Gapi $gapi */ + $gapi = $moduleManager->isInstalled('gapi') ? Module::getInstanceByName('gapi') : false; + if (Validate::isLoadedObject($gapi) && $gapi->isConfigured()) + { + $metric = $unique ? 'visitors' : 'visits'; + if ($result = $gapi->requestReportData( + $granularity ? 'ga:date' : '', + 'ga:' . $metric, + $date_from, + $date_to, + null, + null, + 1, + 5000 + )) + { + foreach ($result as $row) + { + if ($granularity == 'day') + { + $visits[strtotime( + preg_replace('/^([0-9]{4})([0-9]{2})([0-9]{2})$/', '$1-$2-$3', $row['dimensions']['date']) + )] = $row['metrics'][$metric]; + } + elseif ($granularity == 'month') + { + if (!isset( + $visits[strtotime( + preg_replace( + '/^([0-9]{4})([0-9]{2})([0-9]{2})$/', + '$1-$2-01', + $row['dimensions']['date'] + ) + )] + )) + { + $visits[strtotime( + preg_replace( + '/^([0-9]{4})([0-9]{2})([0-9]{2})$/', + '$1-$2-01', + $row['dimensions']['date'] + ) + )] = 0; + } + $visits[strtotime( + preg_replace('/^([0-9]{4})([0-9]{2})([0-9]{2})$/', '$1-$2-01', $row['dimensions']['date']) + )] += $row['metrics'][$metric]; + } + else + { + $visits = $row['metrics'][$metric]; + } + } + } + } + else + { + if ($granularity == 'day') + { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT date(`date_add`) as date, COUNT(' . ($unique ? 'DISTINCT id_guest' : '*') . ') as visits + FROM `' . _DB_PREFIX_ . 'connections` + WHERE `date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() . ' + GROUP BY date(`date_add`)' + ); + foreach ($result as $row) + { + $visits[strtotime($row['date'])] = $row['visits']; + } + } + elseif ($granularity == 'month') + { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(LAST_DAY(`date_add`), 7) as date, COUNT(' . ($unique ? 'DISTINCT id_guest' : '*') . ') as visits + FROM `' . _DB_PREFIX_ . 'connections` + WHERE `date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() . ' + GROUP BY LAST_DAY(`date_add`)' + ); + foreach ($result as $row) + { + $visits[strtotime($row['date'] . '-01')] = $row['visits']; + } + } + else + { + $visits = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(' . ($unique ? 'DISTINCT id_guest' : '*') . ') as visits + FROM `' . _DB_PREFIX_ . 'connections` + WHERE `date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() + ); + } + } + + return $visits; + } + + public static function getAbandonedCarts($date_from, $date_to) + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(DISTINCT id_guest) + FROM `' . _DB_PREFIX_ . 'cart` + WHERE `date_add` BETWEEN "' . pSQL($date_from) . '" AND "' . pSQL($date_to) . '" + AND NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'orders` WHERE `' . _DB_PREFIX_ . 'orders`.id_cart = `' . _DB_PREFIX_ . 'cart`.id_cart) + ' . Shop::addSqlRestriction() + ); + } + + public static function getInstalledModules() + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(DISTINCT m.`id_module`) + FROM `' . _DB_PREFIX_ . 'module` m + ' . Shop::addSqlAssociation('module', 'm') + ); + } + + public static function getDisabledModules() + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'module` m + ' . Shop::addSqlAssociation('module', 'm', false) . ' + WHERE module_shop.id_module IS NULL OR m.active = 0' + ); + } + + public static function getModulesToUpdate() + { + $context = Context::getContext(); + $logged_on_addons = false; + if ( + isset($context->cookie->username_addons) && isset($context->cookie->password_addons) + && !empty($context->cookie->username_addons) && !empty($context->cookie->password_addons) + ) + { + $logged_on_addons = true; + } + $modules = Module::getModulesOnDisk(true, $logged_on_addons, $context->employee->id); + $upgrade_available = 0; + foreach ($modules as $km => $module) + { + if ($module->installed && isset($module->version_addons) && $module->version_addons) + { // SimpleXMLElement + ++$upgrade_available; + } + } + + return $upgrade_available; + } + + public static function getPercentProductStock() + { + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT SUM(IF(IFNULL(stock.quantity, 0) > 0, 1, 0)) AS with_stock, COUNT(*) AS products + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON p.id_product = pa.id_product + ' . Product::sqlStock('p', 'pa') . ' + WHERE product_shop.active = 1' + ); + + return round($row['products'] ? 100 * $row['with_stock'] / $row['products'] : 0, 2) . '%'; + } + + public static function getPercentProductOutOfStock() + { + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT SUM(IF(IFNULL(stock.quantity, 0) <= 0, 1, 0)) AS without_stock, COUNT(*) AS products + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON p.id_product = pa.id_product + ' . Product::sqlStock('p', 'pa') . ' + WHERE product_shop.active = 1' + ); + + return round($row['products'] ? 100 * $row['without_stock'] / $row['products'] : 0, 2) . '%'; + } + + public static function getProductAverageGrossMargin() + { + $sql = 'SELECT AVG(1 - (IF(IFNULL(product_attribute_shop.wholesale_price, 0) = 0, product_shop.wholesale_price,product_attribute_shop.wholesale_price) / (IFNULL(product_attribute_shop.price, 0) + product_shop.price))) + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON p.id_product = pa.id_product + ' . Shop::addSqlAssociation('product_attribute', 'pa', false) . ' + WHERE product_shop.active = 1'; + $value = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + + return round(100 * $value, 2) . '%'; + } + + public static function getDisabledCategories() + { + return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'category` c + ' . Shop::addSqlAssociation('category', 'c') . ' + WHERE c.active = 0' + ); + } + + public static function getTotalCategories() + { + return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'category` c + ' . Shop::addSqlAssociation('category', 'c') + ); + } + + public static function getDisabledProducts() + { + return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + WHERE product_shop.active = 0' + ); + } + + public static function getTotalProducts() + { + return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') + ); + } + + public static function getTotalSales($date_from, $date_to, $granularity = false) + { + if ($granularity == 'day') + { + $sales = array(); + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(`invoice_date`, 10) AS date, SUM(total_products / o.conversion_rate) AS sales + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 10)' + ); + foreach ($result as $row) + { + $sales[strtotime($row['date'])] = $row['sales']; + } + + return $sales; + } + elseif ($granularity == 'month') + { + $sales = array(); + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(`invoice_date`, 7) AS date, SUM(total_products / o.conversion_rate) AS sales + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 7)' + ); + foreach ($result as $row) + { + $sales[strtotime($row['date'] . '-01')] = $row['sales']; + } + + return $sales; + } + else + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT SUM(total_products / o.conversion_rate) + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') + ); + } + } + + public static function get8020SalesCatalog($date_from, $date_to) + { + $distinct_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(DISTINCT od.product_id) + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON o.id_order = od.id_order + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction(false, 'o') + ); + if (!$distinct_products) + { + return '0%'; + } + + return round(100 * $distinct_products / AdminStatsController::getTotalProducts()) . '%'; + } + + public static function getOrders($date_from, $date_to, $granularity = false) + { + if ($granularity == 'day') + { + $orders = array(); + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(`invoice_date`, 10) AS date, COUNT(*) AS orders + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 10)' + ); + foreach ($result as $row) + { + $orders[strtotime($row['date'])] = $row['orders']; + } + + return $orders; + } + elseif ($granularity == 'month') + { + $orders = array(); + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(`invoice_date`, 7) AS date, COUNT(*) AS orders + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 7)' + ); + foreach ($result as $row) + { + $orders[strtotime($row['date'] . '-01')] = $row['orders']; + } + + return $orders; + } + else + { + $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) AS orders + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') + ); + } + + return $orders; + } + + public static function getEmptyCategories() + { + $total = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'category` c + ' . Shop::addSqlAssociation('category', 'c') . ' + AND c.active = 1 + AND c.nright = c.nleft + 1' + ); + $used = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(DISTINCT cp.id_category) + FROM `' . _DB_PREFIX_ . 'category` c + LEFT JOIN `' . _DB_PREFIX_ . 'category_product` cp ON c.id_category = cp.id_category + ' . Shop::addSqlAssociation('category', 'c') . ' + AND c.active = 1 + AND c.nright = c.nleft + 1' + ); + + return intval($total - $used); + } + + public static function getCustomerMainGender() + { + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT SUM(IF(g.id_gender IS NOT NULL, 1, 0)) AS total, SUM(IF(type = 0, 1, 0)) AS male, SUM(IF(type = 1, 1, 0)) AS female, SUM(IF(type = 2, 1, 0)) AS neutral + FROM `' . _DB_PREFIX_ . 'customer` c + LEFT JOIN `' . _DB_PREFIX_ . 'gender` g ON c.id_gender = g.id_gender + WHERE c.active = 1 ' . Shop::addSqlRestriction() + ); + + if (!$row['total']) + { + return false; + } + elseif ($row['male'] > $row['female'] && $row['male'] >= $row['neutral']) + { + return array('type' => 'male', 'value' => round(100 * $row['male'] / $row['total'])); + } + elseif ($row['female'] >= $row['male'] && $row['female'] >= $row['neutral']) + { + return array('type' => 'female', 'value' => round(100 * $row['female'] / $row['total'])); + } + + return array('type' => 'neutral', 'value' => round(100 * $row['neutral'] / $row['total'])); + } + + public static function getBestCategory($date_from, $date_to) + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT ca.`id_category` + FROM `' . _DB_PREFIX_ . 'category` ca + LEFT JOIN `' . _DB_PREFIX_ . 'category_product` capr ON ca.`id_category` = capr.`id_category` + LEFT JOIN ( + SELECT pr.`id_product`, t.`totalPriceSold` + FROM `' . _DB_PREFIX_ . 'product` pr + LEFT JOIN ( + SELECT pr.`id_product`, + IFNULL(SUM(cp.`product_quantity`), 0) AS totalQuantitySold, + IFNULL(SUM(cp.`product_price` * cp.`product_quantity`), 0) / o.conversion_rate AS totalPriceSold + FROM `' . _DB_PREFIX_ . 'product` pr + LEFT OUTER JOIN `' . _DB_PREFIX_ . 'order_detail` cp ON pr.`id_product` = cp.`product_id` + LEFT JOIN `' . _DB_PREFIX_ . 'orders` o ON o.`id_order` = cp.`id_order` + WHERE o.invoice_date BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + GROUP BY pr.`id_product` + ) t ON t.`id_product` = pr.`id_product` + ) t ON t.`id_product` = capr.`id_product` + WHERE ca.`level_depth` > 1 + GROUP BY ca.`id_category` + ORDER BY SUM(t.`totalPriceSold`) DESC' + ); + } + + public static function getMainCountry($date_from, $date_to) + { + $total_orders = AdminStatsController::getOrders($date_from, $date_to); + if (!$total_orders) + { + return false; + } + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT a.id_country, COUNT(*) AS orders + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'address` a ON o.id_address_delivery = a.id_address + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() + ); + $row['orders'] = round(100 * $row['orders'] / $total_orders, 1); + + return $row; + } + + public static function getAverageCustomerAge() + { + $value = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT AVG(DATEDIFF("' . date('Y-m-d') . ' 00:00:00", birthday)) + FROM `' . _DB_PREFIX_ . 'customer` c + WHERE active = 1 + AND birthday IS NOT NULL AND birthday != "0000-00-00" ' . Shop::addSqlRestriction() + ); + + return round($value / 365); + } + + public static function getPendingMessages() + { + return CustomerThread::getTotalCustomerThreads( + 'status LIKE "%pending%" OR status = "open"' . Shop::addSqlRestriction() + ); + } + + public static function getAverageMessageResponseTime($date_from, $date_to) + { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT MIN(cm1.date_add) AS question, MIN(cm2.date_add) AS reply + FROM `' . _DB_PREFIX_ . 'customer_message` cm1 + INNER JOIN `' . _DB_PREFIX_ . 'customer_message` cm2 ON (cm1.id_customer_thread = cm2.id_customer_thread AND cm1.date_add < cm2.date_add) + JOIN `' . _DB_PREFIX_ . 'customer_thread` ct ON (cm1.id_customer_thread = ct.id_customer_thread) + WHERE cm1.`date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + AND cm1.id_employee = 0 AND cm2.id_employee != 0 + ' . Shop::addSqlRestriction() . ' + GROUP BY cm1.id_customer_thread' + ); + $total_questions = $total_replies = $threads = 0; + foreach ($result as $row) + { + ++$threads; + $total_questions += strtotime($row['question']); + $total_replies += strtotime($row['reply']); + } + if (!$threads) + { + return 0; + } + + return round(($total_replies - $total_questions) / $threads / 3600, 1); + } + + public static function getMessagesPerThread($date_from, $date_to) + { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT COUNT(*) AS messages + FROM `' . _DB_PREFIX_ . 'customer_thread` ct + LEFT JOIN `' . _DB_PREFIX_ . 'customer_message` cm ON (ct.id_customer_thread = cm.id_customer_thread) + WHERE ct.`date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() . ' + AND STATUS = "closed" + GROUP BY ct.id_customer_thread' + ); + $threads = $messages = 0; + foreach ($result as $row) + { + ++$threads; + $messages += $row['messages']; + } + if (!$threads) + { + return 0; + } + + return round($messages / $threads, 1); + } + + public static function getPurchases($date_from, $date_to, $granularity = false) + { + if ($granularity == 'day') + { + $purchases = array(); + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT + LEFT(`invoice_date`, 10) as date, + SUM(od.`product_quantity` * IF( + od.`purchase_supplier_price` > 0, + od.`purchase_supplier_price` / `conversion_rate`, + od.`original_product_price` * ' . (int) Configuration::get('CONF_AVERAGE_PRODUCT_MARGIN') . ' / 100 + )) as total_purchase_price + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON o.id_order = od.id_order + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 10)' + ); + foreach ($result as $row) + { + $purchases[strtotime($row['date'])] = $row['total_purchase_price']; + } + + return $purchases; + } + else + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT SUM(od.`product_quantity` * IF( + od.`purchase_supplier_price` > 0, + od.`purchase_supplier_price` / `conversion_rate`, + od.`original_product_price` * ' . (int) Configuration::get('CONF_AVERAGE_PRODUCT_MARGIN') . ' / 100 + )) as total_purchase_price + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON o.id_order = od.id_order + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') + ); + } + } + + public static function getExpenses($date_from, $date_to, $granularity = false) + { + $expenses = ($granularity == 'day' ? array() : 0); + + $orders = Db::getInstance()->executeS( + ' + SELECT + LEFT(`invoice_date`, 10) AS date, + total_paid_tax_incl / o.conversion_rate AS total_paid_tax_incl, + total_shipping_tax_excl / o.conversion_rate AS total_shipping_tax_excl, + o.module, + a.id_country, + o.id_currency, + c.id_reference AS carrier_reference + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'address` a ON o.id_address_delivery = a.id_address + LEFT JOIN `' . _DB_PREFIX_ . 'carrier` c ON o.id_carrier = c.id_carrier + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') + ); + foreach ($orders as $order) + { + // Add flat fees for this order + $flat_fees = Configuration::get('CONF_ORDER_FIXED') + ( + $order['id_currency'] == Configuration::get('PS_CURRENCY_DEFAULT') + ? Configuration::get('CONF_' . strtoupper($order['module']) . '_FIXED') + : Configuration::get('CONF_' . strtoupper($order['module']) . '_FIXED_FOREIGN') + ); + + // Add variable fees for this order + $var_fees = $order['total_paid_tax_incl'] * ( + $order['id_currency'] == Configuration::get('PS_CURRENCY_DEFAULT') + ? Configuration::get('CONF_' . strtoupper($order['module']) . '_VAR') + : Configuration::get('CONF_' . strtoupper($order['module']) . '_VAR_FOREIGN') + ) / 100; + + // Add shipping fees for this order + $shipping_fees = $order['total_shipping_tax_excl'] * ( + $order['id_country'] == Configuration::get('PS_COUNTRY_DEFAULT') + ? Configuration::get('CONF_' . strtoupper($order['carrier_reference']) . '_SHIP') + : Configuration::get('CONF_' . strtoupper($order['carrier_reference']) . '_SHIP_OVERSEAS') + ) / 100; + + // Tally up these fees + if ($granularity == 'day') + { + if (!isset($expenses[strtotime($order['date'])])) + { + $expenses[strtotime($order['date'])] = 0; + } + $expenses[strtotime($order['date'])] += $flat_fees + $var_fees + $shipping_fees; + } + else + { + $expenses += $flat_fees + $var_fees + $shipping_fees; + } + } + + return $expenses; + } + + public function displayAjaxGetKpi() + { + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + $tooltip = null; + switch (Tools::getValue('kpi')) + { + case 'conversion_rate': + $visitors = AdminStatsController::getVisits( + true, + date('Y-m-d', strtotime('-31 day')), + date('Y-m-d', strtotime('-1 day')), + false /*'day'*/ + ); + $orders = AdminStatsController::getOrders( + date('Y-m-d', strtotime('-31 day')), + date('Y-m-d', strtotime('-1 day')), + false /*'day'*/ + ); + + // $data = array(); + // $from = strtotime(date('Y-m-d 00:00:00', strtotime('-31 day'))); + // $to = strtotime(date('Y-m-d 23:59:59', strtotime('-1 day'))); + // for ($date = $from; $date <= $to; $date = strtotime('+1 day', $date)) + // if (isset($visitors[$date]) && $visitors[$date]) + // $data[$date] = round(100 * ((isset($orders[$date]) && $orders[$date]) ? $orders[$date] : 0) / $visitors[$date], 2); + // else + // $data[$date] = 0; + + $visits_sum = $visitors; //array_sum($visitors); + $orders_sum = $orders; //array_sum($orders); + if ($visits_sum) + { + $value = round(100 * $orders_sum / $visits_sum, 2); + } + elseif ($orders_sum) + { + $value = '∞'; + } + else + { + $value = 0; + } + $value .= '%'; + + // ConfigurationKPI::updateValue('CONVERSION_RATE_CHART', json_encode($data)); + ConfigurationKPI::updateValue('CONVERSION_RATE', $value); + ConfigurationKPI::updateValue( + 'CONVERSION_RATE_EXPIRE', + strtotime(date('Y-m-d 00:00:00', strtotime('+1 day'))) + ); + break; + + case 'abandoned_cart': + $value = AdminStatsController::getAbandonedCarts( + date('Y-m-d H:i:s', strtotime('-2 day')), + date('Y-m-d H:i:s', strtotime('-1 day')) + ); + ConfigurationKPI::updateValue('ABANDONED_CARTS', $value); + ConfigurationKPI::updateValue('ABANDONED_CARTS_EXPIRE', strtotime('+1 hour')); + break; + + case 'installed_modules': + $value = AdminStatsController::getInstalledModules(); + ConfigurationKPI::updateValue('INSTALLED_MODULES', $value); + ConfigurationKPI::updateValue('INSTALLED_MODULES_EXPIRE', strtotime('+2 min')); + break; + + case 'disabled_modules': + $value = AdminStatsController::getDisabledModules(); + ConfigurationKPI::updateValue('DISABLED_MODULES', $value); + ConfigurationKPI::updateValue('DISABLED_MODULES_EXPIRE', strtotime('+2 min')); + break; + + case 'update_modules': + $value = AdminStatsController::getModulesToUpdate(); + ConfigurationKPI::updateValue('UPDATE_MODULES', $value); + ConfigurationKPI::updateValue('UPDATE_MODULES_EXPIRE', strtotime('+2 min')); + break; + + case 'percent_product_stock': + $value = AdminStatsController::getPercentProductStock(); + ConfigurationKPI::updateValue('PERCENT_PRODUCT_STOCK', $value); + ConfigurationKPI::updateValue('PERCENT_PRODUCT_STOCK_EXPIRE', strtotime('+4 hour')); + break; + + case 'percent_product_out_of_stock': + $value = AdminStatsController::getPercentProductOutOfStock(); + $tooltip = $this->trans( + '%value% of your products for sale are out of stock.', + array('%value%' => $value), + 'Admin.Stats.Help' + ); + ConfigurationKPI::updateValue('PERCENT_PRODUCT_OUT_OF_STOCK', $value); + ConfigurationKPI::updateValue('PERCENT_PRODUCT_OUT_OF_STOCK_EXPIRE', strtotime('+4 hour')); + break; + + case 'product_avg_gross_margin': + $value = AdminStatsController::getProductAverageGrossMargin(); + $tooltip = $this->trans( + 'Gross margin expressed in percentage assesses how cost-effectively you sell your goods. Out of $100, you will retain $%value% to cover profit and expenses.', + array('%value%' => $value), + 'Admin.Stats.Help' + ); + ConfigurationKPI::updateValue('PRODUCT_AVG_GROSS_MARGIN', $value); + ConfigurationKPI::updateValue('PRODUCT_AVG_GROSS_MARGIN_EXPIRE', strtotime('+6 hour')); + break; + + case 'disabled_categories': + $value = AdminStatsController::getDisabledCategories(); + ConfigurationKPI::updateValue('DISABLED_CATEGORIES', $value); + ConfigurationKPI::updateValue('DISABLED_CATEGORIES_EXPIRE', strtotime('+2 hour')); + break; + + case 'disabled_products': + $value = round( + 100 * AdminStatsController::getDisabledProducts() / AdminStatsController::getTotalProducts(), + 2 + ) . '%'; + $tooltip = $this->trans( + '%value% of your products are disabled and not visible to your customers', + array('%value%' => $value), + 'Admin.Stats.Help' + ); + ConfigurationKPI::updateValue('DISABLED_PRODUCTS', $value); + ConfigurationKPI::updateValue('DISABLED_PRODUCTS_EXPIRE', strtotime('+2 hour')); + break; + + case '8020_sales_catalog': + $value = AdminStatsController::get8020SalesCatalog(date('Y-m-d', strtotime('-30 days')), date('Y-m-d')); + $tooltip = $this->trans( + 'Within your catalog, %value% of your products have had sales in the last 30 days', + array('%value%' => $value), + 'Admin.Stats.Help' + ); + $value = $this->trans('%value%% of your Catalog', array('%value%' => $value), 'Admin.Stats.Feature'); + ConfigurationKPI::updateValue('8020_SALES_CATALOG', $value); + ConfigurationKPI::updateValue('8020_SALES_CATALOG_EXPIRE', strtotime('+12 hour')); + break; + + case 'empty_categories': + $value = AdminStatsController::getEmptyCategories(); + ConfigurationKPI::updateValue('EMPTY_CATEGORIES', $value); + ConfigurationKPI::updateValue('EMPTY_CATEGORIES_EXPIRE', strtotime('+2 hour')); + break; + + case 'customer_main_gender': + $value = AdminStatsController::getCustomerMainGender(); + + if ($value === false) + { + $value = $this->trans('No customers', array(), 'Admin.Stats.Feature'); + } + elseif ($value['type'] == 'female') + { + $value = $this->trans('%percentage%% Female Customers', array('%percentage%' => $value['value']), 'Admin.Stats.Feature'); + } + elseif ($value['type'] == 'male') + { + $value = $this->trans('%percentage%% Male Customers', array('%percentage%' => $value['value']), 'Admin.Stats.Feature'); + } + else + { + $value = $this->trans('%percentage%% Neutral Customers', array('%percentage%' => $value['value']), 'Admin.Stats.Feature'); + } + + ConfigurationKPI::updateValue('CUSTOMER_MAIN_GENDER', array($this->context->language->id => $value)); + ConfigurationKPI::updateValue( + 'CUSTOMER_MAIN_GENDER_EXPIRE', + array($this->context->language->id => strtotime('+1 day')) + ); + break; + + case 'avg_customer_age': + $value = $this->trans('%value% years', array('%value%' => AdminStatsController::getAverageCustomerAge()), 'Admin.Stats.Feature'); + ConfigurationKPI::updateValue('AVG_CUSTOMER_AGE', array($this->context->language->id => $value)); + ConfigurationKPI::updateValue( + 'AVG_CUSTOMER_AGE_EXPIRE', + array($this->context->language->id => strtotime('+1 day')) + ); + break; + + case 'pending_messages': + $value = (int) AdminStatsController::getPendingMessages(); + ConfigurationKPI::updateValue('PENDING_MESSAGES', $value); + ConfigurationKPI::updateValue('PENDING_MESSAGES_EXPIRE', strtotime('+5 min')); + break; + + case 'avg_msg_response_time': + $value = $this->trans('%average% hours', array('%average%' => AdminStatsController::getAverageMessageResponseTime( + date('Y-m-d', strtotime('-31 day')), + date('Y-m-d', strtotime('-1 day')) + )), 'Admin.Stats.Feature'); + ConfigurationKPI::updateValue('AVG_MSG_RESPONSE_TIME', $value); + ConfigurationKPI::updateValue('AVG_MSG_RESPONSE_TIME_EXPIRE', strtotime('+4 hour')); + break; + + case 'messages_per_thread': + $value = round( + AdminStatsController::getMessagesPerThread( + date('Y-m-d', strtotime('-31 day')), + date('Y-m-d', strtotime('-1 day')) + ), + 1 + ); + ConfigurationKPI::updateValue('MESSAGES_PER_THREAD', $value); + ConfigurationKPI::updateValue('MESSAGES_PER_THREAD_EXPIRE', strtotime('+12 hour')); + break; + + case 'newsletter_registrations': + $moduleManagerBuilder = ModuleManagerBuilder::getInstance(); + $moduleManager = $moduleManagerBuilder->build(); + + $value = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'customer` + WHERE newsletter = 1 + ' . Shop::addSqlRestriction(Shop::SHARE_ORDER) + ); + if ($moduleManager->isInstalled('ps_emailsubscription')) + { + $value += Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'emailsubscription` + WHERE active = 1 + ' . Shop::addSqlRestriction(Shop::SHARE_ORDER) + ); + } + + ConfigurationKPI::updateValue('NEWSLETTER_REGISTRATIONS', $value); + ConfigurationKPI::updateValue('NEWSLETTER_REGISTRATIONS_EXPIRE', strtotime('+6 hour')); + break; + + case 'enabled_languages': + $value = Language::countActiveLanguages(); + ConfigurationKPI::updateValue('ENABLED_LANGUAGES', $value); + ConfigurationKPI::updateValue('ENABLED_LANGUAGES_EXPIRE', strtotime('+1 min')); + break; + + case 'frontoffice_translations': + $themes = (new ThemeManagerBuilder($this->context, Db::getInstance())) + ->buildRepository() + ->getList(); + $languages = Language::getLanguages(); + $total = $translated = 0; + foreach ($themes as $theme) + { + /* @var Theme $theme */ + foreach ($languages as $language) + { + $kpi_key = substr(strtoupper($theme->getName() . '_' . $language['iso_code']), 0, 16); + $total += ConfigurationKPI::get('TRANSLATE_TOTAL_' . $kpi_key); + $translated += ConfigurationKPI::get('TRANSLATE_DONE_' . $kpi_key); + } + } + $value = 0; + if ($translated) + { + $value = round(100 * $translated / $total, 1); + } + $value .= '%'; + ConfigurationKPI::updateValue('FRONTOFFICE_TRANSLATIONS', $value); + ConfigurationKPI::updateValue('FRONTOFFICE_TRANSLATIONS_EXPIRE', strtotime('+2 min')); + break; + + case 'main_country': + if (!($row = AdminStatsController::getMainCountry( + date('Y-m-d', strtotime('-30 day')), + date('Y-m-d') + ))) + { + $value = $this->trans('No orders', array(), 'Admin.Stats.Feature'); + } + else + { + $country = new Country($row['id_country'], $this->context->language->id); + $value = $this->trans( + '%d%% %s', + array('%d%%' => $row['orders'], '%s' => $country->name), + 'Admin.Stats.Feature' + ); + } + + ConfigurationKPI::updateValue('MAIN_COUNTRY', array($this->context->language->id => $value)); + ConfigurationKPI::updateValue( + 'MAIN_COUNTRY_EXPIRE', + array($this->context->language->id => strtotime('+1 day')) + ); + break; + + case 'orders_per_customer': + $value = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'customer` c + WHERE c.active = 1 + ' . Shop::addSqlRestriction() + ); + if ($value) + { + $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'orders` o + WHERE o.valid = 1 + ' . Shop::addSqlRestriction() + ); + $value = round($orders / $value, 2); + } + + ConfigurationKPI::updateValue('ORDERS_PER_CUSTOMER', $value); + ConfigurationKPI::updateValue('ORDERS_PER_CUSTOMER_EXPIRE', strtotime('+1 day')); + break; + + case 'average_order_value': + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT + COUNT(`id_order`) AS orders, + SUM(`total_paid_tax_excl` / `conversion_rate`) AS total_paid_tax_excl + FROM `' . _DB_PREFIX_ . 'orders` + WHERE `invoice_date` BETWEEN "' . pSQL(date('Y-m-d', strtotime('-31 day'))) . ' 00:00:00" AND "' . pSQL( + date('Y-m-d', strtotime('-1 day')) + ) . ' 23:59:59" + ' . Shop::addSqlRestriction() + ); + $value = Tools::displayPrice( + $row['orders'] ? $row['total_paid_tax_excl'] / $row['orders'] : 0, + $currency + ); + ConfigurationKPI::updateValue('AVG_ORDER_VALUE', $value); + ConfigurationKPI::updateValue( + 'AVG_ORDER_VALUE_EXPIRE', + strtotime(date('Y-m-d 00:00:00', strtotime('+1 day'))) + ); + break; + + case 'netprofit_visit': + $date_from = date('Y-m-d', strtotime('-31 day')); + $date_to = date('Y-m-d', strtotime('-1 day')); + + $total_visitors = AdminStatsController::getVisits(false, $date_from, $date_to); + $net_profits = AdminStatsController::getTotalSales($date_from, $date_to); + $net_profits -= AdminStatsController::getExpenses($date_from, $date_to); + $net_profits -= AdminStatsController::getPurchases($date_from, $date_to); + + if ($total_visitors) + { + $value = Tools::displayPrice($net_profits / $total_visitors, $currency); + } + elseif ($net_profits) + { + $value = '∞'; + } + else + { + $value = Tools::displayPrice(0, $currency); + } + + ConfigurationKPI::updateValue('NETPROFIT_VISIT', $value); + ConfigurationKPI::updateValue( + 'NETPROFIT_VISIT_EXPIRE', + strtotime(date('Y-m-d 00:00:00', strtotime('+1 day'))) + ); + break; + + case 'products_per_category': + $products = AdminStatsController::getTotalProducts(); + $categories = AdminStatsController::getTotalCategories(); + $value = round($products / $categories); + ConfigurationKPI::updateValue('PRODUCTS_PER_CATEGORY', $value); + ConfigurationKPI::updateValue('PRODUCTS_PER_CATEGORY_EXPIRE', strtotime('+1 hour')); + break; + + case 'top_category': + if (!($id_category = AdminStatsController::getBestCategory( + date('Y-m-d', strtotime('-1 month')), + date('Y-m-d') + ))) + { + $value = $this->trans('No category', array(), 'Admin.Stats.Feature'); + } + else + { + $category = new Category($id_category, $this->context->language->id); + $value = $category->name; + } + + ConfigurationKPI::updateValue('TOP_CATEGORY', array($this->context->language->id => $value)); + ConfigurationKPI::updateValue( + 'TOP_CATEGORY_EXPIRE', + array($this->context->language->id => strtotime('+1 day')) + ); + break; + + default: + $value = false; + } + if ($value !== false) + { + $array = array('value' => $value, 'tooltip' => $tooltip); + if (isset($data)) + { + $array['data'] = $data; + } + die(json_encode($array)); + } + die(json_encode(array('has_errors' => true))); + } +} diff --git a/controllers/admin/AdminStatsTabController.php b/controllers/admin/AdminStatsTabController.php new file mode 100644 index 00000000..66fdb320 --- /dev/null +++ b/controllers/admin/AdminStatsTabController.php @@ -0,0 +1,309 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +abstract class AdminStatsTabControllerCore extends AdminPreferencesControllerCore +{ + public function init() + { + parent::init(); + + $this->action = 'view'; + $this->display = 'view'; + } + + public function initContent() + { + if ($this->ajax) { + return; + } + + $this->addToolBarModulesListButton(); + $this->toolbar_title = $this->trans('Stats', array(), 'Admin.Stats.Feature'); + + if ($this->display == 'view') { + // Some controllers use the view action without an object + if ($this->className) { + $this->loadObject(true); + } + $this->content .= $this->renderView(); + } + + $this->content .= $this->displayMenu(); + $this->content .= $this->displayCalendar(); + $this->content .= $this->displayStats(); + + $this->context->smarty->assign(array( + 'content' => $this->content, + )); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + unset($this->page_header_toolbar_btn['back']); + } + + public function displayCalendar() + { + return AdminStatsTabController::displayCalendarForm(array( + 'Calendar' => $this->trans('Calendar', array(), 'Admin.Global'), + 'Day' => $this->trans('Day', array(), 'Admin.Global'), + 'Month' => $this->trans('Month', array(), 'Admin.Global'), + 'Year' => $this->trans('Year', array(), 'Admin.Global'), + 'From' => $this->trans('From:', array(), 'Admin.Global'), + 'To' => $this->trans('To:', array(), 'Admin.Global'), + 'Save' => $this->trans('Save', array(), 'Admin.Global'), + ), $this->token); + } + + public static function displayCalendarForm($translations, $token, $action = null, $table = null, $identifier = null, $id = null) + { + $context = Context::getContext(); + + $tpl = $context->controller->createTemplate('calendar.tpl'); + + $context->controller->addJqueryUI('ui.datepicker'); + + if ($identifier === null && Tools::getValue('module')) { + $identifier = 'module'; + $id = Tools::getValue('module'); + } + + $action = Context::getContext()->link->getAdminLink('AdminStats'); + $action .= ($action && $table ? '&' . Tools::safeOutput($action) : ''); + $action .= ($identifier && $id ? '&' . Tools::safeOutput($identifier) . '=' . (int) $id : ''); + $module = Tools::getValue('module'); + $action .= ($module ? '&module=' . Tools::safeOutput($module) : ''); + $action .= (($id_product = Tools::getValue('id_product')) ? '&id_product=' . Tools::safeOutput($id_product) : ''); + $tpl->assign(array( + 'current' => self::$currentIndex, + 'token' => $token, + 'action' => $action, + 'table' => $table, + 'identifier' => $identifier, + 'id' => $id, + 'translations' => $translations, + 'datepickerFrom' => Tools::getValue('datepickerFrom', $context->employee->stats_date_from), + 'datepickerTo' => Tools::getValue('datepickerTo', $context->employee->stats_date_to), + )); + + return $tpl->fetch(); + } + + /* Not used anymore, but still work */ + protected function displayEngines() + { + $tpl = $this->createTemplate('engines.tpl'); + + $autoclean_period = array( + 'never' => $this->trans('Never', array(), 'Admin.Global'), + 'week' => $this->trans('Week', array(), 'Admin.Global'), + 'month' => $this->trans('Month', array(), 'Admin.Global'), + 'year' => $this->trans('Year', array(), 'Admin.Global'), + ); + + $tpl->assign(array( + 'current' => self::$currentIndex, + 'token' => $this->token, + 'graph_engine' => Configuration::get('PS_STATS_RENDER'), + 'grid_engine' => Configuration::get('PS_STATS_GRID_RENDER'), + 'auto_clean' => Configuration::get('PS_STATS_OLD_CONNECT_AUTO_CLEAN'), + 'array_graph_engines' => ModuleGraphEngine::getGraphEngines(), + 'array_grid_engines' => ModuleGridEngine::getGridEngines(), + 'array_auto_clean' => $autoclean_period, + )); + + return $tpl->fetch(); + } + + public function displayMenu() + { + $tpl = $this->createTemplate('menu.tpl'); + + $modules = $this->getModules(); + $module_instance = array(); + foreach ($modules as $m => $module) { + if ($module_instance[$module['name']] = Module::getInstanceByName($module['name'])) { + $modules[$m]['displayName'] = $module_instance[$module['name']]->displayName; + } else { + unset($module_instance[$module['name']]); + unset($modules[$m]); + } + } + + uasort($modules, array($this, 'checkModulesNames')); + + $tpl->assign(array( + 'current' => self::$currentIndex, + 'current_module_name' => Tools::getValue('module', 'statsforecast'), + 'token' => $this->token, + 'modules' => $modules, + 'module_instance' => $module_instance, + )); + + return $tpl->fetch(); + } + + public function checkModulesNames($a, $b) + { + return (bool) ($a['displayName'] > $b['displayName']); + } + + protected function getModules() + { + $sql = 'SELECT h.`name` AS hook, m.`name` + FROM `' . _DB_PREFIX_ . 'module` m + LEFT JOIN `' . _DB_PREFIX_ . 'hook_module` hm ON hm.`id_module` = m.`id_module` + LEFT JOIN `' . _DB_PREFIX_ . 'hook` h ON hm.`id_hook` = h.`id_hook` + WHERE h.`name` = \'displayAdminStatsModules\' + AND m.`active` = 1 + GROUP BY hm.id_module + ORDER BY hm.`position`'; + + return Db::getInstance()->executeS($sql); + } + + public function displayStats() + { + $tpl = $this->createTemplate('stats.tpl'); + + if ((!($module_name = Tools::getValue('module')) || !Validate::isModuleName($module_name)) && ($module_instance = Module::getInstanceByName('statsforecast')) && $module_instance->active) { + $module_name = 'statsforecast'; + } + + if ($module_name) { + $_GET['module'] = $module_name; + + if (!isset($module_instance)) { + $module_instance = Module::getInstanceByName($module_name); + } + + if ($module_instance && $module_instance->active) { + $hook = Hook::exec('displayAdminStatsModules', null, $module_instance->id); + } + } + + $tpl->assign(array( + 'module_name' => $module_name, + 'module_instance' => isset($module_instance) ? $module_instance : null, + 'hook' => isset($hook) ? $hook : null, + )); + + return $tpl->fetch(); + } + + public function postProcess() + { + $this->context = Context::getContext(); + + $this->processDateRange(); + + if (Tools::getValue('submitSettings')) { + if ($this->access('edit')) { + self::$currentIndex .= '&module=' . Tools::getValue('module'); + Configuration::updateValue('PS_STATS_RENDER', Tools::getValue('PS_STATS_RENDER', Configuration::get('PS_STATS_RENDER'))); + Configuration::updateValue('PS_STATS_GRID_RENDER', Tools::getValue('PS_STATS_GRID_RENDER', Configuration::get('PS_STATS_GRID_RENDER'))); + Configuration::updateValue('PS_STATS_OLD_CONNECT_AUTO_CLEAN', Tools::getValue('PS_STATS_OLD_CONNECT_AUTO_CLEAN', Configuration::get('PS_STATS_OLD_CONNECT_AUTO_CLEAN'))); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } + } + + public function processDateRange() + { + if (Tools::isSubmit('submitDatePicker')) { + if ((!Validate::isDate($from = Tools::getValue('datepickerFrom')) || !Validate::isDate($to = Tools::getValue('datepickerTo'))) || (strtotime($from) > strtotime($to))) { + $this->errors[] = $this->trans('The specified date is invalid.', array(), 'Admin.Stats.Notification'); + } + } + if (Tools::isSubmit('submitDateDay')) { + $from = date('Y-m-d'); + $to = date('Y-m-d'); + } + if (Tools::isSubmit('submitDateDayPrev')) { + $yesterday = time() - 60 * 60 * 24; + $from = date('Y-m-d', $yesterday); + $to = date('Y-m-d', $yesterday); + } + if (Tools::isSubmit('submitDateMonth')) { + $from = date('Y-m-01'); + $to = date('Y-m-t'); + } + if (Tools::isSubmit('submitDateMonthPrev')) { + $m = (date('m') == 1 ? 12 : date('m') - 1); + $y = ($m == 12 ? date('Y') - 1 : date('Y')); + $from = $y . '-' . $m . '-01'; + $to = $y . '-' . $m . date('-t', mktime(12, 0, 0, $m, 15, $y)); + } + if (Tools::isSubmit('submitDateYear')) { + $from = date('Y-01-01'); + $to = date('Y-12-31'); + } + if (Tools::isSubmit('submitDateYearPrev')) { + $from = (date('Y') - 1) . date('-01-01'); + $to = (date('Y') - 1) . date('-12-31'); + } + if (isset($from) && isset($to) && !count($this->errors)) { + $this->context->employee->stats_date_from = $from; + $this->context->employee->stats_date_to = $to; + $this->context->employee->update(); + if (!$this->isXmlHttpRequest()) { + Tools::redirectAdmin($_SERVER['REQUEST_URI']); + } + } + } + + public function ajaxProcessSetDashboardDateRange() + { + $this->processDateRange(); + + if ($this->isXmlHttpRequest()) { + if (is_array($this->errors) && count($this->errors)) { + die(json_encode(array( + 'has_errors' => true, + 'errors' => array($this->errors), + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, ) + )); + } else { + die(json_encode(array( + 'has_errors' => false, + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, ) + )); + } + } + } + + protected function getDate() + { + $year = isset($this->context->cookie->stats_year) ? $this->context->cookie->stats_year : date('Y'); + $month = isset($this->context->cookie->stats_month) ? sprintf('%02d', $this->context->cookie->stats_month) : '%'; + $day = isset($this->context->cookie->stats_day) ? sprintf('%02d', $this->context->cookie->stats_day) : '%'; + + return $year . '-' . $month . '-' . $day; + } +} diff --git a/controllers/admin/AdminStatusesController.php b/controllers/admin/AdminStatusesController.php new file mode 100644 index 00000000..e3d2628d --- /dev/null +++ b/controllers/admin/AdminStatusesController.php @@ -0,0 +1,692 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property OrderState $object + */ +class AdminStatusesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'order_state'; + $this->className = 'OrderState'; + $this->lang = true; + $this->deleted = false; + $this->colorOnBackground = false; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->imageType = 'gif'; + $this->fieldImageSettings = array( + 'name' => 'icon', + 'dir' => 'os', + ); + + parent::__construct(); + + $this->bulk_actions = array('delete' => array('text' => $this->trans('Delete selected', array(), 'Admin.Actions'), 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'))); + } + + public function init() + { + if (Tools::isSubmit('addorder_return_state')) { + $this->display = 'add'; + } + if (Tools::isSubmit('updateorder_return_state')) { + $this->display = 'edit'; + } + + return parent::init(); + } + + /** + * init all variables to render the order status list. + */ + protected function initOrderStatutsList() + { + $this->fields_list = array( + 'id_order_state' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + 'width' => 'auto', + 'color' => 'color', + ), + 'logo' => array( + 'title' => $this->trans('Icon', array(), 'Admin.Shopparameters.Feature'), + 'align' => 'text-center', + 'image' => 'os', + 'orderby' => false, + 'search' => false, + 'class' => 'fixed-width-xs', + ), + 'send_email' => array( + 'title' => $this->trans('Send email to customer', array(), 'Admin.Shopparameters.Feature'), + 'align' => 'text-center', + 'active' => 'sendEmail', + 'type' => 'bool', + 'ajax' => true, + 'orderby' => false, + 'class' => 'fixed-width-sm', + ), + 'delivery' => array( + 'title' => $this->trans('Delivery', array(), 'Admin.Global'), + 'align' => 'text-center', + 'active' => 'delivery', + 'type' => 'bool', + 'ajax' => true, + 'orderby' => false, + 'class' => 'fixed-width-sm', + ), + 'invoice' => array( + 'title' => $this->trans('Invoice', array(), 'Admin.Global'), + 'align' => 'text-center', + 'active' => 'invoice', + 'type' => 'bool', + 'ajax' => true, + 'orderby' => false, + 'class' => 'fixed-width-sm', + ), + 'template' => array( + 'title' => $this->trans('Email template', array(), 'Admin.Shopparameters.Feature'), + ), + ); + } + + /** + * init all variables to render the order return list. + */ + protected function initOrdersReturnsList() + { + $this->table = 'order_return_state'; + $this->className = 'OrderReturnState'; + $this->_defaultOrderBy = $this->identifier = 'id_order_return_state'; + $this->list_id = 'order_return_state'; + $this->deleted = false; + $this->_orderBy = null; + + $this->fields_list = array( + 'id_order_return_state' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + 'align' => 'left', + 'width' => 'auto', + 'color' => 'color', + ), + ); + } + + protected function initOrderReturnsForm() + { + $id_order_return_state = (int) Tools::getValue('id_order_return_state'); + + // Create Object OrderReturnState + $order_return_state = new OrderReturnState($id_order_return_state); + + //init field form variable for order return form + $this->fields_form = array(); + + //$this->initToolbar(); + $this->getlanguages(); + $helper = new HelperForm(); + $helper->currentIndex = self::$currentIndex; + $helper->token = $this->token; + $helper->table = 'order_return_state'; + $helper->identifier = 'id_order_return_state'; + $helper->id = $order_return_state->id; + $helper->toolbar_scroll = false; + $helper->languages = $this->_languages; + $helper->default_form_language = $this->default_form_language; + $helper->allow_employee_form_lang = $this->allow_employee_form_lang; + + if ($order_return_state->id) { + $helper->fields_value = array( + 'name' => $this->getFieldValue($order_return_state, 'name'), + 'color' => $this->getFieldValue($order_return_state, 'color'), + ); + } else { + $helper->fields_value = array( + 'name' => $this->getFieldValue($order_return_state, 'name'), + 'color' => '#ffffff', + ); + } + + $helper->toolbar_btn = $this->toolbar_btn; + $helper->title = $this->trans('Edit return status', array(), 'Admin.Shopparameters.Feature'); + + return $helper; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_order_state'] = array( + 'href' => self::$currentIndex . '&addorder_state&token=' . $this->token, + 'desc' => $this->trans('Add new order status', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ); + $this->page_header_toolbar_btn['new_order_return_state'] = array( + 'href' => self::$currentIndex . '&addorder_return_state&token=' . $this->token, + 'desc' => $this->trans('Add new order return status', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + /** + * Function used to render the list to display for this controller. + */ + public function renderList() + { + //init and render the first list + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->addRowActionSkipList('delete', $this->getUnremovableStatuses()); + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + $this->initOrderStatutsList(); + $lists = parent::renderList(); + + //init and render the second list + $this->list_skip_actions = array(); + $this->_filter = false; + $this->addRowActionSkipList('delete', array(1, 2, 3, 4, 5)); + $this->initOrdersReturnsList(); + $this->checkFilterForOrdersReturnsList(); + + // call postProcess() to take care of actions and filters + $this->postProcess(); + $this->toolbar_title = $this->trans('Return statuses', array(), 'Admin.Shopparameters.Feature'); + + parent::initToolbar(); + $lists .= parent::renderList(); + + return $lists; + } + + protected function getUnremovableStatuses() + { + return array_map(function ($row) { + return (int) $row['id_order_state']; + }, Db::getInstance()->executeS('SELECT id_order_state FROM ' . _DB_PREFIX_ . 'order_state WHERE unremovable = 1')); + } + + protected function checkFilterForOrdersReturnsList() + { + // test if a filter is applied for this list + if (Tools::isSubmit('submitFilter' . $this->table) || $this->context->cookie->{'submitFilter' . $this->table} !== false) { + $this->filter = true; + } + + // test if a filter reset request is required for this list + if (isset($_POST['submitReset' . $this->table])) { + $this->action = 'reset_filters'; + } else { + $this->action = ''; + } + } + + public function renderForm() + { + $this->fields_form = array( + 'tinymce' => true, + 'legend' => array( + 'title' => $this->trans('Order status', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-time', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Status name', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => array( + $this->trans('Order status (e.g. \'Pending\').', array(), 'Admin.Shopparameters.Help'), + $this->trans('Invalid characters: numbers and', array(), 'Admin.Shopparameters.Help') . ' !<>,;?=+()@#"{}_$%:', + ), + ), + array( + 'type' => 'file', + 'label' => $this->trans('Icon', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'icon', + 'hint' => $this->trans('Upload an icon from your computer (File type: .gif, suggested size: 16x16).', array(), 'Admin.Shopparameters.Help'), + ), + array( + 'type' => 'color', + 'label' => $this->trans('Color', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'color', + 'hint' => $this->trans('Status will be highlighted in this color. HTML colors only.', array(), 'Admin.Shopparameters.Help') . ' "lightblue", "#CC6600")', + ), + array( + 'type' => 'checkbox', + 'name' => 'logable', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Consider the associated order as validated.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'checkbox', + 'name' => 'invoice', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Allow a customer to download and view PDF versions of his/her invoices.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'checkbox', + 'name' => 'hidden', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Hide this status in all customer orders.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'checkbox', + 'name' => 'send_email', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Send an email to the customer when his/her order status has changed.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'checkbox', + 'name' => 'pdf_invoice', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Attach invoice PDF to email.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'checkbox', + 'name' => 'pdf_delivery', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Attach delivery slip PDF to email.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'checkbox', + 'name' => 'shipped', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Set the order as shipped.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'checkbox', + 'name' => 'paid', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Set the order as paid.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'checkbox', + 'name' => 'delivery', + 'values' => array( + 'query' => array( + array('id' => 'on', 'name' => $this->trans('Show delivery PDF.', array(), 'Admin.Shopparameters.Feature'), 'val' => '1'), + ), + 'id' => 'id', + 'name' => 'name', + ), + ), + array( + 'type' => 'select_template', + 'label' => $this->trans('Template', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'template', + 'lang' => true, + 'options' => array( + 'query' => $this->getTemplates(), + 'id' => 'id', + 'name' => 'name', + 'folder' => 'folder', + ), + 'hint' => array( + $this->trans('Only letters, numbers and underscores ("_") are allowed.', array(), 'Admin.Shopparameters.Help'), + $this->trans('Email template for both .html and .txt.', array(), 'Admin.Shopparameters.Help'), + ), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + if (Tools::isSubmit('updateorder_state') || Tools::isSubmit('addorder_state')) { + return $this->renderOrderStatusForm(); + } elseif (Tools::isSubmit('updateorder_return_state') || Tools::isSubmit('addorder_return_state')) { + return $this->renderOrderReturnsForm(); + } else { + return parent::renderForm(); + } + } + + protected function renderOrderStatusForm() + { + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_value = array( + 'logable_on' => $this->getFieldValue($obj, 'logable'), + 'invoice_on' => $this->getFieldValue($obj, 'invoice'), + 'hidden_on' => $this->getFieldValue($obj, 'hidden'), + 'send_email_on' => $this->getFieldValue($obj, 'send_email'), + 'shipped_on' => $this->getFieldValue($obj, 'shipped'), + 'paid_on' => $this->getFieldValue($obj, 'paid'), + 'delivery_on' => $this->getFieldValue($obj, 'delivery'), + 'pdf_delivery_on' => $this->getFieldValue($obj, 'pdf_delivery'), + 'pdf_invoice_on' => $this->getFieldValue($obj, 'pdf_invoice'), + ); + + if ($this->getFieldValue($obj, 'color') !== false) { + $this->fields_value['color'] = $this->getFieldValue($obj, 'color'); + } else { + $this->fields_value['color'] = '#ffffff'; + } + + return parent::renderForm(); + } + + protected function renderOrderReturnsForm() + { + $helper = $this->initOrderReturnsForm(); + $helper->show_cancel_button = true; + + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + if (!Validate::isCleanHtml($back)) { + die(Tools::displayError()); + } + + $helper->back_url = $back; + + $this->fields_form[0]['form'] = array( + 'tinymce' => true, + 'legend' => array( + 'title' => $this->trans('Return status', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-time', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Status name', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => array( + $this->trans('Order\'s return status name.', array(), 'Admin.Shopparameters.Help'), + $this->trans('Invalid characters: numbers and', array(), 'Admin.Shopparameters.Help') . ' !<>,;?=+()@#"�{}_$%:', + ), + ), + array( + 'type' => 'color', + 'label' => $this->trans('Color', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'color', + 'hint' => $this->trans('Status will be highlighted in this color. HTML colors only.', array(), 'Admin.Shopparameters.Help') . ' "lightblue", "#CC6600")', + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + return $helper->generateForm($this->fields_form); + } + + protected function getTemplates() + { + $default_path = '../mails/'; + // Mail templates can also be found in the theme folder + $theme_path = '../themes/' . $this->context->shop->theme->getName() . '/mails/'; + + $array = array(); + foreach (Language::getLanguages(false) as $language) { + $iso_code = $language['iso_code']; + + // If there is no folder for the given iso_code in /mails or in /themes/[theme_name]/mails, we bypass this language + if (!@filemtime(_PS_ADMIN_DIR_ . '/' . $default_path . $iso_code) && !@filemtime(_PS_ADMIN_DIR_ . '/' . $theme_path . $iso_code)) { + continue; + } + + $theme_templates_dir = _PS_ADMIN_DIR_ . '/' . $theme_path . $iso_code; + $theme_templates = is_dir($theme_templates_dir) ? scandir($theme_templates_dir, SCANDIR_SORT_NONE) : array(); + // We merge all available emails in one array + $templates = array_unique(array_merge(scandir(_PS_ADMIN_DIR_ . '/' . $default_path . $iso_code, SCANDIR_SORT_NONE), $theme_templates)); + foreach ($templates as $key => $template) { + if (!strncmp(strrev($template), 'lmth.', 5)) { + $search_result = array_search($template, $theme_templates); + $array[$iso_code][] = array( + 'id' => substr($template, 0, -5), + 'name' => substr($template, 0, -5), + 'folder' => ((!empty($search_result) ? $theme_path : $default_path)), + ); + } + } + } + + return $array; + } + + public function postProcess() + { + if (Tools::isSubmit($this->table . 'Orderby') || Tools::isSubmit($this->table . 'Orderway')) { + $this->filter = true; + } + + if (Tools::isSubmit('submitAddorder_return_state')) { + $id_order_return_state = Tools::getValue('id_order_return_state'); + + // Create Object OrderReturnState + $order_return_state = new OrderReturnState((int) $id_order_return_state); + + $order_return_state->color = Tools::getValue('color'); + $order_return_state->name = array(); + foreach (Language::getIDs(false) as $id_lang) { + $order_return_state->name[$id_lang] = Tools::getValue('name_' . $id_lang); + } + + // Update object + if (!$order_return_state->save()) { + $this->errors[] = $this->trans('An error has occurred: Can\'t save the current order\'s return status.', array(), 'Admin.Orderscustomers.Notification'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=4&token=' . $this->token); + } + } + + if (Tools::isSubmit('submitBulkdeleteorder_return_state')) { + $this->className = 'OrderReturnState'; + $this->table = 'order_return_state'; + $this->boxes = Tools::getValue('order_return_stateBox'); + parent::processBulkDelete(); + } + + if (Tools::isSubmit('deleteorder_return_state')) { + $id_order_return_state = Tools::getValue('id_order_return_state'); + + // Create Object OrderReturnState + $order_return_state = new OrderReturnState((int) $id_order_return_state); + + if (!$order_return_state->delete()) { + $this->errors[] = $this->trans('An error has occurred: Can\'t delete the current order\'s return status.', array(), 'Admin.Orderscustomers.Notification'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . $this->token); + } + } + + if (Tools::isSubmit('submitAdd' . $this->table)) { + $this->deleted = false; // Disabling saving historisation + $_POST['invoice'] = (int) Tools::getValue('invoice_on'); + $_POST['logable'] = (int) Tools::getValue('logable_on'); + $_POST['send_email'] = (int) Tools::getValue('send_email_on'); + $_POST['hidden'] = (int) Tools::getValue('hidden_on'); + $_POST['shipped'] = (int) Tools::getValue('shipped_on'); + $_POST['paid'] = (int) Tools::getValue('paid_on'); + $_POST['delivery'] = (int) Tools::getValue('delivery_on'); + $_POST['pdf_delivery'] = (int) Tools::getValue('pdf_delivery_on'); + $_POST['pdf_invoice'] = (int) Tools::getValue('pdf_invoice_on'); + if (!$_POST['send_email']) { + foreach (Language::getIDs(false) as $id_lang) { + $_POST['template_' . $id_lang] = ''; + } + } + + return parent::postProcess(); + } elseif (Tools::isSubmit('delete' . $this->table)) { + $order_state = new OrderState(Tools::getValue('id_order_state'), $this->context->language->id); + if (!$order_state->isRemovable()) { + $this->errors[] = $this->trans('For security reasons, you cannot delete default order statuses.', array(), 'Admin.Shopparameters.Notification'); + } else { + return parent::postProcess(); + } + } elseif (Tools::isSubmit('submitBulkdelete' . $this->table)) { + foreach (Tools::getValue($this->table . 'Box') as $selection) { + $order_state = new OrderState((int) $selection, $this->context->language->id); + if (!$order_state->isRemovable()) { + $this->errors[] = $this->trans('For security reasons, you cannot delete default order statuses.', array(), 'Admin.Shopparameters.Notification'); + break; + } + } + + if (!count($this->errors)) { + return parent::postProcess(); + } + } else { + return parent::postProcess(); + } + } + + protected function filterToField($key, $filter) + { + if ($this->table == 'order_state') { + $this->initOrderStatutsList(); + } elseif ($this->table == 'order_return_state') { + $this->initOrdersReturnsList(); + } + + return parent::filterToField($key, $filter); + } + + protected function afterImageUpload() + { + parent::afterImageUpload(); + + if (($id_order_state = (int) Tools::getValue('id_order_state')) && + isset($_FILES) && count($_FILES) && file_exists(_PS_ORDER_STATE_IMG_DIR_ . $id_order_state . '.gif')) { + $current_file = _PS_TMP_IMG_DIR_ . 'order_state_mini_' . $id_order_state . '_' . $this->context->shop->id . '.gif'; + + if (file_exists($current_file)) { + unlink($current_file); + } + } + + return true; + } + + public function ajaxProcessSendEmailOrderState() + { + $id_order_state = (int) Tools::getValue('id_order_state'); + + $sql = 'UPDATE ' . _DB_PREFIX_ . 'order_state SET `send_email`= NOT `send_email` WHERE id_order_state=' . $id_order_state; + $result = Db::getInstance()->execute($sql); + + if ($result) { + echo json_encode(array('success' => 1, 'text' => $this->trans('The status has been updated successfully.', array(), 'Admin.Notifications.Success'))); + } else { + echo json_encode(array('success' => 0, 'text' => $this->trans('An error occurred while updating the status.', array(), 'Admin.Notifications.Error'))); + } + } + + public function ajaxProcessDeliveryOrderState() + { + $id_order_state = (int) Tools::getValue('id_order_state'); + + $sql = 'UPDATE ' . _DB_PREFIX_ . 'order_state SET `delivery`= NOT `delivery` WHERE id_order_state=' . $id_order_state; + $result = Db::getInstance()->execute($sql); + + if ($result) { + echo json_encode(array('success' => 1, 'text' => $this->trans('The status has been updated successfully.', array(), 'Admin.Notifications.Success'))); + } else { + echo json_encode(array('success' => 0, 'text' => $this->trans('An error occurred while updating the status.', array(), 'Admin.Notifications.Error'))); + } + } + + public function ajaxProcessInvoiceOrderState() + { + $id_order_state = (int) Tools::getValue('id_order_state'); + + $sql = 'UPDATE ' . _DB_PREFIX_ . 'order_state SET `invoice`= NOT `invoice` WHERE id_order_state=' . $id_order_state; + $result = Db::getInstance()->execute($sql); + + if ($result) { + echo json_encode(array('success' => 1, 'text' => $this->trans('The status has been updated successfully.', array(), 'Admin.Notifications.Success'))); + } else { + echo json_encode(array('success' => 0, 'text' => $this->trans('An error occurred while updating the status.', array(), 'Admin.Notifications.Error'))); + } + } +} diff --git a/controllers/admin/AdminStoresController.php b/controllers/admin/AdminStoresController.php new file mode 100644 index 00000000..2310ba64 --- /dev/null +++ b/controllers/admin/AdminStoresController.php @@ -0,0 +1,594 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Store $object + */ +class AdminStoresControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'store'; + $this->className = 'Store'; + $this->lang = false; + $this->toolbar_scroll = false; + + parent::__construct(); + + if (!Tools::getValue('realedit')) { + $this->deleted = false; + } + + $this->fieldImageSettings = array( + 'name' => 'image', + 'dir' => 'st', + ); + + $this->fields_list = array( + 'id_store' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'sl!name'), + 'address1' => array('title' => $this->trans('Address', array(), 'Admin.Global'), 'filter_key' => 'sl!address1'), + 'city' => array('title' => $this->trans('City', array(), 'Admin.Global')), + 'postcode' => array('title' => $this->trans('Zip/postal code', array(), 'Admin.Global')), + 'state' => array('title' => $this->trans('State', array(), 'Admin.Global'), 'filter_key' => 'st!name'), + 'country' => array('title' => $this->trans('Country', array(), 'Admin.Global'), 'filter_key' => 'cl!name'), + 'phone' => array('title' => $this->trans('Phone', array(), 'Admin.Global')), + 'fax' => array('title' => $this->trans('Fax', array(), 'Admin.Global')), + 'active' => array('title' => $this->trans('Enabled', array(), 'Admin.Global'), 'align' => 'center', 'active' => 'status', 'type' => 'bool', 'orderby' => false), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->_buildOrderedFieldsShop($this->_getDefaultFieldsContent()); + } + + public function renderOptions() + { + // Set toolbar options + $this->display = 'options'; + $this->show_toolbar = true; + $this->toolbar_scroll = true; + $this->initToolbar(); + + return parent::renderOptions(); + } + + public function initToolbar() + { + parent::initToolbar(); + + if ($this->display == 'options') { + unset($this->toolbar_btn['new']); + } elseif ($this->display != 'add' && $this->display != 'edit') { + unset($this->toolbar_btn['save']); + } + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_store'] = array( + 'href' => self::$currentIndex . '&addstore&token=' . $this->token, + 'desc' => $this->trans('Add new store', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + // Set toolbar options + $this->display = null; + $this->initToolbar(); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = 'cl.`name` country, st.`name` state, sl.*'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl + ON (cl.`id_country` = a.`id_country` + AND cl.`id_lang` = ' . (int) $this->context->language->id . ') + LEFT JOIN `' . _DB_PREFIX_ . 'state` st + ON (st.`id_state` = a.`id_state`) + LEFT JOIN `' . _DB_PREFIX_ . 'store_lang` sl + ON (sl.`id_store` = a.`id_store` + AND sl.`id_lang` = ' . (int) $this->context->language->id . ') '; + + return parent::renderList(); + } + + public function renderForm() + { + if (!($obj = $this->loadObject(true))) { + return; + } + + $image = _PS_STORE_IMG_DIR_ . $obj->id . '.jpg'; + $image_url = ImageManager::thumbnail($image, $this->table . '_' . (int) $obj->id . '.' . $this->imageType, 350, + $this->imageType, true, true); + $image_size = file_exists($image) ? filesize($image) / 1000 : false; + + $tmp_addr = new Address(); + $res = $tmp_addr->getFieldsRequiredDatabase(); + $required_fields = array(); + foreach ($res as $row) { + $required_fields[(int) $row['id_required_field']] = $row['field_name']; + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Stores', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-home', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => false, + 'hint' => array( + $this->trans('Store name (e.g. City Center Mall Store).', array(), 'Admin.Shopparameters.Feature'), + $this->trans('Allowed characters: letters, spaces and %s', array(), 'Admin.Shopparameters.Feature'), + ), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Address', array(), 'Admin.Global'), + 'name' => 'address1', + 'lang' => true, + 'required' => true, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Address (2)', array(), 'Admin.Global'), + 'name' => 'address2', + 'lang' => true, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'name' => 'postcode', + 'required' => in_array('postcode', $required_fields), + ), + array( + 'type' => 'text', + 'label' => $this->trans('City', array(), 'Admin.Global'), + 'name' => 'city', + 'required' => true, + ), + array( + 'type' => 'select', + 'label' => $this->trans('Country', array(), 'Admin.Global'), + 'name' => 'id_country', + 'required' => true, + 'default_value' => (int) $this->context->country->id, + 'options' => array( + 'query' => Country::getCountries($this->context->language->id), + 'id' => 'id_country', + 'name' => 'name', + ), + ), + array( + 'type' => 'select', + 'label' => $this->trans('State', array(), 'Admin.Global'), + 'name' => 'id_state', + 'required' => true, + 'options' => array( + 'id' => 'id_state', + 'name' => 'name', + 'query' => null, + ), + ), + array( + 'type' => 'latitude', + 'label' => $this->trans('Latitude / Longitude', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'latitude', + 'required' => true, + 'maxlength' => 12, + 'hint' => $this->trans('Store coordinates (e.g. 45.265469/-47.226478).', array(), 'Admin.Shopparameters.Feature'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Phone', array(), 'Admin.Global'), + 'name' => 'phone', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Fax', array(), 'Admin.Global'), + 'name' => 'fax', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Email address', array(), 'Admin.Global'), + 'name' => 'email', + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Note', array(), 'Admin.Global'), + 'name' => 'note', + 'lang' => true, + 'cols' => 42, + 'rows' => 4, + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Active', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Whether or not to display this store.', array(), 'Admin.Shopparameters.Help'), + ), + array( + 'type' => 'file', + 'label' => $this->trans('Picture', array(), 'Admin.Shopparameters.Feature'), + 'name' => 'image', + 'display_image' => true, + 'image' => $image_url ? $image_url : false, + 'size' => $image_size, + 'hint' => $this->trans('Storefront picture.', array(), 'Admin.Shopparameters.Help'), + ), + ), + 'hours' => array( + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $days = array(); + $days[1] = $this->trans('Monday', array(), 'Admin.Shopparameters.Feature'); + $days[2] = $this->trans('Tuesday', array(), 'Admin.Shopparameters.Feature'); + $days[3] = $this->trans('Wednesday', array(), 'Admin.Shopparameters.Feature'); + $days[4] = $this->trans('Thursday', array(), 'Admin.Shopparameters.Feature'); + $days[5] = $this->trans('Friday', array(), 'Admin.Shopparameters.Feature'); + $days[6] = $this->trans('Saturday', array(), 'Admin.Shopparameters.Feature'); + $days[7] = $this->trans('Sunday', array(), 'Admin.Shopparameters.Feature'); + + $hours = array(); + + $hours_temp = ($this->getFieldValue($obj, 'hours')); + if (is_array($hours_temp) && !empty($hours_temp)) { + $langs = Language::getLanguages(false); + $hours_temp = array_map('json_decode', $hours_temp); + $hours = array_map( + array($this, 'adaptHoursFormat'), + $hours_temp + ); + $hours = (count($langs) > 1) ? $hours : $hours[reset($langs)['id_lang']]; + } + + $this->fields_value = array( + 'latitude' => $this->getFieldValue($obj, 'latitude') ? $this->getFieldValue($obj, 'latitude') : '', + 'longitude' => $this->getFieldValue($obj, 'longitude') ? $this->getFieldValue($obj, 'longitude') : '', + 'days' => $days, + 'hours' => $hours, + ); + + return parent::renderForm(); + } + + public function postProcess() + { + if (isset($_POST['submitAdd' . $this->table])) { + $langs = Language::getLanguages(false); + /* Cleaning fields */ + foreach ($_POST as $kp => $vp) { + if (!in_array($kp, array('checkBoxShopGroupAsso_store', 'checkBoxShopAsso_store', 'hours'))) { + $_POST[$kp] = trim($vp); + } + if ('hours' === $kp) { + foreach ($vp as $day => $value) { + $_POST['hours'][$day] = is_array($value) ? array_map('trim', $_POST['hours'][$day]) : trim($value); + } + } + } + + /* Rewrite latitude and longitude to 8 digits */ + $_POST['latitude'] = number_format((float) $_POST['latitude'], 8); + $_POST['longitude'] = number_format((float) $_POST['longitude'], 8); + + /* If the selected country does not contain states */ + $id_state = (int) Tools::getValue('id_state'); + $id_country = (int) Tools::getValue('id_country'); + $country = new Country((int) $id_country); + + if ($id_country && $country && !(int) $country->contains_states && $id_state) { + $this->errors[] = $this->trans('You\'ve selected a state for a country that does not contain states.', array(), 'Admin.Advparameters.Notification'); + } + + /* If the selected country contains states, then a state have to be selected */ + if ((int) $country->contains_states && !$id_state) { + $this->errors[] = $this->trans('An address located in a country containing states must have a state selected.', array(), 'Admin.Shopparameters.Notification'); + } + + $latitude = (float) Tools::getValue('latitude'); + $longitude = (float) Tools::getValue('longitude'); + + if (empty($latitude) || empty($longitude)) { + $this->errors[] = $this->trans('Latitude and longitude are required.', array(), 'Admin.Shopparameters.Notification'); + } + + $postcode = Tools::getValue('postcode'); + /* Check zip code format */ + if ($country->zip_code_format && !$country->checkZipCode($postcode)) { + $this->errors[] = $this->trans('Your Zip/postal code is incorrect.', array(), 'Admin.Notifications.Error') . '
' . $this->trans('It must be entered as follows:', array(), 'Admin.Notifications.Error') . ' ' . str_replace('C', $country->iso_code, str_replace('N', '0', str_replace('L', 'A', $country->zip_code_format))); + } elseif (empty($postcode) && $country->need_zip_code) { + $this->errors[] = $this->trans('A Zip/postal code is required.', array(), 'Admin.Notifications.Error'); + } elseif ($postcode && !Validate::isPostCode($postcode)) { + $this->errors[] = $this->trans('The Zip/postal code is invalid.', array(), 'Admin.Notifications.Error'); + } + /* Store hours */ + foreach ($langs as $lang) { + $hours = array(); + for ($i = 1; $i < 8; ++$i) { + if (1 < count($langs)) { + $hours[] = explode(' | ', $_POST['hours'][$i][$lang['id_lang']]); + unset($_POST['hours'][$i][$lang['id_lang']]); + } else { + $hours[] = explode(' | ', $_POST['hours'][$i]); + unset($_POST['hours'][$i]); + } + } + $encodedHours[$lang['id_lang']] = json_encode($hours); + } + $_POST['hours'] = (1 < count($langs)) ? $encodedHours : json_encode($hours); + } + + if (!count($this->errors)) { + parent::postProcess(); + } else { + $this->display = 'add'; + } + } + + protected function postImage($id) + { + $ret = parent::postImage($id); + $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI'); + + if (($id_store = (int) Tools::getValue('id_store')) && isset($_FILES) && count($_FILES) && file_exists(_PS_STORE_IMG_DIR_ . $id_store . '.jpg')) { + $images_types = ImageType::getImagesTypes('stores'); + foreach ($images_types as $image_type) { + ImageManager::resize(_PS_STORE_IMG_DIR_ . $id_store . '.jpg', + _PS_STORE_IMG_DIR_ . $id_store . '-' . stripslashes($image_type['name']) . '.jpg', + (int) $image_type['width'], (int) $image_type['height'] + ); + + if ($generate_hight_dpi_images) { + ImageManager::resize(_PS_STORE_IMG_DIR_ . $id_store . '.jpg', + _PS_STORE_IMG_DIR_ . $id_store . '-' . stripslashes($image_type['name']) . '2x.jpg', + (int) $image_type['width'] * 2, (int) $image_type['height'] * 2 + ); + } + } + } + + return $ret; + } + + protected function _getDefaultFieldsContent() + { + $this->context = Context::getContext(); + $countryList = array(); + $countryList[] = array('id' => '0', 'name' => $this->trans('Choose your country', array(), 'Admin.Shopparameters.Feature')); + foreach (Country::getCountries($this->context->language->id) as $country) { + $countryList[] = array('id' => $country['id_country'], 'name' => $country['name']); + } + $stateList = array(); + $stateList[] = array('id' => '0', 'name' => $this->trans('Choose your state (if applicable)', array(), 'Admin.Shopparameters.Feature')); + foreach (State::getStates($this->context->language->id) as $state) { + $stateList[] = array('id' => $state['id_state'], 'name' => $state['name']); + } + + $formFields = array( + 'PS_SHOP_NAME' => array( + 'title' => $this->trans('Shop name', array(), 'Admin.Shopparameters.Feature'), + 'hint' => $this->trans('Displayed in emails and page titles.', array(), 'Admin.Shopparameters.Feature'), + 'validation' => 'isGenericName', + 'required' => true, + 'type' => 'text', + 'no_escape' => true, + ), + 'PS_SHOP_EMAIL' => array('title' => $this->trans('Shop email', array(), 'Admin.Shopparameters.Feature'), + 'hint' => $this->trans('Displayed in emails sent to customers.', array(), 'Admin.Shopparameters.Help'), + 'validation' => 'isEmail', + 'required' => true, + 'type' => 'text', + ), + 'PS_SHOP_DETAILS' => array( + 'title' => $this->trans('Registration number', array(), 'Admin.Shopparameters.Feature'), + 'hint' => $this->trans('Shop registration information (e.g. SIRET or RCS).', array(), 'Admin.Shopparameters.Help'), + 'validation' => 'isGenericName', + 'type' => 'textarea', + 'cols' => 30, + 'rows' => 5, + ), + 'PS_SHOP_ADDR1' => array( + 'title' => $this->trans('Shop address line 1', array(), 'Admin.Shopparameters.Feature'), + 'validation' => 'isAddress', + 'type' => 'text', + ), + 'PS_SHOP_ADDR2' => array( + 'title' => $this->trans('Shop address line 2', array(), 'Admin.Shopparameters.Feature'), + 'validation' => 'isAddress', + 'type' => 'text', + ), + 'PS_SHOP_CODE' => array( + 'title' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'validation' => 'isGenericName', + 'type' => 'text', + ), + 'PS_SHOP_CITY' => array( + 'title' => $this->trans('City', array(), 'Admin.Global'), + 'validation' => 'isGenericName', + 'type' => 'text', + ), + 'PS_SHOP_COUNTRY_ID' => array( + 'title' => $this->trans('Country', array(), 'Admin.Global'), + 'validation' => 'isInt', + 'type' => 'select', + 'list' => $countryList, + 'identifier' => 'id', + 'cast' => 'intval', + 'defaultValue' => (int) $this->context->country->id, + ), + 'PS_SHOP_STATE_ID' => array( + 'title' => $this->trans('State', array(), 'Admin.Global'), + 'validation' => 'isInt', + 'type' => 'select', + 'list' => $stateList, + 'identifier' => 'id', + 'cast' => 'intval', + ), + 'PS_SHOP_PHONE' => array( + 'title' => $this->trans('Phone', array(), 'Admin.Global'), + 'validation' => 'isGenericName', + 'type' => 'text', + ), + 'PS_SHOP_FAX' => array( + 'title' => $this->trans('Fax', array(), 'Admin.Global'), + 'validation' => 'isGenericName', + 'type' => 'text', + ), + ); + + return $formFields; + } + + protected function _buildOrderedFieldsShop($formFields) + { + // You cannot do that, because the fields must be sorted for the country you've selected. + // Simple example: the current country is France, where we don't display the state. You choose "US" as a country in the form. The state is not dsplayed at the right place... + + // $associatedOrderKey = array( + // 'PS_SHOP_NAME' => 'company', + // 'PS_SHOP_ADDR1' => 'address1', + // 'PS_SHOP_ADDR2' => 'address2', + // 'PS_SHOP_CITY' => 'city', + // 'PS_SHOP_STATE_ID' => 'State:name', + // 'PS_SHOP_CODE' => 'postcode', + // 'PS_SHOP_COUNTRY_ID' => 'Country:name', + // 'PS_SHOP_PHONE' => 'phone'); + // $fields = array(); + // $orderedFields = AddressFormat::getOrderedAddressFields(Configuration::get('PS_SHOP_COUNTRY_ID'), false, true); + // foreach ($orderedFields as $lineFields) + // if (($patterns = explode(' ', $lineFields))) + // foreach ($patterns as $pattern) + // if (($key = array_search($pattern, $associatedOrderKey))) + // $fields[$key] = $formFields[$key]; + // foreach ($formFields as $key => $value) + // if (!isset($fields[$key])) + // $fields[$key] = $formFields[$key]; + + $fields = $formFields; + $this->fields_options['contact'] = array( + 'title' => $this->trans('Contact details', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-user', + 'fields' => $fields, + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ); + } + + public function beforeUpdateOptions() + { + if (isset($_POST['PS_SHOP_STATE_ID']) && $_POST['PS_SHOP_STATE_ID'] != '0') { + $sql = 'SELECT `active` FROM `' . _DB_PREFIX_ . 'state` + WHERE `id_country` = ' . (int) Tools::getValue('PS_SHOP_COUNTRY_ID') . ' + AND `id_state` = ' . (int) Tools::getValue('PS_SHOP_STATE_ID'); + $isStateOk = Db::getInstance()->getValue($sql); + if ($isStateOk != 1) { + $this->errors[] = $this->trans('The specified state is not located in this country.', array(), 'Admin.Shopparameters.Notification'); + } + } + } + + public function updateOptionPsShopCountryId($value) + { + if (!$this->errors && $value) { + $country = new Country($value, $this->context->language->id); + if ($country->id) { + Configuration::updateValue('PS_SHOP_COUNTRY_ID', $value); + Configuration::updateValue('PS_SHOP_COUNTRY', pSQL($country->name)); + } + } + } + + public function updateOptionPsShopStateId($value) + { + if (!$this->errors && $value) { + $state = new State($value); + if ($state->id) { + Configuration::updateValue('PS_SHOP_STATE_ID', $value); + Configuration::updateValue('PS_SHOP_STATE', pSQL($state->name)); + } + } + } + + /** + * Adapt the format of hours. + * + * @param array $value + * + * @return array + */ + private function adaptHoursFormat($value) + { + $separator = array_fill(0, count($value), ' | '); + + return array_map('implode', $value, $separator); + } +} diff --git a/controllers/admin/AdminSuppliersController.php b/controllers/admin/AdminSuppliersController.php new file mode 100644 index 00000000..7ccd0174 --- /dev/null +++ b/controllers/admin/AdminSuppliersController.php @@ -0,0 +1,557 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Supplier $object + */ +class AdminSuppliersControllerCore extends AdminController +{ + public $bootstrap = true; + + public function __construct() + { + $this->table = 'supplier'; + $this->className = 'Supplier'; + + parent::__construct(); + + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->allow_export = true; + + $this->_defaultOrderBy = 'name'; + $this->_defaultOrderWay = 'ASC'; + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + ), + ); + + $this->_select = 'COUNT(DISTINCT ps.`id_product`) AS products'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'product_supplier` ps ON (a.`id_supplier` = ps.`id_supplier`)'; + $this->_group = 'GROUP BY a.`id_supplier`'; + + $this->fieldImageSettings = array('name' => 'logo', 'dir' => 'su'); + + $this->fields_list = array( + 'id_supplier' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'logo' => array('title' => $this->trans('Logo', array(), 'Admin.Global'), 'align' => 'center', 'image' => 'su', 'orderby' => false, 'search' => false), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global')), + 'products' => array('title' => $this->trans('Number of products', array(), 'Admin.Catalog.Feature'), 'align' => 'right', 'filter_type' => 'int', 'tmpTableFilter' => true), + 'active' => array('title' => $this->trans('Enabled', array(), 'Admin.Global'), 'align' => 'center', 'active' => 'status', 'type' => 'bool', 'orderby' => false, 'class' => 'fixed-width-xs'), + ); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryUi('ui.widget'); + $this->addJqueryPlugin('tagify'); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_supplier'] = array( + 'href' => self::$currentIndex . '&addsupplier&token=' . $this->token, + 'desc' => $this->trans('Add new supplier', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + // loads current warehouse + if (!($obj = $this->loadObject(true))) { + return; + } + + $image = _PS_SUPP_IMG_DIR_ . $obj->id . '.jpg'; + $image_url = ImageManager::thumbnail($image, $this->table . '_' . (int) $obj->id . '.' . $this->imageType, 350, + $this->imageType, true, true); + $image_size = file_exists($image) ? filesize($image) / 1000 : false; + + $tmp_addr = new Address(); + $res = $tmp_addr->getFieldsRequiredDatabase(); + $required_fields = array(); + foreach ($res as $row) { + $required_fields[(int) $row['id_required_field']] = $row['field_name']; + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Suppliers', array(), 'Admin.Global'), + 'icon' => 'icon-truck', + ), + 'input' => array( + array( + 'type' => 'hidden', + 'name' => 'id_address', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'col' => 4, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + (in_array('company', $required_fields) ? + array( + 'type' => 'text', + 'label' => $this->trans('Company', array(), 'Admin.Global'), + 'name' => 'company', + 'display' => in_array('company', $required_fields), + 'required' => in_array('company', $required_fields), + 'maxlength' => 16, + 'col' => 4, + 'hint' => $this->trans('Company name for this supplier', array(), 'Admin.Catalog.Help'), + ) + : null + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + 'lang' => true, + 'hint' => array( + $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + $this->trans('Will appear in the list of suppliers.', array(), 'Admin.Catalog.Help'), + ), + 'autoload_rte' => 'rte', //Enable TinyMCE editor for short description + ), + array( + 'type' => 'text', + 'label' => $this->trans('Phone', array(), 'Admin.Global'), + 'name' => 'phone', + 'required' => in_array('phone', $required_fields), + 'maxlength' => 16, + 'col' => 4, + 'hint' => $this->trans('Phone number for this supplier', array(), 'Admin.Catalog.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Mobile phone', array(), 'Admin.Global'), + 'name' => 'phone_mobile', + 'required' => in_array('phone_mobile', $required_fields), + 'maxlength' => 16, + 'col' => 4, + 'hint' => $this->trans('Mobile phone number for this supplier.', array(), 'Admin.Catalog.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Address', array(), 'Admin.Global'), + 'name' => 'address', + 'maxlength' => 128, + 'col' => 6, + 'required' => true, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Address (2)', array(), 'Admin.Global'), + 'name' => 'address2', + 'required' => in_array('address2', $required_fields), + 'col' => 6, + 'maxlength' => 128, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'name' => 'postcode', + 'required' => in_array('postcode', $required_fields), + 'maxlength' => 12, + 'col' => 2, + ), + array( + 'type' => 'text', + 'label' => $this->trans('City', array(), 'Admin.Global'), + 'name' => 'city', + 'maxlength' => 32, + 'col' => 4, + 'required' => true, + ), + array( + 'type' => 'select', + 'label' => $this->trans('Country', array(), 'Admin.Global'), + 'name' => 'id_country', + 'required' => true, + 'col' => 4, + 'default_value' => (int) $this->context->country->id, + 'options' => array( + 'query' => Country::getCountries($this->context->language->id, false), + 'id' => 'id_country', + 'name' => 'name', + ), + ), + array( + 'type' => 'select', + 'label' => $this->trans('State', array(), 'Admin.Global'), + 'name' => 'id_state', + 'col' => 4, + 'options' => array( + 'id' => 'id_state', + 'query' => array(), + 'name' => 'name', + ), + ), + array( + 'type' => 'file', + 'label' => $this->trans('Logo', array(), 'Admin.Global'), + 'name' => 'logo', + 'display_image' => true, + 'image' => $image_url ? $image_url : false, + 'size' => $image_size, + 'hint' => $this->trans('Upload a supplier logo from your computer.', array(), 'Admin.Catalog.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta title', array(), 'Admin.Global'), + 'name' => 'meta_title', + 'lang' => true, + 'col' => 4, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Meta description', array(), 'Admin.Global'), + 'name' => 'meta_description', + 'lang' => true, + 'col' => 6, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'tags', + 'label' => $this->trans('Meta keywords', array(), 'Admin.Global'), + 'name' => 'meta_keywords', + 'lang' => true, + 'col' => 6, + 'hint' => array( + $this->trans('To add "tags" click in the field, write something and then press "Enter".', array(), 'Admin.Catalog.Help'), + $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Enable', array(), 'Admin.Actions'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + // loads current address for this supplier - if possible + $address = null; + if (isset($obj->id)) { + $id_address = Address::getAddressIdBySupplierId($obj->id); + + if ($id_address > 0) { + $address = new Address((int) $id_address); + } + } + + // force specific fields values (address) + if ($address != null) { + $this->fields_value = array( + 'id_address' => $address->id, + 'phone' => $address->phone, + 'phone_mobile' => $address->phone_mobile, + 'address' => $address->address1, + 'address2' => $address->address2, + 'postcode' => $address->postcode, + 'city' => $address->city, + 'id_country' => $address->id_country, + 'id_state' => $address->id_state, + ); + } else { + $this->fields_value = array( + 'id_address' => 0, + 'id_country' => Configuration::get('PS_COUNTRY_DEFAULT'), + ); + } + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + return parent::renderForm(); + } + + /** + * AdminController::initToolbar() override. + * + * @see AdminController::initToolbar() + */ + public function initToolbar() + { + parent::initToolbar(); + $this->addPageHeaderToolBarModulesListButton(); + + if (empty($this->display) && $this->can_import) { + $this->toolbar_btn['import'] = array( + 'href' => $this->context->link->getAdminLink('AdminImport', true) . '&import_type=suppliers', + 'desc' => $this->trans('Import', array(), 'Admin.Actions'), + ); + } + } + + public function renderView() + { + $this->initTabModuleList(); + $this->toolbar_title = $this->object->name; + $products = $this->object->getProductsLite($this->context->language->id); + $total_product = count($products); + + for ($i = 0; $i < $total_product; ++$i) { + $products[$i] = new Product($products[$i]['id_product'], false, $this->context->language->id); + $products[$i]->loadStockData(); + // Build attributes combinations + $combinations = $products[$i]->getAttributeCombinations($this->context->language->id); + foreach ($combinations as $combination) { + $comb_infos = Supplier::getProductInformationsBySupplier($this->object->id, + $products[$i]->id, + $combination['id_product_attribute']); + $comb_array[$combination['id_product_attribute']]['product_supplier_reference'] = $comb_infos['product_supplier_reference']; + $comb_array[$combination['id_product_attribute']]['product_supplier_price_te'] = Tools::displayPrice($comb_infos['product_supplier_price_te'], new Currency($comb_infos['id_currency'])); + $comb_array[$combination['id_product_attribute']]['reference'] = $combination['reference']; + $comb_array[$combination['id_product_attribute']]['ean13'] = $combination['ean13']; + $comb_array[$combination['id_product_attribute']]['upc'] = $combination['upc']; + $comb_array[$combination['id_product_attribute']]['quantity'] = $combination['quantity']; + $comb_array[$combination['id_product_attribute']]['attributes'][] = array( + $combination['group_name'], + $combination['attribute_name'], + $combination['id_attribute'], + ); + } + + if (isset($comb_array)) { + foreach ($comb_array as $key => $product_attribute) { + $list = ''; + foreach ($product_attribute['attributes'] as $attribute) { + $list .= $attribute[0] . ' - ' . $attribute[1] . ', '; + } + $comb_array[$key]['attributes'] = rtrim($list, ', '); + } + isset($comb_array) ? $products[$i]->combination = $comb_array : ''; + unset($comb_array); + } else { + $product_infos = Supplier::getProductInformationsBySupplier($this->object->id, + $products[$i]->id, + 0); + $products[$i]->product_supplier_reference = $product_infos['product_supplier_reference']; + $products[$i]->product_supplier_price_te = Tools::displayPrice($product_infos['product_supplier_price_te'], new Currency($product_infos['id_currency'])); + } + } + + $this->tpl_view_vars = array( + 'supplier' => $this->object, + 'products' => $products, + 'stock_management' => Configuration::get('PS_STOCK_MANAGEMENT'), + 'shopContext' => Shop::getContext(), + ); + + return parent::renderView(); + } + + protected function afterImageUpload() + { + $return = true; + $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI'); + + /* Generate image with differents size */ + if (($id_supplier = (int) Tools::getValue('id_supplier')) && + isset($_FILES) && count($_FILES) && file_exists(_PS_SUPP_IMG_DIR_ . $id_supplier . '.jpg')) { + $images_types = ImageType::getImagesTypes('suppliers'); + foreach ($images_types as $image_type) { + $file = _PS_SUPP_IMG_DIR_ . $id_supplier . '.jpg'; + if (!ImageManager::resize($file, _PS_SUPP_IMG_DIR_ . $id_supplier . '-' . stripslashes($image_type['name']) . '.jpg', (int) $image_type['width'], (int) $image_type['height'])) { + $return = false; + } + + if ($generate_hight_dpi_images) { + if (!ImageManager::resize($file, _PS_SUPP_IMG_DIR_ . $id_supplier . '-' . stripslashes($image_type['name']) . '2x.jpg', (int) $image_type['width'] * 2, (int) $image_type['height'] * 2)) { + $return = false; + } + } + } + + $current_logo_file = _PS_TMP_IMG_DIR_ . 'supplier_mini_' . $id_supplier . '_' . $this->context->shop->id . '.jpg'; + + if (file_exists($current_logo_file)) { + unlink($current_logo_file); + } + } + + return $return; + } + + /** + * AdminController::postProcess() override. + * + * @see AdminController::postProcess() + */ + public function postProcess() + { + // checks access + if (Tools::isSubmit('submitAdd' . $this->table) && !($this->access('add'))) { + $this->errors[] = $this->trans('You do not have permission to add suppliers.', array(), 'Admin.Catalog.Notification'); + + return parent::postProcess(); + } + + if (Tools::isSubmit('submitAdd' . $this->table)) { + if (Tools::isSubmit('id_supplier') && !($obj = $this->loadObject(true))) { + return; + } + + // updates/creates address if it does not exist + if (Tools::isSubmit('id_address') && (int) Tools::getValue('id_address') > 0) { + $address = new Address((int) Tools::getValue('id_address')); + } // updates address + else { + $address = new Address(); + } // creates address + + $address->alias = Tools::getValue('name', null); + $address->lastname = 'supplier'; // skip problem with numeric characters in supplier name + $address->firstname = 'supplier'; // skip problem with numeric characters in supplier name + $address->address1 = Tools::getValue('address', null); + $address->address2 = Tools::getValue('address2', null); + $address->postcode = Tools::getValue('postcode', null); + $address->phone = Tools::getValue('phone', null); + $address->phone_mobile = Tools::getValue('phone_mobile', null); + $address->id_country = Tools::getValue('id_country', null); + $address->id_state = Tools::getValue('id_state', null); + $address->city = Tools::getValue('city', null); + + $validation = $address->validateController(); + + // checks address validity + if (count($validation) > 0) { + foreach ($validation as $item) { + $this->errors[] = $item; + } + $this->errors[] = $this->trans('The address is not correct. Please make sure all of the required fields are completed.', array(), 'Admin.Catalog.Notification'); + } else { + if (Tools::isSubmit('id_address') && Tools::getValue('id_address') > 0) { + $address->update(); + } else { + $address->save(); + $_POST['id_address'] = $address->id; + } + } + + return parent::postProcess(); + } elseif (Tools::isSubmit('delete' . $this->table)) { + if (!($obj = $this->loadObject(true))) { + return; + } elseif (SupplyOrder::supplierHasPendingOrders($obj->id)) { + $this->errors[] = $this->trans('It is not possible to delete a supplier if there are pending supplier orders.', array(), 'Admin.Catalog.Notification'); + } else { + //delete all product_supplier linked to this supplier + Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'product_supplier` WHERE `id_supplier`=' . (int) $obj->id); + + $id_address = Address::getAddressIdBySupplierId($obj->id); + $address = new Address($id_address); + if (Validate::isLoadedObject($address)) { + $address->deleted = 1; + $address->save(); + } + + return parent::postProcess(); + } + } else { + return parent::postProcess(); + } + } + + /** + * @see AdminController::afterAdd() + * + * @param Supplier $object + * + * @return bool + */ + protected function afterAdd($object) + { + $id_address = (int) $_POST['id_address']; + $address = new Address($id_address); + if (Validate::isLoadedObject($address)) { + $address->id_supplier = $object->id; + $address->save(); + } + + return true; + } + + /** + * @see AdminController::afterUpdate() + * + * @param Supplier $object + * + * @return bool + */ + protected function afterUpdate($object) + { + $id_address = (int) $_POST['id_address']; + $address = new Address($id_address); + if (Validate::isLoadedObject($address)) { + if ($address->id_supplier != $object->id) { + $address->id_supplier = $object->id; + $address->save(); + } + } + + return true; + } +} diff --git a/controllers/admin/AdminTabsController.php b/controllers/admin/AdminTabsController.php new file mode 100644 index 00000000..5373b26b --- /dev/null +++ b/controllers/admin/AdminTabsController.php @@ -0,0 +1,382 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Tab $object + */ +class AdminTabsControllerCore extends AdminController +{ + protected $position_identifier = 'id_tab'; + + public function __construct() + { + $this->bootstrap = true; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->table = 'tab'; + $this->list_id = 'tab'; + $this->className = 'Tab'; + $this->lang = true; + + parent::__construct(); + + $this->fieldImageSettings = array( + 'name' => 'icon', + 'dir' => 't', + ); + $this->imageType = 'gif'; + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->l('Delete selected'), + 'confirm' => $this->l('Delete selected items?'), + 'icon' => 'icon-trash', + ), + ); + $this->fields_list = array( + 'id_tab' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + ), + 'class_name' => array( + 'title' => $this->l('Class'), + ), + 'module' => array( + 'title' => $this->l('Module'), + ), + 'active' => array( + 'title' => $this->trans('Enabled', array(), 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + ), + 'position' => array( + 'title' => $this->l('Position'), + 'filter_key' => 'a!position', + 'position' => 'position', + 'align' => 'center', + 'class' => 'fixed-width-md', + ), + ); + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_title = $this->l('Menus'); + + if ($this->display == 'details') { + $this->page_header_toolbar_btn['back_to_list'] = array( + 'href' => Context::getContext()->link->getAdminLink('AdminTabs'), + 'desc' => $this->l('Back to list', null, null, false), + 'icon' => 'process-icon-back', + ); + } elseif (empty($this->display)) { + $this->page_header_toolbar_btn['new_menu'] = array( + 'href' => self::$currentIndex . '&addtab&token=' . $this->token, + 'desc' => $this->l('Add new menu', null, null, false), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $tabs = Tab::getTabs($this->context->language->id, 0); + // If editing, we clean itself + if (Tools::isSubmit('id_tab')) { + foreach ($tabs as $key => $tab) { + if ($tab['id_tab'] == Tools::getValue('id_tab')) { + unset($tabs[$key]); + } + } + } + + // added category "Home" in var $tabs + $tab_zero = array( + 'id_tab' => 0, + 'name' => $this->l('Home'), + ); + array_unshift($tabs, $tab_zero); + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->l('Menus'), + 'icon' => 'icon-list-ul', + ), + 'input' => array( + array( + 'type' => 'hidden', + 'name' => 'position', + 'required' => false, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->l('Invalid characters:') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->l('Class'), + 'name' => 'class_name', + 'required' => true, + ), + array( + 'type' => 'text', + 'label' => $this->l('Module'), + 'name' => 'module', + ), + array( + 'type' => 'switch', + 'label' => $this->l('Status'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->l('Show or hide menu.'), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + $display_parent = true; + if (Validate::isLoadedObject($this->object) && !class_exists($this->object->class_name . 'Controller')) { + $display_parent = false; + } + + if ($display_parent) { + $this->fields_form['input'][] = array( + 'type' => 'select', + 'label' => $this->l('Parent'), + 'name' => 'id_parent', + 'options' => array( + 'query' => $tabs, + 'id' => 'id_tab', + 'name' => 'name', + ), + ); + } + + return parent::renderForm(); + } + + /** + * AdminController::renderList() override. + * + * @see AdminController::renderList() + */ + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('details'); + $this->addRowAction('delete'); + + $this->_where = 'AND a.`id_parent` = 0'; + $this->_orderBy = 'position'; + + return parent::renderList(); + } + + public function initProcess() + { + if (Tools::getIsset('details' . $this->table)) { + $this->list_id = 'details'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + } else { + $this->list_id = 'tab'; + } + + return parent::initProcess(); + } + + public function renderDetails() + { + if (($id = Tools::getValue('id_tab'))) { + $this->lang = false; + $this->list_id = 'details'; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->toolbar_btn = array(); + + /** @var Tab $tab */ + $tab = $this->loadObject($id); + $this->toolbar_title = $tab->name[$this->context->employee->id_lang]; + + $this->_select = 'b.*'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'tab_lang` b ON (b.`id_tab` = a.`id_tab` AND b.`id_lang` = ' . + (int) $this->context->language->id . ')'; + $this->_where = 'AND a.`id_parent` = ' . (int) $id; + $this->_orderBy = 'position'; + $this->_use_found_rows = false; + + self::$currentIndex = self::$currentIndex . '&details' . $this->table; + $this->processFilter(); + + return parent::renderList(); + } + } + + public function postProcess() + { + /* PrestaShop demo mode */ + if (_PS_MODE_DEMO_) { + $this->errors[] = $this->trans('This functionality has been disabled.', array(), 'Admin.Notifications.Error'); + + return; + } + /* PrestaShop demo mode*/ + + if (($id_tab = (int) Tools::getValue('id_tab')) && ($direction = Tools::getValue('move')) && Validate::isLoadedObject($tab = new Tab($id_tab))) { + if ($tab->move($direction)) { + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token); + } + } elseif (Tools::getValue('position') && !Tools::isSubmit('submitAdd' . $this->table)) { + if ($this->access('edit') !== '1') { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } elseif (!Validate::isLoadedObject($object = new Tab((int) Tools::getValue($this->identifier)))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', array(), 'Admin.Notifications.Error') . + ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + if (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', array(), 'Admin.Notifications.Error'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=5&token=' . Tools::getAdminTokenLite('AdminTabs')); + } + } elseif (Tools::isSubmit('submitAdd' . $this->table) && Tools::getValue('id_tab') === Tools::getValue('id_parent')) { + $this->errors[] = $this->trans('You can\'t put this menu inside itself. ', array(), 'Admin.Advparameters.Notification'); + } elseif (Tools::isSubmit('submitAdd' . $this->table) && $id_parent = (int) Tools::getValue('id_parent')) { + $this->redirect_after = self::$currentIndex . '&id_' . $this->table . '=' . $id_parent . '&details' . $this->table . '&conf=4&token=' . $this->token; + } elseif (isset($_GET['details' . $this->table]) && is_array($this->bulk_actions)) { + $submit_bulk_actions = array_merge(array( + 'enableSelection' => array( + 'text' => $this->l('Enable selection'), + 'icon' => 'icon-power-off text-success', + ), + 'disableSelection' => array( + 'text' => $this->l('Disable selection'), + 'icon' => 'icon-power-off text-danger', + ), + ), $this->bulk_actions); + foreach ($submit_bulk_actions as $bulk_action => $params) { + if (Tools::isSubmit('submitBulk' . $bulk_action . $this->table) || Tools::isSubmit('submitBulk' . $bulk_action)) { + if ($this->access('edit')) { + $this->action = 'bulk' . $bulk_action; + $this->boxes = Tools::getValue($this->list_id . 'Box'); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + break; + } elseif (Tools::isSubmit('submitBulk')) { + if ($this->access('edit')) { + $this->action = 'bulk' . Tools::getValue('select_submitBulk'); + $this->boxes = Tools::getValue($this->list_id . 'Box'); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + break; + } + } + } else { + // Temporary add the position depend of the selection of the parent category + if (!Tools::isSubmit('id_tab')) { // @todo Review + $_POST['position'] = Tab::getNbTabs(Tools::getValue('id_parent')); + } + } + + if (!count($this->errors)) { + parent::postProcess(); + } + } + + protected function afterImageUpload() + { + /** @var Tab $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + @rename(_PS_IMG_DIR_ . 't/' . $obj->id . '.gif', _PS_IMG_DIR_ . 't/' . $obj->class_name . '.gif'); + } + + public function ajaxProcessUpdatePositions() + { + $way = (int) (Tools::getValue('way')); + $id_tab = (int) (Tools::getValue('id')); + $positions = Tools::getValue('tab'); + + // when changing positions in a tab sub-list, the first array value is empty and needs to be removed + if (!$positions[0]) { + unset($positions[0]); + // reset indexation from 0 + $positions = array_merge($positions); + } + + foreach ($positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_tab) { + if ($tab = new Tab((int) $pos[2])) { + if (isset($position) && $tab->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for tab ' . (int) $pos[1] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update tab ' . (int) $id_tab . ' to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "This tab (' . (int) $id_tab . ') can t be loaded"}'; + } + + break; + } + } + } +} diff --git a/controllers/admin/AdminTagsController.php b/controllers/admin/AdminTagsController.php new file mode 100644 index 00000000..31cf3aed --- /dev/null +++ b/controllers/admin/AdminTagsController.php @@ -0,0 +1,168 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Tag $object + */ +class AdminTagsControllerCore extends AdminController +{ + public $bootstrap = true; + + public function __construct() + { + $this->table = 'tag'; + $this->className = 'Tag'; + + parent::__construct(); + + $this->fields_list = array( + 'id_tag' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'lang' => array( + 'title' => $this->trans('Language', array(), 'Admin.Global'), + 'filter_key' => 'l!name', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + 'filter_key' => 'a!name', + ), + 'products' => array( + 'title' => $this->trans('Products', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + 'havingFilter' => true, + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + ), + ); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_tag'] = array( + 'href' => self::$currentIndex . '&addtag&token=' . $this->token, + 'desc' => $this->trans('Add new tag', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = 'l.name as lang, COUNT(pt.id_product) as products'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'product_tag` pt + ON (a.`id_tag` = pt.`id_tag`) + LEFT JOIN `' . _DB_PREFIX_ . 'lang` l + ON (l.`id_lang` = a.`id_lang`)'; + $this->_group = 'GROUP BY a.name, a.id_lang'; + + return parent::renderList(); + } + + public function postProcess() + { + if ($this->access('edit') && Tools::getValue('submitAdd' . $this->table)) { + if (($id = (int) Tools::getValue($this->identifier)) && ($obj = new $this->className($id)) && Validate::isLoadedObject($obj)) { + /** @var Tag $obj */ + $previous_products = $obj->getProducts(); + $removed_products = array(); + + foreach ($previous_products as $product) { + if (!in_array($product['id_product'], $_POST['products'])) { + $removed_products[] = $product['id_product']; + } + } + + if (Configuration::get('PS_SEARCH_INDEXATION')) { + Search::removeProductsSearchIndex($removed_products); + } + + $obj->setProducts($_POST['products']); + } + } + + return parent::postProcess(); + } + + public function renderForm() + { + /** @var Tag $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Tag', array(), 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-tag', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + ), + array( + 'type' => 'select', + 'label' => $this->trans('Language', array(), 'Admin.Global'), + 'name' => 'id_lang', + 'required' => true, + 'options' => array( + 'query' => Language::getLanguages(false), + 'id' => 'id_lang', + 'name' => 'name', + ), + ), + ), + 'selects' => array( + 'products' => $obj->getProducts(true), + 'products_unselected' => $obj->getProducts(false), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + return parent::renderForm(); + } +} diff --git a/controllers/admin/AdminTaxRulesGroupController.php b/controllers/admin/AdminTaxRulesGroupController.php new file mode 100644 index 00000000..3362d16c --- /dev/null +++ b/controllers/admin/AdminTaxRulesGroupController.php @@ -0,0 +1,577 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property TaxRulesGroup $object + */ +class AdminTaxRulesGroupControllerCore extends AdminController +{ + public $tax_rule; + public $selected_countries = array(); + public $selected_states = array(); + public $errors_tax_rule; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'tax_rules_group'; + $this->className = 'TaxRulesGroup'; + $this->lang = false; + + parent::__construct(); + + $this->fields_list = array( + 'id_tax_rules_group' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + ), + 'active' => array( + 'title' => $this->trans('Enabled', array(), 'Admin.Global'), + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + 'align' => 'center', + 'class' => 'fixed-width-sm', + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->_where .= ' AND a.deleted = 0'; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_tax_rules_group'] = array( + 'href' => self::$currentIndex . '&addtax_rules_group&token=' . $this->token, + 'desc' => $this->trans('Add new tax rules group', array(), 'Admin.International.Feature'), + 'icon' => 'process-icon-new', + ); + } + if ($this->display === 'edit') { + $this->page_header_toolbar_btn['new'] = array( + 'href' => '#', + 'desc' => $this->trans('Add a new tax rule', array(), 'Admin.International.Feature'), + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + return parent::renderList(); + } + + public function initRulesList($id_group) + { + $this->table = 'tax_rule'; + $this->list_id = 'tax_rule'; + $this->identifier = 'id_tax_rule'; + $this->className = 'TaxRule'; + $this->lang = false; + $this->list_simple_header = false; + $this->toolbar_btn = null; + $this->list_no_link = true; + + $this->bulk_actions = array( + 'delete' => array('text' => $this->trans('Delete selected', array(), 'Admin.Actions'), 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), 'icon' => 'icon-trash'), + ); + + $this->fields_list = array( + 'country_name' => array( + 'title' => $this->trans('Country', array(), 'Admin.Global'), + ), + 'state_name' => array( + 'title' => $this->trans('State', array(), 'Admin.Global'), + ), + 'zipcode' => array( + 'title' => $this->trans('Zip/Postal code', array(), 'Admin.Global'), + 'class' => 'fixed-width-md', + ), + 'behavior' => array( + 'title' => $this->trans('Behavior', array(), 'Admin.International.Feature'), + ), + 'rate' => array( + 'title' => $this->trans('Tax', array(), 'Admin.Global'), + 'class' => 'fixed-width-sm', + ), + 'description' => array( + 'title' => $this->trans('Description', array(), 'Admin.Global'), + ), + ); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = ' + c.`name` AS country_name, + s.`name` AS state_name, + CONCAT_WS(" - ", a.`zipcode_from`, a.`zipcode_to`) AS zipcode, + t.rate'; + + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` c + ON (a.`id_country` = c.`id_country` AND id_lang = ' . (int) $this->context->language->id . ') + LEFT JOIN `' . _DB_PREFIX_ . 'state` s + ON (a.`id_state` = s.`id_state`) + LEFT JOIN `' . _DB_PREFIX_ . 'tax` t + ON (a.`id_tax` = t.`id_tax`)'; + $this->_where = 'AND `id_tax_rules_group` = ' . (int) $id_group; + $this->_use_found_rows = false; + + $this->show_toolbar = false; + $this->tpl_list_vars = array('id_tax_rules_group' => (int) $id_group); + + $this->_filter = false; + + return parent::renderList(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Tax Rules', array(), 'Admin.International.Feature'), + 'icon' => 'icon-money', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Enable', array(), 'Admin.Actions'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save and stay', array(), 'Admin.Actions'), + 'stay' => true, + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + if (!($obj = $this->loadObject(true))) { + return; + } + if (!isset($obj->id)) { + $this->no_back = false; + $content = parent::renderForm(); + } else { + $this->no_back = true; + $this->page_header_toolbar_btn['new'] = array( + 'href' => '#', + 'desc' => $this->trans('Add a new tax rule', array(), 'Admin.International.Feature'), + ); + $content = parent::renderForm(); + $this->tpl_folder = 'tax_rules/'; + $content .= $this->initRuleForm(); + + // We change the variable $ tpl_folder to avoid the overhead calling the file in list_action_edit.tpl in intList (); + + $content .= $this->initRulesList((int) $obj->id); + } + + return $content; + } + + public function initRuleForm() + { + $this->fields_form[0]['form'] = array( + 'legend' => array( + 'title' => $this->trans('New tax rule', array(), 'Admin.International.Feature'), + 'icon' => 'icon-money', + ), + 'input' => array( + array( + 'type' => 'select', + 'label' => $this->trans('Country', array(), 'Admin.Global'), + 'name' => 'country', + 'id' => 'country', + 'options' => array( + 'query' => Country::getCountries($this->context->language->id), + 'id' => 'id_country', + 'name' => 'name', + 'default' => array( + 'value' => 0, + 'label' => $this->trans('All', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'select', + 'label' => $this->trans('State', array(), 'Admin.Global'), + 'name' => 'states[]', + 'id' => 'states', + 'multiple' => true, + 'options' => array( + 'query' => array(), + 'id' => 'id_state', + 'name' => 'name', + 'default' => array( + 'value' => 0, + 'label' => $this->trans('All', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'hidden', + 'name' => 'action', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Zip/postal code range', array(), 'Admin.International.Feature'), + 'name' => 'zipcode', + 'required' => false, + 'hint' => $this->trans('You can define a range of Zip/postal codes (e.g., 75000-75015) or simply use one Zip/postal code.', array(), 'Admin.International.Help'), + ), + array( + 'type' => 'select', + 'label' => $this->trans('Behavior', array(), 'Admin.International.Feature'), + 'name' => 'behavior', + 'required' => false, + 'options' => array( + 'query' => array( + array( + 'id' => 0, + 'name' => $this->trans('This tax only', array(), 'Admin.International.Feature'), + ), + array( + 'id' => 1, + 'name' => $this->trans('Combine', array(), 'Admin.International.Feature'), + ), + array( + 'id' => 2, + 'name' => $this->trans('One after another', array(), 'Admin.International.Feature'), + ), + ), + 'id' => 'id', + 'name' => 'name', + ), + 'hint' => array( + $this->trans('You must define the behavior if an address matches multiple rules:', array(), 'Admin.International.Help') . '
', + $this->trans('- This tax only: Will apply only this tax', array(), 'Admin.International.Help') . '
', + $this->trans('- Combine: Combine taxes (e.g.: 10% + 5% = 15%)', array(), 'Admin.International.Help') . '
', + $this->trans('- One after another: Apply taxes one after another (e.g.: 100 + 10% => 110 + 5% = 115.5)', array(), 'Admin.International.Help'), + ), + ), + array( + 'type' => 'select', + 'label' => $this->trans('Tax', array(), 'Admin.Global'), + 'name' => 'id_tax', + 'required' => false, + 'options' => array( + 'query' => Tax::getTaxes((int) $this->context->language->id), + 'id' => 'id_tax', + 'name' => 'name', + 'default' => array( + 'value' => 0, + 'label' => $this->trans('No Tax', array(), 'Admin.International.Help'), + ), + ), + 'hint' => $this->trans('(Total tax: 9%)', array(), 'Admin.International.Help'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + ), + ), + 'submit' => array( + 'title' => $this->trans('Save and stay', array(), 'Admin.Actions'), + 'stay' => true, + ), + ); + + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_value = array( + 'action' => 'create_rule', + 'id_tax_rules_group' => $obj->id, + 'id_tax_rule' => '', + ); + + $this->getlanguages(); + $helper = new HelperForm(); + $helper->override_folder = $this->tpl_folder; + $helper->currentIndex = self::$currentIndex; + $helper->token = $this->token; + $helper->table = 'tax_rule'; + $helper->identifier = 'id_tax_rule'; + $helper->id = $obj->id; + $helper->toolbar_scroll = true; + $helper->show_toolbar = true; + $helper->languages = $this->_languages; + $helper->default_form_language = $this->default_form_language; + $helper->allow_employee_form_lang = $this->allow_employee_form_lang; + $helper->fields_value = $this->getFieldsValue($this->object); + $helper->toolbar_btn['save_new_rule'] = array( + 'href' => self::$currentIndex . '&id_tax_rules_group=' . $obj->id . '&action=create_rule&token=' . $this->token, + 'desc' => 'Save tax rule', + 'class' => 'process-icon-save', + ); + $helper->submit_action = 'create_rule'; + + return $helper->generateForm($this->fields_form); + } + + public function initProcess() + { + if (Tools::isSubmit('deletetax_rule')) { + if ($this->access('delete')) { + $this->action = 'delete_tax_rule'; + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitBulkdeletetax_rule')) { + if ($this->access('delete')) { + $this->action = 'bulk_delete_tax_rules'; + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::getValue('action') == 'create_rule') { + if ($this->access('add')) { + $this->action = 'create_rule'; + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', array(), 'Admin.Notifications.Error'); + } + } else { + parent::initProcess(); + } + } + + protected function processCreateRule() + { + $zip_code = Tools::getValue('zipcode'); + $zip_code = ('' === $zip_code) ? 0 : $zip_code; + $id_rule = (int) Tools::getValue('id_tax_rule'); + $id_tax = (int) Tools::getValue('id_tax'); + $id_tax_rules_group = (int) Tools::getValue('id_tax_rules_group'); + $behavior = (int) Tools::getValue('behavior'); + $description = pSQL(Tools::getValue('description')); + + if ((int) ($id_country = Tools::getValue('country')) == 0) { + $countries = Country::getCountries($this->context->language->id); + $this->selected_countries = array(); + foreach ($countries as $country) { + $this->selected_countries[] = (int) $country['id_country']; + } + } else { + $this->selected_countries = array($id_country); + } + $this->selected_states = Tools::getValue('states'); + + if (empty($this->selected_states) || count($this->selected_states) == 0) { + $this->selected_states = array(0); + } + $tax_rules_group = new TaxRulesGroup((int) $id_tax_rules_group); + foreach ($this->selected_countries as $id_country) { + $first = true; + foreach ($this->selected_states as $id_state) { + if ($tax_rules_group->hasUniqueTaxRuleForCountry($id_country, $id_state, $id_rule)) { + $this->errors[] = $this->trans('A tax rule already exists for this country/state with tax only behavior.', array(), 'Admin.International.Notification'); + continue; + } + $tr = new TaxRule(); + + // update or creation? + if (isset($id_rule) && $first) { + $tr->id = $id_rule; + $first = false; + } + + $tr->id_tax = $id_tax; + $tax_rules_group = new TaxRulesGroup((int) $id_tax_rules_group); + $tr->id_tax_rules_group = (int) $tax_rules_group->id; + $tr->id_country = (int) $id_country; + $tr->id_state = (int) $id_state; + list($tr->zipcode_from, $tr->zipcode_to) = $tr->breakDownZipCode($zip_code); + + // Construct Object Country + $country = new Country((int) $id_country, (int) $this->context->language->id); + + if ($zip_code && $country->need_zip_code) { + if ($country->zip_code_format) { + foreach (array($tr->zipcode_from, $tr->zipcode_to) as $zip_code) { + if ($zip_code) { + if (!$country->checkZipCode($zip_code)) { + $this->errors[] = $this->trans( + 'The Zip/postal code is invalid. It must be typed as follows: %format% for %country%.', + array( + '%format%' => str_replace('C', $country->iso_code, str_replace('N', '0', str_replace('L', 'A', $country->zip_code_format))), + '%country%' => $country->name, + ), + 'Admin.International.Notification' + ); + } + } + } + } + } + + $tr->behavior = (int) $behavior; + $tr->description = $description; + $this->tax_rule = $tr; + $_POST['id_state'] = $tr->id_state; + + $this->errors = array_merge($this->errors, $this->validateTaxRule($tr)); + + if (count($this->errors) == 0) { + $tax_rules_group = $this->updateTaxRulesGroup($tax_rules_group); + $tr->id = (int) $tax_rules_group->getIdTaxRuleGroupFromHistorizedId((int) $tr->id); + $tr->id_tax_rules_group = (int) $tax_rules_group->id; + + if (!$tr->save()) { + $this->errors[] = $this->trans('An error has occurred: Cannot save the current tax rule.', array(), 'Admin.International.Notification'); + } + } + } + } + + if (count($this->errors) == 0) { + Tools::redirectAdmin( + self::$currentIndex . '&' . $this->identifier . '=' . (int) $tax_rules_group->id . '&conf=4&update' . $this->table . '&token=' . $this->token + ); + } else { + $this->display = 'edit'; + } + } + + protected function processBulkDeleteTaxRules() + { + $this->deleteTaxRule(Tools::getValue('tax_ruleBox')); + } + + protected function processDeleteTaxRule() + { + $this->deleteTaxRule(array(Tools::getValue('id_tax_rule'))); + } + + protected function deleteTaxRule(array $id_tax_rule_list) + { + $result = true; + + foreach ($id_tax_rule_list as $id_tax_rule) { + $tax_rule = new TaxRule((int) $id_tax_rule); + if (Validate::isLoadedObject($tax_rule)) { + $tax_rules_group = new TaxRulesGroup((int) $tax_rule->id_tax_rules_group); + $tax_rules_group = $this->updateTaxRulesGroup($tax_rules_group); + $tax_rule = new TaxRule($tax_rules_group->getIdTaxRuleGroupFromHistorizedId((int) $id_tax_rule)); + if (Validate::isLoadedObject($tax_rule)) { + $result &= $tax_rule->delete(); + } + } + } + + Tools::redirectAdmin( + self::$currentIndex . '&' . $this->identifier . '=' . (int) $tax_rules_group->id . '&conf=4&update' . $this->table . '&token=' . $this->token + ); + } + + /** + * Check if the tax rule could be added in the database. + * + * @param TaxRule $tr + * + * @return array + */ + protected function validateTaxRule(TaxRule $tr) + { + // @TODO: check if the rule already exists + return $tr->validateController(); + } + + protected function displayAjaxUpdateTaxRule() + { + if ($this->access('view')) { + $id_tax_rule = Tools::getValue('id_tax_rule'); + $tax_rules = new TaxRule((int) $id_tax_rule); + $output = array(); + foreach ($tax_rules as $key => $result) { + $output[$key] = $result; + } + die(json_encode($output)); + } + } + + /** + * @param TaxRulesGroup $object + * + * @return TaxRulesGroup + */ + protected function updateTaxRulesGroup($object) + { + static $tax_rules_group = null; + if ($tax_rules_group === null) { + $object->update(); + $tax_rules_group = $object; + } + + return $tax_rules_group; + } +} diff --git a/controllers/admin/AdminTaxesController.php b/controllers/admin/AdminTaxesController.php new file mode 100644 index 00000000..e0e4cf1e --- /dev/null +++ b/controllers/admin/AdminTaxesController.php @@ -0,0 +1,296 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Tax $object + */ +class AdminTaxesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'tax'; + $this->className = 'Tax'; + $this->lang = true; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->fields_list = array( + 'id_tax' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'width' => 'auto'), + 'rate' => array('title' => $this->trans('Rate', array(), 'Admin.International.Feature'), 'align' => 'center', 'suffix' => '%', 'class' => 'fixed-width-md'), + 'active' => array('title' => $this->trans('Enabled', array(), 'Admin.Global'), 'width' => 25, 'align' => 'center', 'active' => 'status', 'type' => 'bool', 'orderby' => false, 'class' => 'fixed-width-sm', 'remove_onclick' => true), + ); + + $ecotax_desc = ''; + if (Configuration::get('PS_USE_ECOTAX')) { + $ecotax_desc = $this->trans('If you disable the ecotax, the ecotax for all your products will be set to 0.', array(), 'Admin.International.Help'); + } + + $this->fields_options = array( + 'general' => array( + 'title' => $this->trans('Tax options', array(), 'Admin.International.Feature'), + 'fields' => array( + 'PS_TAX' => array( + 'title' => $this->trans('Enable tax', array(), 'Admin.International.Feature'), + 'desc' => $this->trans('Select whether or not to include tax on purchases.', array(), 'Admin.International.Help'), + 'cast' => 'intval', 'type' => 'bool', ), + 'PS_TAX_DISPLAY' => array( + 'title' => $this->trans('Display tax in the shopping cart', array(), 'Admin.International.Feature'), + 'desc' => $this->trans('Select whether or not to display tax on a distinct line in the cart.', array(), 'Admin.International.Help'), + 'cast' => 'intval', + 'type' => 'bool', ), + 'PS_TAX_ADDRESS_TYPE' => array( + 'title' => $this->trans('Based on', array(), 'Admin.International.Feature'), + 'cast' => 'pSQL', + 'type' => 'select', + 'list' => array( + array( + 'name' => $this->trans('Invoice address', array(), 'Admin.International.Feature'), + 'id' => 'id_address_invoice', + ), + array( + 'name' => $this->trans('Delivery address', array(), 'Admin.International.Feature'), + 'id' => 'id_address_delivery', ), + ), + 'identifier' => 'id', + ), + 'PS_USE_ECOTAX' => array( + 'title' => $this->trans('Use ecotax', array(), 'Admin.International.Feature'), + 'desc' => $ecotax_desc, + 'validation' => 'isBool', + 'cast' => 'intval', + 'type' => 'bool', + ), + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ), + ); + + if (Configuration::get('PS_USE_ECOTAX') || Tools::getValue('PS_USE_ECOTAX')) { + $this->fields_options['general']['fields']['PS_ECOTAX_TAX_RULES_GROUP_ID'] = array( + 'title' => $this->trans('Ecotax', array(), 'Admin.International.Feature'), + 'hint' => $this->trans('Define the ecotax (e.g. French ecotax: 19.6%).', array(), 'Admin.International.Help'), + 'cast' => 'intval', + 'type' => 'select', + 'identifier' => 'id_tax_rules_group', + 'list' => TaxRulesGroup::getTaxRulesGroupsForOptions(), + ); + } + + $this->_where .= ' AND a.deleted = 0'; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_tax'] = array( + 'href' => self::$currentIndex . '&addtax&token=' . $this->token, + 'desc' => $this->trans('Add new tax', array(), 'Admin.International.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + /** + * Display delete action link. + * + * @param string|null $token + * @param int $id + * + * @return string + * + * @throws Exception + * @throws SmartyException + */ + public function displayDeleteLink($token, $id) + { + if (!array_key_exists('Delete', self::$cache_lang)) { + self::$cache_lang['Delete'] = $this->trans('Delete', array(), 'Admin.Actions'); + } + + if (!array_key_exists('DeleteItem', self::$cache_lang)) { + self::$cache_lang['DeleteItem'] = $this->trans('Delete item #', array(), 'Admin.International.Feature'); + } + + if (TaxRule::isTaxInUse($id)) { + $confirm = $this->trans('This tax is currently in use as a tax rule. Are you sure you\'d like to continue?', array(), 'Admin.International.Notification'); + } + + $this->context->smarty->assign(array( + 'href' => self::$currentIndex . '&' . $this->identifier . '=' . $id . '&delete' . $this->table . '&token=' . ($token != null ? $token : $this->token), + 'confirm' => (isset($confirm) ? '\r' . $confirm : self::$cache_lang['DeleteItem'] . $id . ' ? '), + 'action' => self::$cache_lang['Delete'], + )); + + return $this->context->smarty->fetch('helpers/list/list_action_delete.tpl'); + } + + /** + * Fetch the template for action enable. + * + * @param string $token + * @param int $id + * @param int $value state enabled or not + * @param string $active status + * @param int $id_category + * @param int $id_product + */ + public function displayEnableLink($token, $id, $value, $active, $id_category = null, $id_product = null) + { + if ($value && TaxRule::isTaxInUse($id)) { + $confirm = $this->trans('This tax is currently in use as a tax rule. If you continue, this tax will be removed from the tax rule. Are you sure you\'d like to continue?', array(), 'Admin.International.Notification'); + } + $tpl_enable = $this->context->smarty->createTemplate('helpers/list/list_action_enable.tpl'); + $tpl_enable->assign(array( + 'enabled' => (bool) $value, + 'url_enable' => self::$currentIndex . '&' . $this->identifier . '=' . (int) $id . '&' . $active . $this->table . + ((int) $id_category && (int) $id_product ? '&id_category=' . (int) $id_category : '') . '&token=' . ($token != null ? $token : $this->token), + 'confirm' => isset($confirm) ? $confirm : null, + )); + + return $tpl_enable->fetch(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Taxes', array(), 'Admin.Global'), + 'icon' => 'icon-money', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'lang' => true, + 'hint' => $this->trans('Tax name to display in carts and on invoices (e.g. "VAT").', array(), 'Admin.International.Help') . ' - ' . $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'text', + 'label' => $this->trans('Rate', array(), 'Admin.International.Feature'), + 'name' => 'rate', + 'maxlength' => 6, + 'required' => true, + 'hint' => $this->trans('Format: XX.XX or XX.XXX (e.g. 19.60 or 13.925)', array(), 'Admin.International.Help') . ' - ' . $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Enable', array(), 'Admin.Actions'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + return parent::renderForm(); + } + + public function postProcess() + { + if ($this->action == 'save') { + /* Checking fields validity */ + $this->validateRules(); + if (!count($this->errors)) { + $id = (int) (Tools::getValue('id_' . $this->table)); + + /* Object update */ + if (isset($id) && !empty($id)) { + /** @var Tax $object */ + $object = new $this->className($id); + if (Validate::isLoadedObject($object)) { + $this->copyFromPost($object, $this->table); + $result = $object->update(false, false); + + if (!$result) { + $this->errors[] = $this->trans('An error occurred while updating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ''; + } elseif ($this->postImage($object->id)) { + Tools::redirectAdmin(self::$currentIndex . '&id_' . $this->table . '=' . $object->id . '&conf=4' . '&token=' . $this->token); + } + } else { + $this->errors[] = $this->trans('An error occurred while updating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', array(), 'Admin.Notifications.Error'); + } + } else { + // Object creation + /** @var Tax $object */ + $object = new $this->className(); + $this->copyFromPost($object, $this->table); + if (!$object->add()) { + $this->errors[] = $this->trans('An error occurred while creating an object.', array(), 'Admin.Notifications.Error') . ' ' . $this->table . ''; + } elseif (($_POST['id_' . $this->table] = $object->id /* voluntary */) && $this->postImage($object->id) && $this->_redirect) { + Tools::redirectAdmin(self::$currentIndex . '&id_' . $this->table . '=' . $object->id . '&conf=3' . '&token=' . $this->token); + } + } + } + } else { + parent::postProcess(); + } + } + + public function updateOptionPsUseEcotax($value) + { + $old_value = (int) Configuration::get('PS_USE_ECOTAX'); + + if ($old_value != $value) { + // Reset ecotax + if ($value == 0) { + Product::resetEcoTax(); + } + + Configuration::updateValue('PS_USE_ECOTAX', (int) $value); + } + } +} diff --git a/controllers/admin/AdminThemesController.php b/controllers/admin/AdminThemesController.php new file mode 100644 index 00000000..7a370ce0 --- /dev/null +++ b/controllers/admin/AdminThemesController.php @@ -0,0 +1,1120 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +use PrestaShop\PrestaShop\Core\Addon\Theme\ThemeManager; +use PrestaShop\PrestaShop\Core\Addon\Theme\ThemeManagerBuilder; +use PrestaShop\PrestaShop\Core\Addon\Theme\ThemeRepository; +use PrestaShop\PrestaShop\Core\Shop\LogoUploader; +use PrestaShop\PrestaShop\Adapter\SymfonyContainer; + +/** + * @property Theme $object + */ +class AdminThemesControllerCore extends AdminController +{ + // temp + const CACHE_FILE_CUSTOMER_THEMES_LIST = '/config/xml/customer_themes_list.xml'; + const CACHE_FILE_MUST_HAVE_THEMES_LIST = '/config/xml/must_have_themes_list.xml'; + + /** + * @var ThemeManager + */ + public $theme_manager; + + /** + * @var ThemeRepository + */ + protected $theme_repository; + protected $toolbar_scroll = false; + protected $authAccesses = array(); + private $img_error; + + /* @var LogoUploader $logo_uploader */ + private $logo_uploader; + + public function __construct() + { + $this->bootstrap = true; + parent::__construct(); + + $this->theme_manager = (new ThemeManagerBuilder($this->context, Db::getInstance()))->build(); + $this->theme_repository = (new ThemeManagerBuilder($this->context, Db::getInstance()))->buildRepository(); + $this->logo_uploader = new LogoUploader($this->context->shop); + } + + public function init() + { + // No cache for auto-refresh uploaded logo + header('Cache-Control: no-cache, must-revalidate'); + header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); + + parent::init(); + $this->can_display_themes = (!Shop::isFeatureActive() || Shop::getContext() == Shop::CONTEXT_SHOP); + + if (Tools::getValue('display') == 'configureLayouts') { + $this->initConfigureLayouts(); + } elseif (Tools::getValue('action') == 'importtheme') { + $this->display = 'importtheme'; + } + + libxml_use_internal_errors(true); + } + + public function downloadAddonsThemes() + { + if (!$this->logged_on_addons) { + return false; + } + + try { + $this->validateAddAuthorization(); + } catch (Exception $e) { + return false; + } + + if (!$this->isFresh(self::CACHE_FILE_CUSTOMER_THEMES_LIST, 86400)) { + file_put_contents( + _PS_ROOT_DIR_ . self::CACHE_FILE_CUSTOMER_THEMES_LIST, + Tools::addonsRequest('customer_themes') + ); + } + + $customer_themes_list = file_get_contents(_PS_ROOT_DIR_ . self::CACHE_FILE_CUSTOMER_THEMES_LIST); + if (!empty($customer_themes_list) && $customer_themes_list_xml = @simplexml_load_string($customer_themes_list)) { + foreach ($customer_themes_list_xml->theme as $addons_theme) { + //get addons theme if folder does not exist + $ids_themes = Tools::unSerialize(Configuration::get('PS_ADDONS_THEMES_IDS')); + + if (!is_array($ids_themes) || (is_array($ids_themes) && !in_array((string) $addons_theme->id, $ids_themes))) { + $zip_content = Tools::addonsRequest( + 'module', + array( + 'id_module' => pSQL($addons_theme->id), + 'username_addons' => pSQL(trim($this->context->cookie->username_addons)), + 'password_addons' => pSQL(trim($this->context->cookie->password_addons)), + ) + ); + + $uniqid = uniqid(); + $sandbox = _PS_CACHE_DIR_ . 'sandbox' . DIRECTORY_SEPARATOR . $uniqid . DIRECTORY_SEPARATOR; + mkdir($sandbox); + + file_put_contents($sandbox . (string) $addons_theme->getName() . '.zip', $zip_content); + + if ($this->extractTheme($sandbox . (string) $addons_theme->getName() . '.zip', $sandbox)) { + if ($theme_directory = $this->installTheme(self::UPLOADED_THEME_DIR_NAME, $sandbox, false)) { + $ids_themes[$theme_directory] = (string) $addons_theme->id; + } + } + + Tools::deleteDirectory($sandbox); + } + Configuration::updateValue('PS_ADDONS_THEMES_IDS', serialize($ids_themes)); + } + } + } + + protected function processUpdateOptions() + { + parent::processUpdateOptions(); + + if (!count($this->errors)) { + Tools::redirectAdmin(Context::getContext()->link->getAdminLink('AdminThemes') . '&conf=6'); + } + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + if (empty($this->display)) { + if ($this->isAddGranted()) { + $this->page_header_toolbar_btn['import_theme'] = array( + 'href' => self::$currentIndex . '&action=importtheme&token=' . $this->token, + 'desc' => $this->trans('Add new theme', array(), 'Admin.Design.Feature'), + 'icon' => 'process-icon-new', + ); + $this->page_header_toolbar_btn['export_theme'] = array( + 'href' => self::$currentIndex . '&action=exporttheme&token=' . $this->token, + 'desc' => $this->trans('Export current theme', array(), 'Admin.Design.Feature'), + 'icon' => 'process-icon-export', + ); + } + + if ($this->context->mode) { + unset($this->toolbar_btn['new']); + } + } + + if ($this->display == 'importtheme') { + $this->toolbar_title[] = $this->trans('Import theme', array(), 'Admin.Design.Feature'); + } else { + $this->toolbar_title[] = $this->trans('Theme', array(), 'Admin.Design.Feature'); + } + + $title = implode(' ' . Configuration::get('PS_NAVIGATION_PIPE') . ' ', $this->toolbar_title); + $this->page_header_toolbar_title = $title; + } + + public function initContent() + { + if ($this->display == 'list') { + $this->display = ''; + } + + if (isset($this->display) && method_exists($this, 'render' . $this->display)) { + $this->content .= $this->initPageHeaderToolbar(); + + $this->content .= $this->{'render' . $this->display}(); + $this->context->smarty->assign(array( + 'content' => $this->content, + 'show_page_header_toolbar' => $this->show_page_header_toolbar, + 'page_header_toolbar_title' => $this->page_header_toolbar_title, + 'page_header_toolbar_btn' => $this->page_header_toolbar_btn, + )); + } else { + $content = ''; + if (Configuration::hasKey('PS_LOGO') && trim(Configuration::get('PS_LOGO')) != '' + && file_exists(_PS_IMG_DIR_ . Configuration::get('PS_LOGO')) && filesize(_PS_IMG_DIR_ . Configuration::get('PS_LOGO'))) { + list($width, $height, $type, $attr) = getimagesize(_PS_IMG_DIR_ . Configuration::get('PS_LOGO')); + Configuration::updateValue('SHOP_LOGO_HEIGHT', (int) round($height)); + Configuration::updateValue('SHOP_LOGO_WIDTH', (int) round($width)); + } + + $this->content .= $content; + + return parent::initContent(); + } + } + + /** + * Ajax request handler for displaying theme catalog from the marketplace. + * Not used anymore. + * + * @deprecated since 1.7.4.0 + */ + public function ajaxProcessGetAddonsThemes() + { + $parent_domain = Tools::getHttpHost(true) . substr($_SERVER['REQUEST_URI'], 0, -1 * strlen(basename($_SERVER['REQUEST_URI']))); + $iso_lang = $this->context->language->iso_code; + $iso_currency = $this->context->currency->iso_code; + $iso_country = $this->context->country->iso_code; + $activity = Configuration::get('PS_SHOP_ACTIVITY'); + $addons_url = Tools::getCurrentUrlProtocolPrefix() . 'addons.prestashop.com/iframe/search-1.7.php?psVersion=' . _PS_VERSION_ . '&onlyThemes=1&isoLang=' . $iso_lang . '&isoCurrency=' . $iso_currency . '&isoCountry=' . $iso_country . '&activity=' . (int) $activity . '&parentUrl=' . $parent_domain; + + die(Tools::file_get_contents($addons_url)); + } + + public function renderView() + { + $this->tpl_view_vars = array( + 'doc' => $this->doc, + 'theme_name' => $this->theme_name, + 'img_error' => $this->img_error, + 'modules_errors' => $this->modules_errors, + 'back_link' => Context::getContext()->link->getAdminLink('AdminThemes'), + 'image_link' => Context::getContext()->link->getAdminLink('AdminImages'), + ); + + return parent::renderView(); + } + + /** + * This functions make checks about AdminThemes configuration edition only. + * + * @since 1.4 + */ + public function postProcess() + { + if (isset($_GET['error'])) { + $this->errors[] = $this->trans( + 'You do not have permission to edit this.', + array(), + 'Admin.Notifications.Error' + ); + } + + // done here, because if it is true, $_FILES & $_POST are empty, so we don't have any message. + try { + $this->validateUploadSize(); + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + + /* Specific processes, depending on action and/or submitted form */ + switch (Tools::getValue('action')) { + case 'importtheme': + if (Tools::isSubmit('submitAddconfiguration')) { + $this->postProcessSubmitAddConfiguration(); + } + break; + + case 'exporttheme': + if (false === $this->postProcessExportTheme()) { + return false; + } + break; + + case 'enableTheme': + $this->postProcessEnableTheme(); + break; + + case 'deleteTheme': + $this->postProcessDeleteTheme(); + break; + + case 'resetToDefaults': + $this->postProcessResetToDefaults(); + break; + + case 'submitConfigureLayouts': + $this->postProcessSubmitConfigureLayouts(); + break; + + // Main Theme page + default: + if (Tools::isSubmit('submitGenerateRTL')) { + $this->postProcessSubmitGenerateRTL(); + } + + if (Tools::isSubmit('submitOptionsconfiguration')) { + $this->postProcessSubmitOptionsConfiguration(); + } + } + + return parent::postProcess(); + } + + public function processUploadFile($dest) + { + try { + $this->validateAddAuthorization(); + } catch (Exception $e) { + $this->errors[] = $this->trans( + 'You do not have permission to upload this.', + array(), + 'Admin.Notifications.Error' + ); + + return false; + } + + switch ($_FILES['themearchive']['error']) { + case UPLOAD_ERR_OK: + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $this->errors[] = $this->trans('The uploaded file is too large.', array(), 'Admin.Design.Notification'); + + return false; + default: + $this->errors[] = $this->trans('Unknown error.', array(), 'Admin.Notifications.Error'); + + return false; + } + + $tmp_name = $_FILES['themearchive']['tmp_name']; + $mimeType = false; + $goodMimeType = false; + + if (function_exists('finfo_open')) { + $finfo = @finfo_open(FILEINFO_MIME); + $mimeType = @finfo_file($finfo, $tmp_name); + @finfo_close($finfo); + } elseif (function_exists('mime_content_type')) { + $mimeType = @mime_content_type($tmp_name); + } elseif (function_exists('exec')) { + $mimeType = trim(@exec('file -b --mime-type ' . escapeshellarg($tmp_name))); + if (!$mimeType) { + $mimeType = trim(@exec('file --mime ' . escapeshellarg($tmp_name))); + } + if (!$mimeType) { + $mimeType = trim(@exec('file -bi ' . escapeshellarg($tmp_name))); + } + } + + if (!empty($mimeType)) { + preg_match('#application/zip#', $mimeType, $matches); + if (!empty($matches)) { + $goodMimeType = true; + } + } + + if (false === $goodMimeType) { + $this->errors[] = $this->trans('Invalid file format.', array(), 'Admin.Design.Notification'); + + return false; + } + + $name = $_FILES['themearchive']['name']; + if (!Validate::isFileName($name)) { + $dest = _PS_ALL_THEMES_DIR_ . sha1_file($tmp_name) . '.zip'; + } + + if (!move_uploaded_file( + $_FILES['themearchive']['tmp_name'], + $dest + )) { + $this->errors[] = $this->trans('Failed to move uploaded file.', array(), 'Admin.Design.Notification'); + + return false; + } + + return $dest; + } + + /** + * Function used to render the options for this controller. + */ + public function renderOptions() + { + // Download user themes from Addons + if ($this->logged_on_addons) { + $this->downloadAddonsThemes(); + } + + // Employee languages used for link and utm_source + $lang = new Language($this->context->language->id); + $iso_lang_uc = strtoupper($lang->iso_code); + $url = 'https://addons.prestashop.com/create-favicon.php?pab=1'; + + $this->fields_options = array( + 'appearance' => array( + 'title' => $this->trans('Your current theme', array(), 'Admin.Design.Feature'), + 'icon' => 'icon-html5', + 'tabs' => array( + 'logo' => $this->trans('Logo', array(), 'Admin.Global'), + 'logo2' => $this->trans('Invoice & Email Logos', array(), 'Admin.Design.Feature'), + 'icons' => $this->trans('Favicons', array(), 'Admin.Design.Feature'), + ), + 'fields' => array( + 'PS_LOGO' => array( + 'title' => $this->trans('Header logo', array(), 'Admin.Design.Feature'), + 'hint' => $this->trans('Will appear on main page. Recommended size for the default theme: height %height% and width %width%.', + array( + '%height%' => '40px', + '%width%' => '200px', + ), + 'Admin.Design.Help'), + 'type' => 'file', + 'name' => 'PS_LOGO', + 'tab' => 'logo', + 'thumb' => _PS_IMG_ . Configuration::get('PS_LOGO'), + ), + 'PS_LOGO_MAIL' => array( + 'title' => $this->trans('Mail logo', array(), 'Admin.Design.Feature'), + 'desc' => ((Configuration::get('PS_LOGO_MAIL') === false) ? '' . $this->trans('Warning: if no email logo is available, the main logo will be used instead.', array(), 'Admin.Design.Notification') . '
' : ''), + 'hint' => $this->trans('Will appear on email headers. If undefined, the header logo will be used.', array(), 'Admin.Design.Help'), + 'type' => 'file', + 'name' => 'PS_LOGO_MAIL', + 'tab' => 'logo2', + 'thumb' => (Configuration::get('PS_LOGO_MAIL') !== false && file_exists(_PS_IMG_DIR_ . Configuration::get('PS_LOGO_MAIL'))) ? _PS_IMG_ . Configuration::get('PS_LOGO_MAIL') : _PS_IMG_ . Configuration::get('PS_LOGO'), + ), + 'PS_LOGO_INVOICE' => array( + 'title' => $this->trans('Invoice logo', array(), 'Admin.Design.Feature'), + 'desc' => ((Configuration::get('PS_LOGO_INVOICE') === false) ? '' . $this->trans('Warning: if no invoice logo is available, the main logo will be used instead.', array(), 'Admin.Design.Help') . '
' : ''), + 'hint' => $this->trans('Will appear on invoice headers.', array(), 'Admin.Design.Help') . ' ' . $this->trans('Warning: you can use a PNG file for transparency, but it can take up to 1 second per page for processing. Please consider using JPG instead.', array(), 'Admin.Design.Help'), + 'type' => 'file', + 'name' => 'PS_LOGO_INVOICE', + 'tab' => 'logo2', + 'thumb' => (Configuration::get('PS_LOGO_INVOICE') !== false && file_exists(_PS_IMG_DIR_ . Configuration::get('PS_LOGO_INVOICE'))) ? _PS_IMG_ . Configuration::get('PS_LOGO_INVOICE') : _PS_IMG_ . Configuration::get('PS_LOGO'), + ), + 'PS_FAVICON' => array( + 'title' => $this->trans('Favicon', array(), 'Admin.Design.Feature'), + 'desc' => $this->trans('Use our [1]favicon generator on PrestaShop Marketplace[/1] to boost your brand image!', array('[1]' => '', '[/1]' => ''), 'Admin.Design.Help'), + 'hint' => $this->trans('It is the small icon that appears in browser tabs, next to the web address', array(), 'Admin.Design.Help'), + 'type' => 'file', + 'name' => 'PS_FAVICON', + 'tab' => 'icons', + 'thumb' => _PS_IMG_ . Configuration::get('PS_FAVICON') . (Tools::getValue('conf') ? sprintf('?%04d', rand(0, 9999)) : ''), + ), + ), + 'after_tabs' => array( + 'cur_theme' => $this->context->shop->theme, + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + 'buttons' => array( + 'storeLink' => array( + 'id' => 'visit-theme-catalog-link', + 'title' => $this->trans('Visit the theme catalog', array(), 'Admin.Design.Feature'), + 'icon' => 'process-icon-themes', + 'href' => Tools::getCurrentUrlProtocolPrefix() . 'addons.prestashop.com/en/3-templates-prestashop' + . '?utm_source=back-office&utm_medium=theme-button' + . '&utm_campaign=back-office-' . $iso_lang_uc + . '&utm_content=' . (defined('_PS_HOST_MODE_') ? 'cloud' : 'download'), + 'js' => 'return !window.open(this.href)', + ), + ), + ), + ); + + if (in_array('1', array_column($this->_languages, 'is_rtl'))) { + $themes_list = array(); + $allThemes = $this->theme_repository->getList(); + foreach ($allThemes as $theme) { + $themeName = $theme->getName(); + $themes_list[] = array('theme' => $themeName, 'name' => $themeName); + } + $this->fields_options['RTL'] = array( + 'title' => $this->trans('Adaptation to Right-to-Left languages', array(), 'Admin.Design.Feature'), + 'description' => $this->trans('Be careful! Please check your theme in an RTL language before generating the RTL stylesheet: your theme could be already adapted to RTL.\nOnce you click on "Adapt to RTL", any RTL-specific file that you might have added to your theme might be deleted by the created stylesheet.', array(), 'Admin.Design.Help'), + 'fields' => array( + 'PS_THEMES_LIST' => array( + 'title' => $this->trans('Theme to adapt', array(), 'Admin.Design.Feature'), + 'type' => 'select', + 'identifier' => 'theme', + 'list' => $themes_list, + ), + 'PS_GENERATE_RTL' => array( + 'title' => $this->trans('Generate RTL stylesheet', array(), 'Admin.Design.Feature'), + 'validation' => 'isBool', + 'cast' => 'intval', + 'type' => 'bool', + 'default' => 0, + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + 'name' => 'submitGenerateRTL', + ), + ); + } + + $other_themes = $this->theme_repository->getListExcluding([$this->context->shop->theme->getName()]); + if (!empty($other_themes)) { + $this->fields_options['theme'] = array( + 'title' => $this->trans('Select a theme for the "%name%" shop', array('%name%' => $this->context->shop->name), 'Admin.Design.Feature'), + 'description' => (!$this->can_display_themes) ? $this->trans('You must select a shop from the above list if you wish to choose a theme.', array(), 'Admin.Design.Help') : '', + 'fields' => array( + 'theme_for_shop' => array( + 'type' => 'theme', + 'themes' => $other_themes, + 'can_display_themes' => $this->can_display_themes, + 'can_delete_themes' => $this->isDeleteGranted(), + 'no_multishop_checkbox' => true, + ), + ), + ); + } + + if (isset($this->display) && method_exists($this, 'render' . $this->display)) { + return $this->{'render' . $this->display}(); + } + if ($this->fields_options && is_array($this->fields_options)) { + $helper = new HelperOptions($this); + $this->setHelperDisplay($helper); + $helper->toolbar_scroll = true; + $helper->title = $this->trans('Theme appearance', array(), 'Admin.Design.Feature'); + $helper->toolbar_btn = array( + 'save' => array( + 'href' => '#', + 'desc' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + $helper->id = $this->id; + $helper->tpl_vars = $this->tpl_option_vars; + $options = $helper->generateOptions($this->fields_options); + + return $options; + } + } + + public function renderImportTheme() + { + try { + $this->validateAddAuthorization(true); + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + + return false; + } + + $fields_form = array(); + + $toolbar_btn['save'] = array( + 'href' => '#', + 'desc' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + if ($this->context->mode != Context::MODE_HOST) { + $fields_form[0] = array( + 'form' => array( + 'tinymce' => false, + 'legend' => array( + 'title' => $this->trans('Import from your computer', array(), 'Admin.Design.Feature'), + 'icon' => 'icon-picture', + ), + 'input' => array( + array( + 'type' => 'file', + 'label' => $this->trans('Zip file', array(), 'Admin.Design.Feature'), + 'desc' => $this->trans('Browse your computer files and select the Zip file for your new theme.', array(), 'Admin.Design.Help'), + 'name' => 'themearchive', + ), + ), + 'submit' => array( + 'id' => 'zip', + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ), + ); + + $fields_form[1] = array( + 'form' => array( + 'tinymce' => false, + 'legend' => array( + 'title' => $this->trans('Import from the web', array(), 'Admin.Design.Feature'), + 'icon' => 'icon-picture', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Archive URL', array(), 'Admin.Design.Feature'), + 'desc' => $this->trans('Indicate the complete URL to an online Zip file that contains your new theme. For instance, "http://example.com/files/theme.zip".', array(), 'Admin.Design.Help'), + 'name' => 'themearchiveUrl', + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ), + ); + + $theme_archive_server = array(); + $files = scandir(_PS_ALL_THEMES_DIR_, SCANDIR_SORT_NONE); + $theme_archive_server[] = '-'; + + foreach ($files as $file) { + if (is_file(_PS_ALL_THEMES_DIR_ . $file) && substr(_PS_ALL_THEMES_DIR_ . $file, -4) == '.zip') { + $theme_archive_server[] = array( + 'id' => basename(_PS_ALL_THEMES_DIR_ . $file), + 'name' => basename(_PS_ALL_THEMES_DIR_ . $file), + ); + } + } + + $fields_form[2] = array( + 'form' => array( + 'tinymce' => false, + 'legend' => array( + 'title' => $this->trans('Import from FTP', array(), 'Admin.Design.Feature'), + 'icon' => 'icon-picture', + ), + 'input' => array( + array( + 'type' => 'select', + 'label' => $this->trans('Select the archive', array(), 'Admin.Design.Feature'), + 'name' => 'theme_archive_server', + 'desc' => $this->trans('This selector lists the Zip files that you uploaded in the \'/themes\' folder.', array(), 'Admin.Design.Help'), + 'options' => array( + 'id' => 'id', + 'name' => 'name', + 'query' => $theme_archive_server, + ), + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ), + ); + } + + $this->context->smarty->assign( + array( + 'import_theme' => true, + 'logged_on_addons' => $this->logged_on_addons, + 'iso_code' => $this->context->language->iso_code, + ) + ); + + $helper = new HelperForm(); + + $helper->currentIndex = $this->context->link->getAdminLink('AdminThemes', false) . '&action=importtheme'; + $helper->token = Tools::getAdminTokenLite('AdminThemes'); + $helper->show_toolbar = true; + $helper->toolbar_btn = $toolbar_btn; + $helper->fields_value['themearchiveUrl'] = ''; + $helper->fields_value['theme_archive_server'] = array(); + $helper->multiple_fieldsets = true; + $helper->override_folder = $this->tpl_folder; + $helper->languages = $this->getLanguages(); + $helper->default_form_language = (int) $this->context->language->id; + + return $helper->generateForm($fields_form); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJS(_PS_JS_DIR_ . 'admin/themes.js'); + + if ($this->context->mode == Context::MODE_HOST && Tools::getValue('action') == 'importtheme') { + $this->addJS(_PS_JS_DIR_ . 'admin/addons.js'); + } + } + + public function initConfigureLayouts() + { + $theme = $this->theme_repository->getInstanceByName($this->context->shop->theme->getName()); + + $this->context->smarty->assign([ + 'pages' => Meta::getAllMeta($this->context->language->id), + 'default_layout' => $this->translateAttributes($theme->getDefaultLayout()), + 'page_layouts' => $theme->getPageLayouts(), + 'available_layouts' => $this->translateAttributes($theme->getAvailableLayouts()), + ]); + + $this->setTemplate('controllers/themes/configurelayouts.tpl'); + } + + /** + * Translate attributes (from yml). + * + * @param $attributes + * + * @return mixed + */ + private function translateAttributes($attributes) + { + if (!empty($attributes)) { + foreach ($attributes as $key => &$layout) { + // layout can be an array of array, or just an array :/ we just translate name & description, see theme.dist.yml + if (is_array($layout)) { + if (array_key_exists('name', $layout)) { + $layout['name'] = $this->translator->trans($layout['name'], array(), 'Admin.Design.Feature'); + } + if (array_key_exists('description', $layout)) { + $layout['description'] = $this->translator->trans($layout['description'], array(), 'Admin.Design.Feature'); + } + } else { + if (in_array($key, array('name', 'description'))) { + $attributes[$key] = $this->translator->trans($layout, array(), 'Admin.Design.Feature'); + } + } + } + } + + return $attributes; + } + + public function processSubmitConfigureLayouts() + { + if ($this->isAllGranted()) { + $this->context->shop->theme->setPageLayouts(Tools::getValue('layouts')); + $this->theme_manager->saveTheme($this->context->shop->theme); + } + + Tools::clearCache(); + } + + /** + * Verify if all actions are authorized in this controller. + * + * @return bool true if access is granted + */ + protected function isAllGranted() + { + // Delete access is the highest access level + return $this->isDeleteGranted(); + } + + /** + * Verify if delete is authorized in this controller. + * + * @return bool true if delete access is granted + */ + protected function isDeleteGranted() + { + return $this->isAccessGranted(AdminController::LEVEL_DELETE); + } + + /** + * Verify if add is authorized in this controller. + * + * @return bool true if add access is granted + */ + protected function isAddGranted() + { + return $this->isAccessGranted(AdminController::LEVEL_ADD); + } + + /** + * Verify if edit is authorized in this controller. + * + * @return bool true if edit access is granted + */ + protected function isEditGranted() + { + return $this->isAccessGranted(AdminController::LEVEL_EDIT); + } + + /** + * Verify if view is authorized in this controller. + * + * @return bool true if view access is granted + */ + protected function isViewGranted() + { + return $this->isAccessGranted(AdminController::LEVEL_VIEW); + } + + /** + * Verify if $accessLevel is granted for this controller. + * + * @param int $accessLevel Access level to be verified + * + * @return bool true if this access level is granted for this controller + */ + protected function isAccessGranted($accessLevel) + { + $accessLevel = (int) $accessLevel; + if (!in_array( + $accessLevel, + array( + AdminController::LEVEL_VIEW, + AdminController::LEVEL_EDIT, + AdminController::LEVEL_ADD, + AdminController::LEVEL_DELETE, + ) + )) { + throw new InvalidArgumentException('Unknown access level : ' . $accessLevel); + } + + if (empty($this->authAccesses[$accessLevel])) { + $this->authAccesses[$accessLevel] = $this->authorizationLevel() >= (int) $accessLevel; + } + + return $this->authAccesses[$accessLevel]; + } + + /** + * Validate access for all action types. + * + * If access is not granted, the thrown Exception's error message contains the translated rejection message. + * + * @param bool $allowDemoMode Should we grant access in demo mode ? + * + * @throws Exception + */ + protected function validateAllAuthorizations($allowDemoMode = false) + { + // DELETE level is the max possible level. If it is granted, everything else is granted. + if ((_PS_MODE_DEMO_ && !$allowDemoMode) + || !$this->isDeleteGranted() + ) { + throw new Exception( + $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error') + ); + } + } + + /** + * Validate delete access. + * + * If access is not granted, the thrown Exception's error message contains the translated rejection message. + * + * @param bool $allowDemoMode Should we grant access in demo mode ? + * + * @throws Exception + */ + protected function validateDeleteAuthorization($allowDemoMode = false) + { + if ((_PS_MODE_DEMO_ && !$allowDemoMode) + || !$this->isDeleteGranted() + ) { + throw new Exception( + $this->trans('You do not have permission to delete this.', array(), 'Admin.Notifications.Error') + ); + } + } + + /** + * Validate add access. + * + * If access is not granted, the thrown Exception's error message contains the translated rejection message. + * + * @param bool $allowDemoMode Should we grant access in demo mode ? + * + * @throws Exception + */ + protected function validateAddAuthorization($allowDemoMode = false) + { + if ((_PS_MODE_DEMO_ && !$allowDemoMode) + || !$this->isAddGranted() + ) { + throw new Exception( + $this->trans('You do not have permission to add this.', array(), 'Admin.Notifications.Error') + ); + } + } + + /** + * Validate edit access. + * + * If access is not granted, the thrown Exception's error message contains the translated rejection message. + * + * @param bool $allowDemoMode Should we grant access in demo mode ? + * + * @throws Exception + */ + protected function validateEditAuthorization($allowDemoMode = false) + { + if ((_PS_MODE_DEMO_ && !$allowDemoMode) + || !$this->isEditGranted() + ) { + throw new Exception( + $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error') + ); + } + } + + /** + * Validate view access. + * + * If access is not granted, the thrown Exception's error message contains the translated rejection message. + * + * @param bool $allowDemoMode Should we grant access in demo mode ? + * + * @throws Exception + */ + protected function validateViewAuthorization($allowDemoMode = false) + { + if ((_PS_MODE_DEMO_ && !$allowDemoMode) + || !$this->isViewGranted() + ) { + throw new Exception( + $this->trans('You do not have permission to view this.', array(), 'Admin.Notifications.Error') + ); + } + } + + protected function validateUploadSize() + { + $post_max_size = Tools::getMaxUploadSize(); + if ($post_max_size + && isset($_SERVER['CONTENT_LENGTH']) + && $_SERVER['CONTENT_LENGTH'] + && $_SERVER['CONTENT_LENGTH'] > $post_max_size + ) { + throw new Exception($this->trans('The uploaded file is too large.', array(), 'Admin.Design.Notification')); + } + } + + /** + * Set redirect_after to same URL, but with an "&error" flag. + */ + protected function addErrorToRedirectAfter() + { + $this->redirect_after = self::$currentIndex . '&token=' . $this->token . '&error'; + } + + /** + * Specific postProcess for "exporttheme" action. + * + * @return bool false if access not granted + */ + protected function postProcessExportTheme() + { + try { + $this->validateAddAuthorization(true); + } catch (Exception $e) { + $this->errors[] = $this->trans( + 'You do not have permission to edit this.', + array(), + 'Admin.Notifications.Error' + ); + + return false; + } + + $exporter = SymfonyContainer::getInstance()->get('prestashop.core.addon.theme.exporter'); + $path = $exporter->export($this->context->shop->theme); + $this->confirmations[] = $this->trans( + 'Your theme has been correctly exported: %path%', + ['%path%' => $path], + 'Admin.Notifications.Success' + ); + + return true; + } + + /** + * Specific postProcess for "submitAddconfiguration" action. + */ + protected function postProcessSubmitAddConfiguration() + { + try { + $this->validateAddAuthorization(); + + if ($filename = Tools::getValue('theme_archive_server')) { + $path = _PS_ALL_THEMES_DIR_ . $filename; + $this->theme_manager->install($path); + } elseif ($filename = Tools::getValue('themearchive')) { + $path = _PS_ALL_THEMES_DIR_ . $filename; + $destination = $this->processUploadFile($path); + if (!empty($destination)) { + $this->theme_manager->install($destination); + @unlink($destination); + } + } elseif ($source = Tools::getValue('themearchiveUrl')) { + $this->theme_manager->install($source); + } + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + + if (empty($this->errors)) { + $this->redirect_after = $this->context->link->getAdminLink('AdminThemes'); + } + } + + /** + * Specific postProcess for "submitConfigureLayouts" action. + */ + protected function postProcessSubmitConfigureLayouts() + { + try { + $this->validateEditAuthorization(); + $this->processSubmitConfigureLayouts(); + $this->redirect_after = $this->context->link->getAdminLink('AdminThemes'); + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Specific postProcess for "enableTheme" action. + */ + protected function postProcessEnableTheme() + { + try { + $this->validateEditAuthorization(); + + $isThemeEnabled = $this->theme_manager->enable(Tools::getValue('theme_name')); + // get errors if theme wasn't enabled + if (!$isThemeEnabled) { + $this->errors[] = $this->theme_manager->getErrors(Tools::getValue('theme_name')); + } else { + Tools::clearSmartyCache(); + Tools::clearCache(); + $this->redirect_after = $this->context->link->getAdminLink('AdminThemes'); + } + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Specific postProcess for "deleteTheme" action. + */ + protected function postProcessDeleteTheme() + { + try { + $this->validateDeleteAuthorization(); + $this->theme_manager->uninstall(Tools::getValue('theme_name')); + $this->redirect_after = $this->context->link->getAdminLink('AdminThemes'); + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Specific postProcess for "submitGenerateRTL" action. + */ + protected function postProcessSubmitGenerateRTL() + { + try { + $this->validateEditAuthorization(); + + if ((bool) Tools::getValue('PS_GENERATE_RTL')) { + Language::getRtlStylesheetProcessor() + ->setProcessFOThemes(array(Tools::getValue('PS_THEMES_LIST'))) + ->setRegenerate(true) + ->process(); + + $this->confirmations[] = $this->trans( + 'Your RTL stylesheets has been generated successfully', + array(), + 'Admin.Design.Notification' + ); + } + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Specific postProcess for "resetToDefaults" action. + */ + protected function postProcessResetToDefaults() + { + try { + $this->validateEditAuthorization(); + if ($this->theme_manager->reset(Tools::getValue('theme_name'))) { + $this->confirmations[] = $this->trans( + 'Your theme has been correctly reset to its default settings. You may want to regenerate your images. See the Improve > Design > Images Settings screen for the \'Regenerate thumbnails\' button.', + array(), + 'Admin.Design.Notification' + ); + } + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * Specific postProcess for "submitOptionsconfiguration" action. + */ + protected function postProcessSubmitOptionsConfiguration() + { + try { + $this->validateEditAuthorization(); + + Configuration::updateValue('PS_IMG_UPDATE_TIME', time()); + + if (Tools::getValue('PS_LOGO')) { + $this->logo_uploader->updateHeader(); + } + if (Tools::getValue('PS_LOGO_MAIL')) { + $this->logo_uploader->updateMail(); + } + if (Tools::getValue('PS_LOGO_INVOICE')) { + $this->logo_uploader->updateInvoice(); + } + if (Tools::getValue('PS_FAVICON')) { + $this->logo_uploader->updateFavicon(); + $this->redirect_after = self::$currentIndex . '&token=' . $this->token; + } + + Hook::exec('actionAdminThemesControllerUpdate_optionsAfter'); + } catch (PrestaShopException $e) { + $this->errors[] = $e->getMessage(); + $this->addErrorToRedirectAfter(); + } catch (Exception $e) { + $this->errors[] = $e->getMessage(); + } + } +} diff --git a/controllers/admin/AdminTrackingController.php b/controllers/admin/AdminTrackingController.php new file mode 100644 index 00000000..2aa2f538 --- /dev/null +++ b/controllers/admin/AdminTrackingController.php @@ -0,0 +1,460 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Product|Category $object + */ +class AdminTrackingControllerCore extends AdminController +{ + public $bootstrap = true; + + /** @var HelperList */ + protected $_helper_list; + + public function postprocess() + { + if (Tools::getValue('id_product') && Tools::isSubmit('statusproduct')) { + $this->table = 'product'; + $this->identifier = 'id_product'; + $this->action = 'status'; + $this->className = 'Product'; + } elseif (Tools::getValue('id_category') && Tools::isSubmit('statuscategory')) { + $this->table = 'category'; + $this->identifier = 'id_category'; + $this->action = 'status'; + $this->className = 'Category'; + } + + $this->list_no_link = true; + + parent::postprocess(); + } + + public function initContent() + { + if ($id_category = Tools::getValue('id_category') && Tools::getIsset('viewcategory')) { + Tools::redirectAdmin($this->context->link->getAdminLink('AdminProducts') . '&id_category=' . (int) $id_category . '&viewcategory'); + } + + $this->_helper_list = new HelperList(); + + if (!Configuration::get('PS_STOCK_MANAGEMENT')) { + $this->warnings[] = $this->trans('List of products without available quantities for sale are not displayed because stock management is disabled.', array(), 'Admin.Catalog.Notification'); + } + + $methods = get_class_methods($this); + $tpl_vars['arrayList'] = array(); + foreach ($methods as $method_name) { + if (preg_match('#getCustomList(.+)#', $method_name, $matches)) { + $this->clearListOptions(); + $this->content .= call_user_func(array($this, $matches[0])); + } + } + $this->context->smarty->assign(array( + 'content' => $this->content, + )); + } + + public function getCustomListCategoriesEmpty() + { + $this->table = 'category'; + $this->list_id = 'empty_categories'; + $this->lang = true; + $this->className = 'Category'; + $this->identifier = 'id_category'; + $this->_orderBy = 'id_category'; + $this->_orderWay = 'DESC'; + $this->_list_index = 'index.php?controller=AdminCategories'; + $this->_list_token = Tools::getAdminTokenLite('AdminCategories'); + + $this->addRowAction('edit'); + $this->addRowAction('view'); + $this->addRowAction('delete'); + $this->addRowActionSkipList('delete', array((int) Configuration::get('PS_ROOT_CATEGORY'))); + $this->addRowActionSkipList('edit', array((int) Configuration::get('PS_ROOT_CATEGORY'))); + + $this->fields_list = (array( + 'id_category' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'description' => array('title' => $this->trans('Description', array(), 'Admin.Global'), 'callback' => 'getDescriptionClean'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs'), + )); + $this->clearFilters(); + + $this->_join = Shop::addSqlAssociation('category', 'a'); + $this->_filter = ' AND NOT EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'category_product` cp + WHERE a.`id_category` = cp.id_category + ) + AND a.`id_category` != ' . (int) Configuration::get('PS_ROOT_CATEGORY'); + $this->toolbar_title = $this->trans('List of empty categories:', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function getCustomListProductsAttributesNoStock() + { + if (!Configuration::get('PS_STOCK_MANAGEMENT')) { + return; + } + + $this->table = 'product'; + $this->list_id = 'no_stock_products_attributes'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->className = 'Product'; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + $this->show_toolbar = false; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs', 'filter_key' => 'a!active'), + ); + + $this->clearFilters(); + + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = 'AND EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product` p + ' . Product::sqlStock('p') . ' + WHERE a.id_product = p.id_product AND EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product_attribute` WHERE `' . _DB_PREFIX_ . 'product_attribute`.id_product = p.id_product + ) + AND IFNULL(stock.quantity, 0) <= 0 + )'; + $this->toolbar_title = $this->trans('List of products with combinations but without available quantities for sale:', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function getCustomListProductsNoStock() + { + if (!Configuration::get('PS_STOCK_MANAGEMENT')) { + return; + } + + $this->table = 'product'; + $this->list_id = 'no_stock_products'; + $this->className = 'Product'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->show_toolbar = false; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global')), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs', 'filter_key' => 'a!active'), + ); + $this->clearFilters(); + + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = 'AND EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product` p + ' . Product::sqlStock('p') . ' + WHERE a.id_product = p.id_product AND NOT EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product_attribute` pa WHERE pa.id_product = p.id_product + ) + AND IFNULL(stock.quantity, 0) <= 0 + )'; + + $this->toolbar_title = $this->trans('List of products without combinations and without available quantities for sale:', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function getCustomListProductsDisabled() + { + $this->table = 'product'; + $this->list_id = 'disabled_products'; + $this->className = 'Product'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->_filter = 'AND product_shop.`active` = 0'; + $this->show_toolbar = false; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + ); + + $this->clearFilters(); + + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->toolbar_title = $this->trans('List of disabled products', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function getCustomListProductsWithoutPhoto() + { + $this->table = 'product'; + $this->list_id = 'products_without_photo'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->className = 'Product'; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + $this->show_toolbar = false; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs'), + ); + $this->clearFilters(); + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = 'AND NOT EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'image` img + WHERE a.id_product = img.id_product + )'; + $this->toolbar_title = $this->trans('List of products without images', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(true); + } + + public function getCustomListProductsWithoutDescription() + { + $this->table = 'product'; + $this->list_id = 'products_without_description'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->className = 'Product'; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + $this->show_toolbar = false; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs'), + ); + $this->clearFilters(); + $defaultLanguage = new Language(Configuration::get('PS_LANG_DEFAULT')); + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = 'AND EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product_lang` pl + WHERE + a.id_product = pl.id_product AND + pl.id_lang = ' . (int) $defaultLanguage->id . ' AND + pl.id_shop = ' . (int) $this->context->shop->id . ' AND + description = "" AND description_short = "" + )'; + $this->toolbar_title = $this->trans('List of products without description', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(true); + } + + public function getCustomListProductsWithoutPrice() + { + $this->table = 'product'; + $this->list_id = 'products_without_price'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->className = 'Product'; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + $this->show_toolbar = false; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs'), + ); + $this->clearFilters(); + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = ' AND a.price = "0.000000" AND a.wholesale_price = "0.000000" AND NOT EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'specific_price` sp + WHERE a.id_product = sp.id_product + )'; + $this->toolbar_title = $this->trans('List of products without price', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function renderList($withPagination = false) + { + $paginationLimit = 20; + $this->processFilter(); + + if (!($this->fields_list && is_array($this->fields_list))) { + return false; + } + $this->getList($this->context->language->id, null, null, 0, $withPagination ? $paginationLimit : null); + + $helper = new HelperList(); + + // Empty list is ok + if (!is_array($this->_list)) { + $this->displayWarning($this->trans('Bad SQL query', array(), 'Admin.Notifications.Error') . '
' . htmlspecialchars($this->_list_error)); + + return false; + } + + $this->setHelperDisplay($helper); + if ($withPagination) { + $helper->_default_pagination = $paginationLimit; + $helper->_pagination = $this->_pagination; + } + $helper->tpl_vars = $this->tpl_list_vars; + $helper->tpl_delete_link_vars = $this->tpl_delete_link_vars; + + // For compatibility reasons, we have to check standard actions in class attributes + foreach ($this->actions_available as $action) { + if (!in_array($action, $this->actions) && isset($this->$action) && $this->$action) { + $this->actions[] = $action; + } + } + $helper->is_cms = $this->is_cms; + $list = $helper->generateList($this->_list, $this->fields_list); + + return $list; + } + + public function displayEnableLink($token, $id, $value, $active, $id_category = null, $id_product = null) + { + $this->_helper_list->currentIndex = $this->_list_index; + $this->_helper_list->identifier = $this->identifier; + $this->_helper_list->table = $this->table; + + return $this->_helper_list->displayEnableLink($this->_list_token, $id, $value, $active, $id_category, $id_product); + } + + public function displayDeleteLink($token, $id, $name = null) + { + $this->_helper_list->currentIndex = $this->_list_index; + $this->_helper_list->identifier = $this->identifier; + $this->_helper_list->table = $this->table; + + return $this->_helper_list->displayDeleteLink($this->_list_token, $id, $name); + } + + public function displayEditLink($token, $id, $name = null) + { + $this->_helper_list->currentIndex = $this->_list_index; + $this->_helper_list->identifier = $this->identifier; + $this->_helper_list->table = $this->table; + + return $this->_helper_list->displayEditLink($this->_list_token, $id, $name); + } + + protected function clearFilters() + { + if (Tools::isSubmit('submitResetempty_categories')) { + $this->processResetFilters('empty_categories'); + } + + if (Tools::isSubmit('submitResetno_stock_products_attributes')) { + $this->processResetFilters('no_stock_products_attributes'); + } + + if (Tools::isSubmit('submitResetno_stock_products')) { + $this->processResetFilters('no_stock_products'); + } + + if (Tools::isSubmit('submitResetdisabled_products')) { + $this->processResetFilters('disabled_products'); + } + + if (Tools::isSubmit('submitResetproducts_without_photo')) { + $this->processResetFilters('products_without_photo'); + } + if (Tools::isSubmit('submitResetproducts_without_description')) { + $this->processResetFilters('products_without_description'); + } + if (Tools::isSubmit('submitResetproducts_without_price')) { + $this->processResetFilters('products_without_price'); + } + } + + public function clearListOptions() + { + $this->table = ''; + $this->actions = array(); + $this->list_skip_actions = array(); + $this->lang = false; + $this->identifier = ''; + $this->_orderBy = ''; + $this->_orderWay = ''; + $this->_filter = ''; + $this->_group = ''; + $this->_where = ''; + $this->list_title = $this->trans('Product disabled', array(), 'Admin.Catalog.Feature'); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, Context::getContext()->shop->id); + } + + public static function getDescriptionClean($description) + { + return Tools::getDescriptionClean($description); + } +} diff --git a/controllers/admin/AdminTranslationsController.php b/controllers/admin/AdminTranslationsController.php new file mode 100644 index 00000000..7855c324 --- /dev/null +++ b/controllers/admin/AdminTranslationsController.php @@ -0,0 +1,3308 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +use PrestaShop\PrestaShop\Core\Cldr\Update; +use PrestaShop\PrestaShop\Core\Addon\Theme\ThemeManagerBuilder; +use PrestaShop\PrestaShop\Core\Addon\Theme\Theme; + +class AdminTranslationsControllerCore extends AdminController +{ + /** Name of theme by default */ + const DEFAULT_THEME_NAME = _PS_DEFAULT_THEME_NAME_; + const TEXTAREA_SIZED = 70; + + /** @var string : Link which list all pack of language */ + protected $link_lang_pack = 'http://i18n.prestashop.com/translations/%ps_version%/available_languages.json'; + + /** @var int : number of sentence which can be translated */ + protected $total_expression = 0; + + /** @var int : number of sentence which aren't translated */ + protected $missing_translations = 0; + + /** @var array : List of ISO code for all languages */ + protected $all_iso_lang = array(); + + /** @var array */ + protected $modules_translations = array(); + + /** @var array : List of folder which must be ignored */ + protected static $ignore_folder = array('.', '..', '.svn', '.git', '.htaccess', 'index.php'); + + /** @var array : List of content type accepted for translation mail file */ + protected static $content_type_accepted = array('txt', 'tpl', 'html'); + + /** @var array : List of theme by translation type : FRONT, BACK, ERRORS... */ + protected $translations_informations = array(); + + /** @var array : List of all languages */ + protected $languages; + + /** @var array : List of all themes */ + protected $themes; + + /** @var string : Directory of selected theme */ + protected $theme_selected; + + /** @var string : Name of translations type */ + protected $type_selected; + + /** @var Language object : Language for the selected language */ + protected $lang_selected; + + /** @var bool : Is true if number of var exceed the suhosin request or post limit */ + protected $post_limit_exceed = false; + + public function __construct() + { + $this->bootstrap = true; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->table = 'translations'; + + parent::__construct(); + + $this->link_lang_pack = str_replace('%ps_version%', _PS_VERSION_, $this->link_lang_pack); + + $this->themes = (new ThemeManagerBuilder($this->context, Db::getInstance())) + ->buildRepository() + ->getList(); + } + + /* + * Set the type which is selected + */ + public function setTypeSelected($type_selected) + { + $this->type_selected = $type_selected; + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + if (!is_null($this->type_selected)) { + $method_name = 'initForm' . $this->type_selected; + if (method_exists($this, $method_name)) { + $this->content = $this->initForm($method_name); + } else { + $this->errors[] = $this->trans('"%type%" does not exist.', array('%type%' => $this->type_selected), 'Admin.Notifications.Error'); + $this->content = $this->initMain(); + } + } else { + $this->content = $this->initMain(); + } + + $this->context->smarty->assign(array( + 'content' => $this->content, + )); + } + + /** + * This function create vars by default and call the good method for generate form. + * + * @param $method_name + * + * @return mixed Call the method $this->method_name() + */ + public function initForm($method_name) + { + // Create a title for each translation page + $title = $this->trans('%1$s (Language: %2$s, Theme: %3$s)', + array( + '%1$s' => (empty($this->translations_informations[$this->type_selected]['name']) ? false : $this->translations_informations[$this->type_selected]['name']), + '%2$s' => $this->lang_selected->name, + '%3$s' => $this->theme_selected ? $this->theme_selected : $this->trans('None', array(), 'Admin.Global'), + ), + 'Admin.International.Feature' + ); + + // Set vars for all forms + $this->tpl_view_vars = array( + 'lang' => $this->lang_selected->iso_code, + 'title' => $title, + 'type' => $this->type_selected, + 'theme' => $this->theme_selected, + 'post_limit_exceeded' => $this->post_limit_exceed, + 'url_submit' => self::$currentIndex . '&submitTranslations' . ucfirst($this->type_selected) . '=1&token=' . $this->token, + 'url_submit_installed_module' => self::$currentIndex . '&submitSelect' . ucfirst($this->type_selected) . '=1&token=' . $this->token, + 'toggle_button' => $this->displayToggleButton(), + 'textarea_sized' => AdminTranslationsControllerCore::TEXTAREA_SIZED, + ); + + // Call method initForm for a type + return $this->{$method_name}(); + } + + /** + * AdminController::initToolbar() override. + * + * @see AdminController::initToolbar() + */ + public function initToolbar() + { + $this->toolbar_btn['save-and-stay'] = array( + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save and stay', array(), 'Admin.Actions'), + ); + $this->toolbar_btn['save'] = array( + 'href' => '#', + 'desc' => $this->trans('Update translations', array(), 'Admin.International.Feature'), + ); + $this->toolbar_btn['cancel'] = array( + 'href' => self::$currentIndex . '&token=' . $this->token, + 'desc' => $this->trans('Cancel', array(), 'Admin.Actions'), + ); + } + + /** + * Generate the Main page. + */ + public function initMain() + { + if ( + !in_array( + $this->authorizationLevel(), + array( + AdminController::LEVEL_VIEW, + AdminController::LEVEL_EDIT, + AdminController::LEVEL_ADD, + AdminController::LEVEL_DELETE, + ) + ) + ) { + Tools::redirectAdmin(Context::getContext()->link->getAdminLink('AdminDashboard')); + } + + // Block add/update a language + $packsToInstall = array(); + $packsToUpdate = array(); + $token = Tools::getAdminToken('AdminLanguages' . (int) Tab::getIdFromClassName('AdminLanguages') . (int) $this->context->employee->id); + $arrayStreamContext = @stream_context_create(array('http' => array('method' => 'GET', 'timeout' => 8))); + + if ($langPacks = Tools::file_get_contents($this->link_lang_pack, false, $arrayStreamContext)) { + if ($langPacks != '' && $langPacks = json_decode($langPacks, true)) { + foreach ($langPacks as $locale => $langName) { + $langDetails = Language::getJsonLanguageDetails($locale); + if (!Language::isInstalledByLocale($locale)) { + $packsToInstall[$locale] = $langDetails['name']; + } else { + $packsToUpdate[$locale] = $langDetails['name']; + } + } + } + } + + $modules = array(); + foreach ($this->getListModules(true) as $module) { + $modules[$module->name] = array( + 'name' => $module->name, + 'displayName' => $module->displayName, + 'urlToTranslate' => !$module->isUsingNewTranslationSystem() ? $this->context->link->getAdminLink( + 'AdminTranslations', + true, + array(), + array( + 'type' => 'modules', + 'module' => $module->name, + ) + ) : '', + ); + } + + $this->tpl_view_vars = array( + 'theme_default' => self::DEFAULT_THEME_NAME, + 'theme_lang_dir' => _THEME_LANG_DIR_, + 'token' => $this->token, + 'languages' => $this->languages, + 'translations_type' => $this->translations_informations, + 'packs_to_install' => $packsToInstall, + 'packs_to_update' => $packsToUpdate, + 'url_submit' => self::$currentIndex . '&token=' . $this->token, + 'themes' => $this->themes, + 'modules' => $modules, + 'current_theme_name' => $this->context->shop->theme_name, + 'url_create_language' => 'index.php?controller=AdminLanguages&addlang&token=' . $token, + 'level' => $this->authorizationLevel(), + ); + + $this->toolbar_scroll = false; + $this->base_tpl_view = 'main.tpl'; + + $this->content .= $this->renderKpis(); + $this->content .= parent::renderView(); + + return $this->content; + } + + /** + * This method merge each arrays of modules translation in the array of modules translations. + */ + protected function getModuleTranslations() + { + global $_MODULE; + $name_var = (empty($this->translations_informations[$this->type_selected]['var']) ? false : $this->translations_informations[$this->type_selected]['var']); + + if (!isset($_MODULE) && !isset($GLOBALS[$name_var])) { + $GLOBALS[$name_var] = array(); + } elseif (isset($_MODULE)) { + if (is_array($GLOBALS[$name_var]) && is_array($_MODULE)) { + $GLOBALS[$name_var] = array_merge($GLOBALS[$name_var], $_MODULE); + } else { + $GLOBALS[$name_var] = $_MODULE; + } + } + } + + /** + * This method is only used by AdminTranslations::submitCopyLang(). + * + * It try to create folder in new theme. + * + * When a translation file is copied for a module, its translation key is wrong. + * We have to change the translation key and rewrite the file. + * + * @param string $dest file name + * + * @return bool + */ + protected function checkDirAndCreate($dest) + { + $bool = true; + + // To get only folder path + $path = dirname($dest); + + // If folder wasn't already added + // Do not use Tools::file_exists_cache because it changes over time! + if (!file_exists($path)) { + if (!mkdir($path, 0777, true)) { + $bool &= false; + $this->errors[] = $this->trans('Cannot create the folder "%folder%". Please check your directory writing permissions.', array('%folder%' => $path), 'Admin.International.Notification'); + } + } + + return $bool; + } + + /** + * Read the Post var and write the translation file. + * This method overwrites the old translation file. + * + * @param bool $override_file Set true if this file is a override + * + * @throws PrestaShopException + */ + protected function writeTranslationFile($override_file = false) + { + $type = Tools::toCamelCase($this->type_selected, true); + + if (isset($this->translations_informations[$this->type_selected])) { + $translation_informations = $this->translations_informations[$this->type_selected]; + } else { + return; + } + + if ($override_file) { + $file_path = $translation_informations['override']['dir'] . $translation_informations['override']['file']; + } else { + $file_path = $translation_informations['dir'] . $translation_informations['file']; + } + + if ($file_path && !file_exists($file_path)) { + if (!file_exists(dirname($file_path)) && !mkdir(dirname($file_path), 0777, true)) { + throw new PrestaShopException($this->trans('Directory "%folder%" cannot be created', array('%folder%' => dirname($file_path)), 'Admin.Notifications.Error')); + } elseif (!touch($file_path)) { + throw new PrestaShopException($this->trans('File "%file%" cannot be created', array('%file%' => $file_path), 'Admin.Notifications.Error')); + } + } + + $thm_name = str_replace('.', '', Tools::getValue('theme')); + $kpi_key = substr(strtoupper($thm_name . '_' . Tools::getValue('lang')), 0, 16); + + if ($fd = fopen($file_path, 'w')) { + // Get value of button save and stay + $save_and_stay = Tools::isSubmit('submitTranslations' . $type . 'AndStay'); + + // Unset all POST which are not translations + unset( + $_POST['submitTranslations' . $type], + $_POST['submitTranslations' . $type . 'AndStay'], + $_POST['lang'], + $_POST['token'], + $_POST['theme'], + $_POST['type'] + ); + + // Get all POST which aren't empty + $to_insert = array(); + foreach ($_POST as $key => $value) { + if (!empty($value)) { + $to_insert[$key] = $value; + } + } + + ConfigurationKPI::updateValue('FRONTOFFICE_TRANSLATIONS_EXPIRE', time()); + ConfigurationKPI::updateValue('TRANSLATE_TOTAL_' . $kpi_key, count($_POST)); + ConfigurationKPI::updateValue('TRANSLATE_DONE_' . $kpi_key, count($to_insert)); + + // translations array is ordered by key (easy merge) + ksort($to_insert); + $tab = $translation_informations['var']; + fwrite($fd, " $value) { + fwrite($fd, '$' . $tab . '[\'' . pSQL($key, true) . '\'] = \'' . pSQL($value, true) . '\';' . "\n"); + } + fwrite($fd, "\n?>"); + fclose($fd); + + // Redirect + if ($save_and_stay) { + $this->redirect(true); + } else { + $this->redirect(); + } + } else { + throw new PrestaShopException($this->trans('Cannot write this file: "%folder%"', array('%folder%' => $file_path), 'Admin.Notifications.Error')); + } + } + + public function submitCopyLang() + { + if (!($from_lang = Tools::getValue('fromLang')) || !($to_lang = Tools::getValue('toLang'))) { + $this->errors[] = $this->trans('You must select two languages in order to copy data from one to another.', array(), 'Admin.International.Notification'); + } elseif (!($from_theme = Tools::getValue('fromTheme')) || !($to_theme = Tools::getValue('toTheme'))) { + $this->errors[] = $this->trans('You must select two themes in order to copy data from one to another.', array(), 'Admin.International.Notification'); + } elseif (!Language::copyLanguageData(Language::getIdByIso($from_lang), Language::getIdByIso($to_lang))) { + $this->errors[] = $this->trans('An error occurred while copying data.', array(), 'Admin.International.Notification'); + } elseif ($from_lang == $to_lang && $from_theme == $to_theme) { + $this->errors[] = $this->trans('There is nothing to copy (same language and theme).', array(), 'Admin.International.Notification'); + } else { + $theme_exists = array('from_theme' => false, 'to_theme' => false); + foreach ($this->themes as $theme) { + if ($theme->getName() == $from_theme) { + $theme_exists['from_theme'] = true; + } + if ($theme->getName() == $to_theme) { + $theme_exists['to_theme'] = true; + } + } + if ($theme_exists['from_theme'] == false || $theme_exists['to_theme'] == false) { + $this->errors[] = $this->trans('Theme(s) not found', array(), 'Admin.International.Notification'); + } + } + if (count($this->errors)) { + return; + } + + $bool = true; + $items = Language::getFilesList($from_lang, $from_theme, $to_lang, $to_theme, false, false, true); + foreach ($items as $source => $dest) { + if (!$this->checkDirAndCreate($dest)) { + $this->errors[] = $this->trans('Impossible to create the directory "%folder%".', array('%folder%' => $dest), 'Admin.International.Notification'); + } elseif (!copy($source, $dest)) { + $this->errors[] = $this->trans('Impossible to copy "%source%" to "%dest%".', array('%source%' => $source, '%dest%' => $dest), 'Admin.International.Notification'); + } elseif (strpos($dest, 'modules') && basename($source) === $from_lang . '.php' && $bool !== false) { + if (!$this->changeModulesKeyTranslation($dest, $from_theme, $to_theme)) { + $this->errors[] = $this->trans('Impossible to translate "%dest%".', array('%dest%' => $dest), 'Admin.International.Notification'); + } + } + } + if (!count($this->errors)) { + $this->redirect(false, 14); + } + $this->errors[] = $this->trans('A part of the data has been copied but some of the language files could not be found.', array(), 'Admin.International.Notification'); + } + + /** + * Change the key translation to according it to theme name. + * + * @param string $path + * @param string $theme_from + * @param string $theme_to + * + * @return bool + */ + public function changeModulesKeyTranslation($path, $theme_from, $theme_to) + { + $content = file_get_contents($path); + $arr_replace = array(); + $bool_flag = true; + if (preg_match_all('#\$_MODULE\[\'([^\']+)\'\]#Ui', $content, $matches)) { + foreach ($matches[1] as $value) { + $arr_replace[$value] = str_replace($theme_from, $theme_to, $value); + } + $content = str_replace(array_keys($arr_replace), array_values($arr_replace), $content); + $bool_flag = (file_put_contents($path, $content) === false) ? false : true; + } + + return $bool_flag; + } + + public function exportTabs() + { + // Get name tabs by iso code + $tabs = Tab::getTabs($this->lang_selected->id); + + // Get name of the default tabs + $tabs_default_lang = Tab::getTabs(1); + + $tabs_default = array(); + foreach ($tabs_default_lang as $tab) { + $tabs_default[$tab['class_name']] = pSQL($tab['name']); + } + + // Create content + $content = " tabs are by default in Spanish + * 2) create a new language, say, Klingon => tabs are populated using the default, Spanish, tabs + * 3) export the Klingon language pack + * + * => Since you have not yet translated the tabs into Klingon, + * without the condition below, you would get tabs exported, but in Spanish. + * This would lead to a Klingon pack actually containing Spanish. + * + * This has caused many issues in the past, so, as a precaution, tabs from + * the default language are not exported. + * + */ + if ($tabs_default[$tab['class_name']] != pSQL($tab['name'])) { + $content .= "\n\$_TABS['" . $tab['class_name'] . "'] = '" . pSQL($tab['name']) . "';"; + } + } + } + $content .= "\n\nreturn \$_TABS;"; + + $dir = _PS_TRANSLATIONS_DIR_ . $this->lang_selected->iso_code . DIRECTORY_SEPARATOR; + $path = $dir . 'tabs.php'; + + // Check if tabs.php exists for the selected Iso Code + if (!Tools::file_exists_cache($dir)) { + if (!mkdir($dir, 0777, true)) { + throw new PrestaShopException('The file ' . $dir . ' cannot be created.'); + } + } + if (!file_put_contents($path, $content)) { + throw new PrestaShopException('File "' . $path . '" does not exist and cannot be created in ' . $dir); + } + if (!is_writable($path)) { + $this->displayWarning($this->trans('This file must be writable: %file%', array('%file%' => $path), 'Admin.Notifications.Error')); + } + } + + public function submitExportLang() + { + if ($this->lang_selected->iso_code && $this->theme_selected) { + $this->exportTabs(); + $items = array_flip(Language::getFilesList($this->lang_selected->iso_code, $this->theme_selected, false, false, false, false, true)); + $file_name = _PS_TRANSLATIONS_DIR_ . '/export/' . $this->lang_selected->iso_code . '.gzip'; + $gz = new Archive_Tar($file_name, true); + if ($gz->createModify($items, null, _PS_ROOT_DIR_)) { + ob_start(); + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Cache-Control: public'); + header('Content-Description: File Transfer'); + header('Content-type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $this->lang_selected->iso_code . '.gzip' . '"'); + header('Content-Transfer-Encoding: binary'); + ob_end_flush(); + readfile($file_name); + @unlink($file_name); + exit; + } + $this->errors[] = $this->trans('An error occurred while creating archive.', array(), 'Admin.International.Notification'); + } + $this->errors[] = $this->trans('Please select a language and a theme.', array(), 'Admin.International.Notification'); + } + + public static function checkAndAddMailsFiles($iso_code, $files_list) + { + if (Language::getIdByIso('en')) { + $default_language = 'en'; + } else { + $default_language = Language::getIsoById((int) Configuration::get('PS_LANG_DEFAULT')); + } + + if (!$default_language || !Validate::isLanguageIsoCode($default_language)) { + return false; + } + + // 1 - Scan mails files + $mails = array(); + if (Tools::file_exists_cache(_PS_MAIL_DIR_ . $default_language . '/')) { + $mails = scandir(_PS_MAIL_DIR_ . $default_language . '/', SCANDIR_SORT_NONE); + } + + $mails_new_lang = array(); + + // Get all email files + foreach ($files_list as $file) { + if (preg_match('#^mails\/([a-z0-9]+)\/#Ui', $file['filename'], $matches)) { + $slash_pos = strrpos($file['filename'], '/'); + $mails_new_lang[] = substr($file['filename'], -(strlen($file['filename']) - $slash_pos - 1)); + } + } + + // Get the difference + $arr_mails_needed = array_diff($mails, $mails_new_lang); + + // Add mails files + foreach ($arr_mails_needed as $mail_to_add) { + if (!in_array($mail_to_add, self::$ignore_folder)) { + @copy(_PS_MAIL_DIR_ . $default_language . '/' . $mail_to_add, _PS_MAIL_DIR_ . $iso_code . '/' . $mail_to_add); + } + } + + // 2 - Scan modules files + $modules = scandir(_PS_MODULE_DIR_, SCANDIR_SORT_NONE); + + $module_mail_en = array(); + $module_mail_iso_code = array(); + + foreach ($modules as $module) { + if (!in_array($module, self::$ignore_folder) && Tools::file_exists_cache(_PS_MODULE_DIR_ . $module . '/mails/' . $default_language . '/')) { + $arr_files = scandir(_PS_MODULE_DIR_ . $module . '/mails/' . $default_language . '/', SCANDIR_SORT_NONE); + + foreach ($arr_files as $file) { + if (!in_array($file, self::$ignore_folder)) { + if (Tools::file_exists_cache(_PS_MODULE_DIR_ . $module . '/mails/' . $default_language . '/' . $file)) { + $module_mail_en[] = _PS_MODULE_DIR_ . $module . '/mails/ISO_CODE/' . $file; + } + + if (Tools::file_exists_cache(_PS_MODULE_DIR_ . $module . '/mails/' . $iso_code . '/' . $file)) { + $module_mail_iso_code[] = _PS_MODULE_DIR_ . $module . '/mails/ISO_CODE/' . $file; + } + } + } + } + } + + // Get the difference in this modules + $arr_modules_mails_needed = array_diff($module_mail_en, $module_mail_iso_code); + + // Add mails files for this modules + foreach ($arr_modules_mails_needed as $file) { + $file_en = str_replace('ISO_CODE', $default_language, $file); + $file_iso_code = str_replace('ISO_CODE', $iso_code, $file); + $dir_iso_code = substr($file_iso_code, 0, -(strlen($file_iso_code) - strrpos($file_iso_code, '/') - 1)); + + if (!file_exists($dir_iso_code)) { + mkdir($dir_iso_code); + file_put_contents($dir_iso_code . '/index.php', Tools::getDefaultIndexContent()); + } + + if (Tools::file_exists_cache($file_en)) { + copy($file_en, $file_iso_code); + } + } + } + + /** + * Move theme translations in selected themes. + * + * @param array $files + * @param array $themes_selected + */ + public function checkAndAddThemesFiles($files, $themes_selected) + { + foreach ($files as $file) { + // Check if file is a file theme + if (preg_match('#^themes\/([a-z0-9]+)\/lang\/#Ui', $file['filename'], $matches)) { + $slash_pos = strrpos($file['filename'], '/'); + $name_file = substr($file['filename'], -(strlen($file['filename']) - $slash_pos - 1)); + $name_default_theme = $matches[1]; + $deleted_old_theme = false; + + // Get the old file theme + if (file_exists(_PS_THEME_DIR_ . 'lang/' . $name_file)) { + $theme_file_old = _PS_THEME_DIR_ . 'lang/' . $name_file; + } else { + $deleted_old_theme = true; + $theme_file_old = str_replace(self::DEFAULT_THEME_NAME, $name_default_theme, _PS_THEME_DIR_ . 'lang/' . $name_file); + } + + // Move the old file theme in the new folder + foreach ($themes_selected as $theme_name) { + if (file_exists($theme_file_old)) { + copy($theme_file_old, str_replace($name_default_theme, $theme_name, $theme_file_old)); + } + } + + if ($deleted_old_theme) { + @unlink($theme_file_old); + } + } + } + } + + /** + * Add new translations tabs by code ISO. + * + * @param array $iso_code + * @param array $files + * + * @return array + */ + public static function addNewTabs($iso_code, $files) + { + $errors = array(); + + foreach ($files as $file) { + // Check if file is a file theme + if (preg_match('#^translations\/' . $iso_code . '\/tabs.php#Ui', $file['filename'], $matches) && Validate::isLanguageIsoCode($iso_code)) { + // Include array width new translations tabs + $_TABS = array(); + clearstatcache(); + if (file_exists(_PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . $file['filename'])) { + include_once _PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . $file['filename']; + } + + if (is_array($_TABS) && count($_TABS)) { + foreach ($_TABS as $class_name => $translations) { + // Get instance of this tab by class name + $tab = Tab::getInstanceFromClassName($class_name); + //Check if class name exists + if (isset($tab->class_name) && !empty($tab->class_name)) { + $id_lang = Language::getIdByIso($iso_code, true); + $tab->name[(int) $id_lang] = $translations; + + // Do not crash at intall + if (!isset($tab->name[Configuration::get('PS_LANG_DEFAULT')])) { + $tab->name[(int) Configuration::get('PS_LANG_DEFAULT')] = $translations; + } + + if (!Validate::isGenericName($tab->name[(int) $id_lang])) { + $errors[] = Context::getContext()->getTranslator()->trans('Tab "%s" is not valid', array($tab->name[(int) $id_lang]), 'Admin.International.Notification'); + } else { + $tab->update(); + } + } + } + } + } + } + + return $errors; + } + + public static function checkTranslationFile($content) + { + $lines = array_map('trim', explode("\n", $content)); + $global = false; + foreach ($lines as $line) { + // PHP tags + if (in_array($line, array('', ''))) { + continue; + } + + // Global variable declaration + if (!$global && preg_match('/^global\s+\$([a-z0-9-_]+)\s*;$/i', $line, $matches)) { + $global = $matches[1]; + continue; + } + // Global variable initialization + if ($global != false && preg_match('/^\$' . preg_quote($global, '/') . '\s*=\s*array\(\s*\)\s*;$/i', $line)) { + continue; + } + + // Global variable initialization without declaration + if (!$global && preg_match('/^\$([a-z0-9-_]+)\s*=\s*array\(\s*\)\s*;$/i', $line, $matches)) { + $global = $matches[1]; + continue; + } + + // Assignation + if (preg_match('/^\$' . preg_quote($global, '/') . '\[\'' . _PS_TRANS_PATTERN_ . '\'\]\s*=\s*\'' . _PS_TRANS_PATTERN_ . '\'\s*;$/i', $line)) { + continue; + } + + // Sometimes the global variable is returned... + if (preg_match('/^return\s+\$' . preg_quote($global, '/') . '\s*;$/i', $line, $matches)) { + continue; + } + + return false; + } + + return true; + } + + public function submitImportLang() + { + if (!isset($_FILES['file']['tmp_name']) || !$_FILES['file']['tmp_name']) { + $this->errors[] = $this->trans('No file has been selected.', array(), 'Admin.Notifications.Error'); + } else { + $gz = new Archive_Tar($_FILES['file']['tmp_name'], true); + $filename = $_FILES['file']['name']; + $iso_code = str_replace(array('.tar.gz', '.gzip'), '', $filename); + + if (Validate::isLangIsoCode($iso_code)) { + $themes_selected = Tools::getValue('theme', array(self::DEFAULT_THEME_NAME)); + $files_list = AdminTranslationsController::filterTranslationFiles($gz->listContent()); + $files_paths = AdminTranslationsController::filesListToPaths($files_list); + + $uniqid = uniqid(); + $sandbox = _PS_CACHE_DIR_ . 'sandbox' . DIRECTORY_SEPARATOR . $uniqid . DIRECTORY_SEPARATOR; + if ($gz->extractList($files_paths, $sandbox)) { + foreach ($files_list as $file2check) { + //don't validate index.php, will be overwrite when extract in translation directory + if (pathinfo($file2check['filename'], PATHINFO_BASENAME) == 'index.php') { + continue; + } + + if (preg_match('@^[0-9a-z-_/\\\\]+\.php$@i', $file2check['filename'])) { + if (!@filemtime($sandbox . $file2check['filename']) || !AdminTranslationsController::checkTranslationFile(file_get_contents($sandbox . $file2check['filename']))) { + $this->errors[] = $this->trans('Validation failed for: %file%', array('%file%' => $file2check['filename']), 'Admin.International.Notification'); + } + } elseif (!preg_match('@mails[0-9a-z-_/\\\\]+\.(html|tpl|txt)$@i', $file2check['filename'])) { + $this->errors[] = $this->trans('Unidentified file found: %file%', array('%file%' => $file2check['filename']), 'Admin.International.Notification'); + } + } + Tools::deleteDirectory($sandbox, true); + } + + $i = 0; + $tmp_array = array(); + foreach ($files_paths as $files_path) { + $path = dirname($files_path); + if (is_dir(_PS_TRANSLATIONS_DIR_ . '../' . $path) && !is_writable(_PS_TRANSLATIONS_DIR_ . '../' . $path) && !in_array($path, $tmp_array)) { + $this->errors[] = (!$i++ ? $this->trans('The archive cannot be extracted.', array(), 'Admin.International.Notification') . ' ' : '') . $this->trans('The server does not have permissions for writing.', array(), 'Admin.Notifications.Error') . ' ' . $this->trans('Please check rights for %file%', array('%file%' => $path), 'Admin.Notifications.Error'); + $tmp_array[] = $path; + } + } + + if (count($this->errors)) { + return false; + } + + if ($error = $gz->extractList($files_paths, _PS_TRANSLATIONS_DIR_ . '../')) { + if (is_object($error) && !empty($error->message)) { + $this->errors[] = $this->trans('The archive cannot be extracted.', array(), 'Admin.International.Notification') . ' ' . $error->message; + } else { + foreach ($files_list as $file2check) { + if (pathinfo($file2check['filename'], PATHINFO_BASENAME) == 'index.php' && file_put_contents(_PS_TRANSLATIONS_DIR_ . '../' . $file2check['filename'], Tools::getDefaultIndexContent())) { + continue; + } + } + + // Clear smarty modules cache + Tools::clearCache(); + + if (Validate::isLanguageFileName($filename)) { + if (!Language::checkAndAddLanguage($iso_code)) { + $conf = 20; + } else { + // Reset cache + Language::loadLanguages(); + + AdminTranslationsController::checkAndAddMailsFiles($iso_code, $files_list); + $this->checkAndAddThemesFiles($files_list, $themes_selected); + $tab_errors = AdminTranslationsController::addNewTabs($iso_code, $files_list); + + if (count($tab_errors)) { + $this->errors += $tab_errors; + + return false; + } + } + } + + //fetch cldr datas for the new imported locale + $languageCode = explode('-', Language::getLanguageCodeByIso($iso_code)); + $cldrUpdate = new Update(_PS_TRANSLATIONS_DIR_); + $cldrUpdate->fetchLocale($languageCode[0] . '-' . strtoupper($languageCode[1])); + + /* + * @see AdminController::$_conf + */ + $this->redirect(false, (isset($conf) ? $conf : '15')); + } + } + $this->errors[] = $this->trans('The archive cannot be extracted.', array(), 'Admin.International.Notification'); + } else { + $this->errors[] = $this->trans('ISO CODE invalid "%iso_code%" for the following file: "%file%"', array('%iso_code%' => $iso_code, '%file%' => $filename), 'Admin.International.Notification'); + } + } + } + + /** + * Filter the translation files contained in a .gzip pack + * and return only the ones that we want. + * + * Right now the function only needs to check that + * the modules for which we want to add translations + * are present on the shop (installed or not). + * + * @param array $list Is the output of Archive_Tar::listContent() + * + * @return array + */ + public static function filterTranslationFiles($list) + { + $kept = array(); + foreach ($list as $file) { + if ('index.php' == basename($file['filename'])) { + continue; + } + if (preg_match('#^modules/([^/]+)/#', $file['filename'], $m)) { + if (is_dir(_PS_MODULE_DIR_ . $m[1])) { + $kept[] = $file; + } + } else { + $kept[] = $file; + } + } + + return $kept; + } + + /** + * Turn the list returned by + * AdminTranslationsController::filterTranslationFiles() + * into a list of paths that can be passed to + * Archive_Tar::extractList(). + * + * @param array $list + * + * @return array + */ + public static function filesListToPaths($list) + { + $paths = array(); + foreach ($list as $item) { + $paths[] = $item['filename']; + } + + return $paths; + } + + public function submitAddLang() + { + $languageDetails = Language::getJsonLanguageDetails(Tools::getValue('params_import_language')); + $isoCode = $languageDetails['iso_code']; + + if (Validate::isLangIsoCode($isoCode)) { + if ($success = Language::downloadAndInstallLanguagePack($isoCode, _PS_VERSION_, null, true)) { + Language::loadLanguages(); + Tools::clearAllCache(); + + $languageCode = explode('-', Language::getLanguageCodeByIso($isoCode)); + $cldrUpdate = new Update(_PS_TRANSLATIONS_DIR_); + $cldrUpdate->fetchLocale($languageCode[0] . '-' . Tools::strtoupper($languageCode[1])); + + /* + * @see AdminController::$_conf + */ + $this->redirect(false, '15'); + } elseif (is_array($success) && count($success) > 0) { + foreach ($success as $error) { + $this->errors[] = $error; + } + } + } + } + + /** + * This method check each file (tpl or php file), get its sentences to translate, + * compare with posted values and write in iso code translation file. + * + * @param string $file_name + * @param array $files + * @param string $theme_name + * @param string $module_name + * @param string|bool $dir + * + * @throws PrestaShopException + */ + protected function findAndWriteTranslationsIntoFile($file_name, $files, $theme_name, $module_name, $dir = false) + { + // These static vars allow to use file to write just one time. + static $cache_file = array(); + static $str_write = ''; + static $array_check_duplicate = array(); + + // Set file_name in static var, this allow to open and wright the file just one time + if (!isset($cache_file[$theme_name . '-' . $file_name])) { + $str_write = ''; + $cache_file[$theme_name . '-' . $file_name] = true; + if (!Tools::file_exists_cache(dirname($file_name))) { + mkdir(dirname($file_name), 0777, true); + } + if (!Tools::file_exists_cache($file_name)) { + file_put_contents($file_name, ''); + } + if (!is_writable($file_name)) { + throw new PrestaShopException( + $this->trans( + 'Cannot write to the theme\'s language file (%s). Please check writing permissions.', + array($file_name), + 'Admin.International.Notification' + ) + ); + } + + // this string is initialized one time for a file + $str_write .= "userParseFile($content, $this->type_selected, $type_file, $module_name); + + // Write each translation on its module file + $template_name = substr(basename($file), 0, -4); + + foreach ($matches as $key) { + if ($theme_name) { + $post_key = md5(strtolower($module_name) . '_' . strtolower($theme_name) . '_' . strtolower($template_name) . '_' . md5($key)); + $pattern = '\'<{' . strtolower($module_name) . '}' . strtolower($theme_name) . '>' . strtolower($template_name) . '_' . md5($key) . '\''; + } else { + $post_key = md5(strtolower($module_name) . '_' . strtolower($template_name) . '_' . md5($key)); + $pattern = '\'<{' . strtolower($module_name) . '}prestashop>' . strtolower($template_name) . '_' . md5($key) . '\''; + } + + if (array_key_exists($post_key, $_POST) && !in_array($pattern, $array_check_duplicate)) { + if ($_POST[$post_key] == '') { + continue; + } + $array_check_duplicate[] = $pattern; + $str_write .= '$_MODULE[' . $pattern . '] = \'' . pSQL(str_replace(array("\r\n", "\r", "\n"), ' ', $_POST[$post_key])) . '\';' . "\n"; + ++$this->total_expression; + } + } + } + } + + if (isset($cache_file[$theme_name . '-' . $file_name]) && $str_write != " $file) { + if ($file[0] === '.' || in_array(substr($file, 0, strrpos($file, '.')), $this->all_iso_lang)) { + unset($files[$key]); + } elseif ($type_clear === 'file' && !in_array(substr($file, strrpos($file, '.')), $arr_good_ext)) { + unset($files[$key]); + } elseif ($type_clear === 'directory' && (!is_dir($path . $file) || in_array($file, $arr_exclude))) { + unset($files[$key]); + } + } + + return $files; + } + + /** + * This method get translation for each files of a module, + * compare with global $_MODULES array and fill AdminTranslations::modules_translations array + * With key as English sentences and values as their iso code translations. + * + * @param array $files + * @param string $theme_name + * @param string $module_name + * @param string|bool $dir + */ + protected function findAndFillTranslations($files, $theme_name, $module_name, $dir = false) + { + $name_var = (empty($this->translations_informations[$this->type_selected]['var']) ? false : $this->translations_informations[$this->type_selected]['var']); + + // added for compatibility + $GLOBALS[$name_var] = array_change_key_case($GLOBALS[$name_var]); + + // Thank to this var similar keys are not duplicate + // in AndminTranslation::modules_translations array + // see below + $array_check_duplicate = array(); + foreach ($files as $file) { + if ((preg_match('/^(.*).tpl$/', $file) || preg_match('/^(.*).php$/', $file)) && Tools::file_exists_cache($file_path = $dir . $file)) { + // Get content for this file + $content = file_get_contents($file_path); + + // Module files can now be ignored by adding this string in a file + if (strpos($content, 'IGNORE_THIS_FILE_FOR_TRANSLATION') !== false) { + continue; + } + + // Get file type + $type_file = substr($file, -4) == '.tpl' ? 'tpl' : 'php'; + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected, $type_file, $module_name); + + // Write each translation on its module file + $template_name = substr(basename($file), 0, -4); + + foreach ($matches as $key) { + $md5_key = md5($key); + $module_key = '<{' . Tools::strtolower($module_name) . '}' . strtolower($theme_name) . '>' . Tools::strtolower($template_name) . '_' . $md5_key; + $default_key = '<{' . Tools::strtolower($module_name) . '}prestashop>' . Tools::strtolower($template_name) . '_' . $md5_key; + // to avoid duplicate entry + if (!in_array($module_key, $array_check_duplicate)) { + $array_check_duplicate[] = $module_key; + if (!isset($this->modules_translations[$theme_name][$module_name][$template_name][$key]['trad'])) { + ++$this->total_expression; + } + if ($theme_name && array_key_exists($module_key, $GLOBALS[$name_var])) { + $this->modules_translations[$theme_name][$module_name][$template_name][$key]['trad'] = html_entity_decode($GLOBALS[$name_var][$module_key], ENT_COMPAT, 'UTF-8'); + } elseif (array_key_exists($default_key, $GLOBALS[$name_var])) { + $this->modules_translations[$theme_name][$module_name][$template_name][$key]['trad'] = html_entity_decode($GLOBALS[$name_var][$default_key], ENT_COMPAT, 'UTF-8'); + } else { + $this->modules_translations[$theme_name][$module_name][$template_name][$key]['trad'] = ''; + ++$this->missing_translations; + } + $this->modules_translations[$theme_name][$module_name][$template_name][$key]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + } + } + } + } + + /** + * Get list of files which must be parsed by directory and by type of translations. + * + * @return array : list of files by directory + */ + public function getFileToParseByTypeTranslation() + { + $directories = array(); + + switch ($this->type_selected) { + case 'front': + $directories['php'] = array( + _PS_FRONT_CONTROLLER_DIR_ => scandir(_PS_FRONT_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/front/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/front/', SCANDIR_SORT_NONE), + _PS_CLASS_DIR_ . 'controller/' => array('FrontController.php'), + ); + + $directories['tpl'] = array(_PS_ALL_THEMES_DIR_ => scandir(_PS_ALL_THEMES_DIR_, SCANDIR_SORT_NONE)); + self::$ignore_folder[] = 'modules'; + $directories['tpl'] = array_merge($directories['tpl'], $this->listFiles(_PS_THEME_SELECTED_DIR_)); + if (isset($directories['tpl'][_PS_THEME_SELECTED_DIR_ . 'pdf/'])) { + unset($directories['tpl'][_PS_THEME_SELECTED_DIR_ . 'pdf/']); + } + + if (Tools::file_exists_cache(_PS_THEME_OVERRIDE_DIR_)) { + $directories['tpl'] = array_merge($directories['tpl'], $this->listFiles(_PS_THEME_OVERRIDE_DIR_)); + } + + break; + + case 'back': + $directories = array( + 'php' => array( + _PS_ADMIN_CONTROLLER_DIR_ => scandir(_PS_ADMIN_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/admin/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/admin/', SCANDIR_SORT_NONE), + _PS_CLASS_DIR_ . 'helper/' => scandir(_PS_CLASS_DIR_ . 'helper/', SCANDIR_SORT_NONE), + _PS_CLASS_DIR_ . 'controller/' => array('AdminController.php'), + _PS_CLASS_DIR_ => array('PaymentModule.php'), + ), + 'php-sf2' => array( + _PS_ROOT_DIR_ . '/src/' => Tools::scandir(_PS_ROOT_DIR_ . '/src/', 'php', '', true), + ), + 'tpl-sf2' => Tools::scandir(_PS_ROOT_DIR_ . '/src/PrestaShopBundle/Resources/views/', 'twig', '', true), + 'tpl' => $this->listFiles(_PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR . 'themes/'), + 'specific' => array( + _PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR => array(), + ), + ); + + // For translate the template which are overridden + if (file_exists(_PS_OVERRIDE_DIR_ . 'controllers' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'templates')) { + $directories['tpl'] = array_merge($directories['tpl'], $this->listFiles(_PS_OVERRIDE_DIR_ . 'controllers' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'templates')); + } + + break; + + case 'errors': + $directories['php'] = array( + _PS_ROOT_DIR_ => scandir(_PS_ROOT_DIR_, SCANDIR_SORT_NONE), + _PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR => scandir(_PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR, SCANDIR_SORT_NONE), + _PS_FRONT_CONTROLLER_DIR_ => scandir(_PS_FRONT_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_ADMIN_CONTROLLER_DIR_ => scandir(_PS_ADMIN_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/front/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/front/', SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/admin/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/admin/', SCANDIR_SORT_NONE), + ); + + // Get all files for folders classes/ and override/classes/ recursively + $directories['php'] = array_merge($directories['php'], $this->listFiles(_PS_CLASS_DIR_, array(), 'php')); + $directories['php'] = array_merge($directories['php'], $this->listFiles(_PS_OVERRIDE_DIR_ . 'classes/', array(), 'php')); + break; + + case 'fields': + $directories['php'] = $this->listFiles(_PS_CLASS_DIR_, array(), 'php'); + break; + + case 'pdf': + $tpl_theme = Tools::file_exists_cache(_PS_THEME_SELECTED_DIR_ . 'pdf/') ? scandir(_PS_THEME_SELECTED_DIR_ . 'pdf/', SCANDIR_SORT_NONE) : array(); + $directories = array( + 'php' => array( + _PS_CLASS_DIR_ . 'pdf/' => scandir(_PS_CLASS_DIR_ . 'pdf/', SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'classes/pdf/' => scandir(_PS_OVERRIDE_DIR_ . 'classes/pdf/', SCANDIR_SORT_NONE), + ), + 'tpl' => array( + _PS_PDF_DIR_ => scandir(_PS_PDF_DIR_, SCANDIR_SORT_NONE), + _PS_THEME_SELECTED_DIR_ . 'pdf/' => $tpl_theme, + ), + ); + $directories['tpl'] = array_merge($directories['tpl'], $this->getModulesHasPDF()); + $directories['php'] = array_merge($directories['php'], $this->getModulesHasPDF(true)); + break; + + case 'mails': + $directories['php'] = array( + _PS_FRONT_CONTROLLER_DIR_ => scandir(_PS_FRONT_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_ADMIN_CONTROLLER_DIR_ => scandir(_PS_ADMIN_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/front/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/front/', SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/admin/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/admin/', SCANDIR_SORT_NONE), + _PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR => scandir(_PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR, SCANDIR_SORT_NONE), + ); + + // Get all files for folders classes/ and override/classes/ recursively + $directories['php'] = array_merge($directories['php'], $this->listFiles(_PS_CLASS_DIR_, array(), 'php')); + $directories['php'] = array_merge($directories['php'], $this->listFiles(_PS_OVERRIDE_DIR_ . 'classes/', array(), 'php')); + $directories['php'] = array_merge($directories['php'], $this->getModulesHasMails()); + break; + } + + return $directories; + } + + /** + * This method parse a file by type of translation and type file. + * + * @param $content + * @param $type_translation : front, back, errors, modules... + * @param string|bool $type_file : (tpl|php) + * @param string $module_name : name of the module + * + * @return array + */ + protected function userParseFile($content, $type_translation, $type_file = false, $module_name = '') + { + switch ($type_translation) { + case 'front': + // Parsing file in Front office + if ($type_file == 'php') { + $regex = '/this->l\((\')' . _PS_TRANS_PATTERN_ . '\'[\)|\,]/U'; + } else { + $regex = '/\{l\s*s=([\'\"])' . _PS_TRANS_PATTERN_ . '\1(\s*sprintf=.*)?(\s*js=1)?\s*\}/U'; + } + break; + + case 'back': + // Parsing file in Back office + if ($type_file == 'php') { + $regex = '/this->l\((\')' . _PS_TRANS_PATTERN_ . '\'[\)|\,]/U'; + } elseif ($type_file == 'specific') { + $regex = '/Translate::getAdminTranslation\((\')' . _PS_TRANS_PATTERN_ . '\'(?:,.*)*\)/U'; + } else { + $regex = '/\{l\s*s\s*=([\'\"])' . _PS_TRANS_PATTERN_ . '\1(\s*sprintf=.*)?(\s*js=1)?(\s*slashes=1)?.*\}/U'; + } + break; + + case 'errors': + // Parsing file for all errors syntax + $regex = '/Tools::displayError\((\')' . _PS_TRANS_PATTERN_ . '\'(,\s*(.+))?\)/U'; + break; + + case 'modules': + // Parsing modules file + if ($type_file == 'php') { + $regex = '/->l\((\')' . _PS_TRANS_PATTERN_ . '\'(, ?\'(.+)\')?(, ?(.+))?\)/U'; + } else { + // In tpl file look for something that should contain mod='module_name' according to the documentation + $regex = '/\{l\s*s=([\'\"])' . _PS_TRANS_PATTERN_ . '\1.*\s+mod=\'' . $module_name . '\'.*\}/U'; + } + break; + + case 'pdf': + // Parsing PDF file + if ($type_file == 'php') { + $regex = array( + '/HTMLTemplate.*::l\((\')' . _PS_TRANS_PATTERN_ . '\'[\)|\,]/U', + '/->l\((\')' . _PS_TRANS_PATTERN_ . '\'(, ?\'(.+)\')?(, ?(.+))?\)/U', + ); + } else { + $regex = '/\{l\s*s=([\'\"])' . _PS_TRANS_PATTERN_ . '\1(\s*sprintf=.*)?(\s*js=1)?(\s*pdf=\'true\')?\s*\}/U'; + } + break; + } + + if (!is_array($regex)) { + $regex = array($regex); + } + + $strings = array(); + foreach ($regex as $regex_row) { + $matches = array(); + $n = preg_match_all($regex_row, $content, $matches); + for ($i = 0; $i < $n; ++$i) { + $quote = $matches[1][$i]; + $string = $matches[2][$i]; + + if ($quote === '"') { + // Escape single quotes because the core will do it when looking for the translation of this string + $string = str_replace('\'', '\\\'', $string); + // Unescape double quotes + $string = preg_replace('/\\\\+"/', '"', $string); + } + + $strings[] = $string; + } + } + + return array_unique($strings); + } + + /** + * Get all translations informations for all type of translations. + * + * array( + * 'type' => array( + * 'name' => string : title for the translation type, + * 'var' => string : name of var for the translation file, + * 'dir' => string : dir of translation file + * 'file' => string : file name of translation file + * ) + * ) + */ + public function getTranslationsInformations() + { + $this->translations_informations = array( + 'back' => array( + 'name' => $this->trans('Back office translations', array(), 'Admin.International.Feature'), + 'var' => '_LANGADM', + 'dir' => _PS_TRANSLATIONS_DIR_ . $this->lang_selected->iso_code . '/', + 'file' => 'admin.php', + 'sf_controller' => true, + 'choice_theme' => false, + ), + 'themes' => array( + 'name' => $this->trans('Themes translations', array(), 'Admin.International.Feature'), + 'var' => '_THEMES', + 'dir' => '', + 'file' => '', + 'sf_controller' => true, + 'choice_theme' => true, + ), + 'modules' => array( + 'name' => $this->trans('Installed modules translations', array(), 'Admin.International.Feature'), + 'var' => '_MODULES', + 'dir' => _PS_ROOT_DIR_ . '/modules/', + 'file' => '', + 'sf_controller' => true, + 'choice_theme' => false, + ), + 'mails' => array( + 'name' => $this->trans('Email translations', array(), 'Admin.International.Feature'), + 'var' => '_LANGMAIL', + 'dir' => _PS_MAIL_DIR_ . $this->lang_selected->iso_code . '/', + 'file' => 'lang.php', + 'sf_controller' => false, + 'choice_theme' => false, + ), + 'others' => array( + 'name' => $this->trans('Other translations', array(), 'Admin.International.Feature'), + 'var' => '_OTHERS', + 'dir' => '', + 'file' => '', + 'sf_controller' => true, + 'choice_theme' => false, + ), + ); + + if (defined('_PS_THEME_SELECTED_DIR_')) { + $this->translations_informations['modules']['override'] = array('dir' => _PS_THEME_SELECTED_DIR_ . 'modules/', 'file' => ''); + $this->translations_informations['mails']['override'] = array('dir' => _PS_THEME_SELECTED_DIR_ . 'mails/' . $this->lang_selected->iso_code . '/', 'file' => 'lang.php'); + } + } + + /** + * Get all informations on : languages, theme and the translation type. + */ + public function getInformations() + { + // Get all Languages + $this->languages = Language::getLanguages(false); + + // Get all iso_code of languages + foreach ($this->languages as $language) { + $this->all_iso_lang[] = $language['iso_code']; + } + + // Get folder name of theme + if (($theme = Tools::getValue('selected-theme')) && !is_array($theme)) { + $theme_exists = $this->theme_exists($theme); + if (!$theme_exists) { + throw new PrestaShopException($this->trans('Invalid theme "%theme%"', array('%theme%' => Tools::safeOutput($theme)), 'Admin.International.Notification')); + } + $this->theme_selected = Tools::safeOutput($theme); + } + + // Set the path of selected theme + if ($this->theme_selected) { + define('_PS_THEME_SELECTED_DIR_', _PS_ROOT_DIR_ . '/themes/' . $this->theme_selected . '/'); + } else { + define('_PS_THEME_SELECTED_DIR_', ''); + } + + // Get type of translation + if (($type = Tools::getValue('type')) && !is_array($type)) { + $this->type_selected = strtolower(Tools::safeOutput($type)); + } + + // Get selected language + if (Tools::getValue('lang') || Tools::getValue('iso_code')) { + $iso_code = Tools::getValue('lang') ? Tools::getValue('lang') : Tools::getValue('iso_code'); + + if (!Validate::isLangIsoCode($iso_code) || !in_array($iso_code, $this->all_iso_lang)) { + throw new PrestaShopException($this->trans('Invalid iso code "%iso_code%"', array('%iso_code%' => Tools::safeOutput($iso_code)), 'Admin.International.Notification')); + } + + $this->lang_selected = new Language((int) Language::getIdByIso($iso_code)); + } else { + $this->lang_selected = new Language((int) Language::getIdByIso('en')); + } + + // Get all information for translations + $this->getTranslationsInformations(); + } + + public function renderKpis() + { + $time = time(); + $kpis = array(); + + /* The data generation is located in AdminStatsControllerCore */ + + $helper = new HelperKpi(); + $helper->id = 'box-languages'; + $helper->icon = 'icon-microphone'; + $helper->color = 'color1'; + $helper->href = $this->context->link->getAdminLink('AdminLanguages'); + $helper->title = $this->trans('Enabled Languages', array(), 'Admin.International.Feature'); + if (ConfigurationKPI::get('ENABLED_LANGUAGES') !== false) { + $helper->value = ConfigurationKPI::get('ENABLED_LANGUAGES'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=enabled_languages'; + $helper->refresh = (bool) (ConfigurationKPI::get('ENABLED_LANGUAGES_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-country'; + $helper->icon = 'icon-home'; + $helper->color = 'color2'; + $helper->title = $this->trans('Main Country', array(), 'Admin.International.Feature'); + $helper->subtitle = $this->trans('30 Days', array(), 'Admin.Global'); + if (ConfigurationKPI::get('MAIN_COUNTRY', $this->context->language->id) !== false) { + $helper->value = ConfigurationKPI::get('MAIN_COUNTRY', $this->context->language->id); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=main_country'; + $helper->refresh = (bool) (ConfigurationKPI::get('MAIN_COUNTRY_EXPIRE', $this->context->language->id) < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-translations'; + $helper->icon = 'icon-list'; + $helper->color = 'color3'; + $helper->title = $this->trans('Front office Translations', array(), 'Admin.International.Feature'); + if (ConfigurationKPI::get('FRONTOFFICE_TRANSLATIONS') !== false) { + $helper->value = ConfigurationKPI::get('FRONTOFFICE_TRANSLATIONS'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=frontoffice_translations'; + $helper->refresh = (bool) (ConfigurationKPI::get('FRONTOFFICE_TRANSLATIONS_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpiRow(); + $helper->kpis = $kpis; + + return $helper->generate(); + } + + /** + * AdminController::postProcess() override. + * + * @see AdminController::postProcess() + */ + public function postProcess() + { + $this->getInformations(); + + /* PrestaShop demo mode */ + if (_PS_MODE_DEMO_) { + $this->errors[] = $this->trans('This functionality has been disabled.', array(), 'Admin.Notifications.Error'); + + return; + } + /* PrestaShop demo mode */ + + try { + if (Tools::isSubmit('submitCopyLang')) { + if ($this->access('add')) { + $this->submitCopyLang(); + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitExport')) { + if ($this->access('add')) { + $this->submitExportLang(); + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitImport')) { + if ($this->access('add')) { + $this->submitImportLang(); + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitAddLanguage')) { + if ($this->access('add')) { + $this->submitAddLang(); + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitTranslationsPdf')) { + if ($this->access('edit')) { + // Only the PrestaShop team should write the translations into the _PS_TRANSLATIONS_DIR_ + if (!$this->theme_selected) { + $this->writeTranslationFile(); + } else { + $this->writeTranslationFile(true); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitTranslationsBack') || Tools::isSubmit('submitTranslationsErrors') || Tools::isSubmit('submitTranslationsFields') || Tools::isSubmit('submitTranslationsFront')) { + if ($this->access('edit')) { + $this->writeTranslationFile(); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitTranslationsMails') || Tools::isSubmit('submitTranslationsMailsAndStay')) { + if ($this->access('edit')) { + $this->submitTranslationsMails(); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitTranslationsModules')) { + if ($this->access('edit')) { + // Get list of modules + if ($modules = $this->getListModules()) { + // Get files of all modules + $arr_files = $this->getAllModuleFiles($modules, null, $this->lang_selected->iso_code, true); + + // Find and write all translation modules files + foreach ($arr_files as $value) { + $this->findAndWriteTranslationsIntoFile($value['file_name'], $value['files'], $value['theme'], $value['module'], $value['dir']); + } + + // Clear modules cache + Tools::clearAllCache(); + + // Redirect + if (Tools::getIsset('submitTranslationsModulesAndStay')) { + $this->redirect(true); + } else { + $this->redirect(); + } + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', array(), 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitSelectModules')) { + $this->redirect(false, false, true); + } + } catch (PrestaShopException $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * This method redirect in the translation main page or in the translation page. + * + * @param bool $save_and_stay : true if the user has clicked on the button "save and stay" + * @param bool $conf : id of confirmation message + * @param bool $modify_translation : true if the user has clicked on the button "Modify translation" + */ + protected function redirect($save_and_stay = false, $conf = false, $modify_translation = false) + { + $conf = !$conf ? 4 : $conf; + $url_base = self::$currentIndex . '&token=' . $this->token . '&conf=' . $conf; + if ($modify_translation) { + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token . '&lang=' . Tools::getValue('langue') . '&type=' . $this->type_selected . '&module=' . Tools::getValue('module') . '&theme=' . $this->theme_selected); + } elseif ($save_and_stay) { + Tools::redirectAdmin($url_base . '&lang=' . $this->lang_selected->iso_code . '&type=' . $this->type_selected . '&module=' . Tools::getValue('module') . '&theme=' . $this->theme_selected); + } else { + Tools::redirectAdmin($url_base . '&action=settings'); + } + } + + protected function getMailPattern() + { + Tools::displayAsDeprecated('Email pattern is no longer used, emails are always saved like they are.'); + // Let the indentation like it. + return ' + + + + + + #content + +'; + } + + /** + * This method is used to write translation for mails. + * This writes subject translation files + * (in root/mails/lang_choosen/lang.php or root/_PS_THEMES_DIR_/mails/lang_choosen/lang.php) + * and mails files. + */ + protected function submitTranslationsMails() + { + $arr_mail_content = array(); + $arr_mail_path = array(); + + if (Tools::getValue('core_mail')) { + $arr_mail_content['core_mail'] = Tools::getValue('core_mail'); + + // Get path of directory for find a good path of translation file + if (!$this->theme_selected) { + $arr_mail_path['core_mail'] = $this->translations_informations[$this->type_selected]['dir']; + } else { + $arr_mail_path['core_mail'] = $this->translations_informations[$this->type_selected]['override']['dir']; + } + } + + if (Tools::getValue('module_mail')) { + $arr_mail_content['module_mail'] = Tools::getValue('module_mail'); + + // Get path of directory for find a good path of translation file + if (!$this->theme_selected) { + $arr_mail_path['module_mail'] = $this->translations_informations['modules']['dir'] . '{module}/mails/' . $this->lang_selected->iso_code . '/'; + } else { + $arr_mail_path['module_mail'] = $this->translations_informations['modules']['override']['dir'] . '{module}/mails/' . $this->lang_selected->iso_code . '/'; + } + } + + // Save each mail content + foreach ($arr_mail_content as $group_name => $all_content) { + foreach ($all_content as $type_content => $mails) { + if (!in_array($type_content, self::$content_type_accepted)) { + throw new PrestaShopException($this->trans('This %type_content% file extension is not accepted.', array('%type_content%' => $type_content), 'Admin.International.Notification')); + } + + foreach ($mails as $mail_name => $content) { + $module_name = false; + $module_name_pipe_pos = stripos($mail_name, '|'); + if ($module_name_pipe_pos) { + $module_name = substr($mail_name, 0, $module_name_pipe_pos); + if (!Validate::isModuleName($module_name)) { + throw new PrestaShopException($this->trans('Invalid module name "%module%"', array('%module%' => Tools::safeOutput($module_name)), 'Admin.International.Notification')); + } + $mail_name = substr($mail_name, $module_name_pipe_pos + 1); + if (!Validate::isTplName($mail_name)) { + throw new PrestaShopException($this->trans('Invalid mail name "%mail%"', array('%mail%' => Tools::safeOutput($mail_name)), 'Admin.International.Notification')); + } + } + + if ($type_content == 'html') { + $content = Tools::htmlentitiesUTF8($content); + $content = htmlspecialchars_decode($content); + // replace correct end of line + $content = str_replace("\r\n", PHP_EOL, $content); + + // Magic Quotes shall... not.. PASS! + if (_PS_MAGIC_QUOTES_GPC_) { + $content = stripslashes($content); + } + } + + if (Validate::isCleanHTML($content)) { + $path = $arr_mail_path[$group_name]; + if ($module_name) { + $path = str_replace('{module}', $module_name, $path); + } + if (!file_exists($path) && !mkdir($path, 0777, true)) { + throw new PrestaShopException($this->trans('Directory "%folder%" cannot be created', array('%folder%' => dirname($path)), 'Admin.International.Notification')); + } + + if ($type_content == 'tpl') { + preg_match('/{\s*[^$]+/s', $content, $matches); + if (!empty($matches)) { + throw new PrestaShopException($this->trans('Your email translations contain some invalid HTML and cannot be saved. Please check your content.', array(), 'Admin.International.Notification')); + } + } + + file_put_contents($path . $mail_name . '.' . $type_content, $content); + } else { + throw new PrestaShopException($this->trans('Your HTML email templates cannot contain JavaScript code.', array(), 'Admin.International.Notification')); + } + } + } + } + + // Update subjects + $array_subjects = array(); + if (($subjects = Tools::getValue('subject')) && is_array($subjects)) { + $array_subjects['core_and_modules'] = array('translations' => array(), 'path' => $arr_mail_path['core_mail'] . 'lang.php'); + foreach ($subjects as $subject_translation) { + $array_subjects['core_and_modules']['translations'] = array_merge($array_subjects['core_and_modules']['translations'], $subject_translation); + } + } + if (!empty($array_subjects)) { + foreach ($array_subjects as $infos) { + $this->writeSubjectTranslationFile($infos['translations'], $infos['path']); + } + } + + if (Tools::isSubmit('submitTranslationsMailsAndStay')) { + $this->redirect(true); + } else { + $this->redirect(); + } + } + + /** + * Include file $dir/$file and return the var $var declared in it. + * This create the file if not exists. + * + * return array : translations + */ + public function fileExists() + { + $var = $this->translations_informations[$this->type_selected]['var']; + $dir = $this->translations_informations[$this->type_selected]['dir']; + $file = $this->translations_informations[$this->type_selected]['file']; + + $$var = array(); + if (!Tools::file_exists_cache($dir)) { + if (!mkdir($dir, 0700)) { + throw new PrestaShopException('Directory ' . $dir . ' cannot be created.'); + } + } + if (!Tools::file_exists_cache($dir . DIRECTORY_SEPARATOR . $file)) { + if (!file_put_contents($dir . '/' . $file, "")) { + throw new PrestaShopException('File "' . $file . '" doesn\'t exists and cannot be created in ' . $dir); + } + } + if (!is_writable($dir . DIRECTORY_SEPARATOR . $file)) { + $this->displayWarning($this->trans('This file must be writable:', array(), 'Admin.Notifications.Error') . ' ' . $dir . '/' . $file); + } + include $dir . DIRECTORY_SEPARATOR . $file; + + return $$var; + } + + public function displayToggleButton($closed = false) + { + $str_output = ' + + '; + + return $str_output; + } + + public function displayLimitPostWarning($count) + { + $return = array(); + if ((ini_get('suhosin.post.max_vars') && ini_get('suhosin.post.max_vars') < $count) || (ini_get('suhosin.request.max_vars') && ini_get('suhosin.request.max_vars') < $count)) { + $return['error_type'] = 'suhosin'; + $return['post.max_vars'] = ini_get('suhosin.post.max_vars'); + $return['request.max_vars'] = ini_get('suhosin.request.max_vars'); + $return['needed_limit'] = $count + 100; + } elseif (ini_get('max_input_vars') && ini_get('max_input_vars') < $count) { + $return['error_type'] = 'conf'; + $return['max_input_vars'] = ini_get('max_input_vars'); + $return['needed_limit'] = $count + 100; + } + + return $return; + } + + /** + * Find sentence which use %d, %s, %%, %1$d, %1$s... + * + * @param $key : english sentence + * + * @return array|bool return list of matches + */ + public function checkIfKeyUseSprintf($key) + { + if (preg_match_all('#(?:%%|%(?:[0-9]+\$)?[+-]?(?:[ 0]|\'.)?-?[0-9]*(?:\.[0-9]+)?[bcdeufFosxX])#', $key, $matches)) { + return implode(', ', $matches[0]); + } + + return false; + } + + /** + * This method generate the form for front translations. + */ + public function initFormFront() + { + if (!$this->theme_exists(Tools::getValue('theme'))) { + $this->errors[] = $this->trans('Invalid theme "%theme%"', array('%theme%' => Tools::getValue('theme')), 'Admin.International.Notification'); + + return; + } + + $missing_translations_front = array(); + $name_var = $this->translations_informations[$this->type_selected]['var']; + $GLOBALS[$name_var] = $this->fileExists(); + + /* List templates to parse */ + $files_by_directory = $this->getFileToParseByTypeTranslation(); + $count = 0; + $tabs_array = array(); + foreach ($files_by_directory as $file_type => $root_directory) { + foreach ($root_directory as $dir => $files) { + $prefix = ''; + if ($dir == _PS_THEME_OVERRIDE_DIR_) { + $prefix = 'override_'; + } + + foreach ($files as $file) { + if (preg_match('/^(.*).(tpl|php)$/', $file) && (Tools::file_exists_cache($file_path = $dir . $file))) { + $prefix_key = $prefix . substr(basename($file), 0, -4); + $new_lang = array(); + + // Get content for this file + $content = file_get_contents($file_path); + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected, $file_type); + + /* Get string translation */ + foreach ($matches as $key) { + if (empty($key)) { + $this->errors[] = $this->trans('Empty string found, please edit: "%file%"', array('%file%' => $file_path), 'Admin.International.Notification'); + $new_lang[$key] = ''; + } else { + // Caution ! front has underscore between prefix key and md5, back has not + if (isset($GLOBALS[$name_var][$prefix_key . '_' . md5($key)])) { + $new_lang[$key]['trad'] = stripslashes(html_entity_decode($GLOBALS[$name_var][$prefix_key . '_' . md5($key)], ENT_COMPAT, 'UTF-8')); + } else { + if (!isset($new_lang[$key]['trad'])) { + $new_lang[$key]['trad'] = ''; + if (!isset($missing_translations_front[$prefix_key])) { + $missing_translations_front[$prefix_key] = 1; + } else { + ++$missing_translations_front[$prefix_key]; + } + } + } + $new_lang[$key]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + } + + if (isset($tabs_array[$prefix_key])) { + $tabs_array[$prefix_key] = array_merge($tabs_array[$prefix_key], $new_lang); + } else { + $tabs_array[$prefix_key] = $new_lang; + } + + $count += count($new_lang); + } + } + } + } + + $this->tpl_view_vars = array_merge($this->tpl_view_vars, array( + 'missing_translations' => $missing_translations_front, + 'count' => $count, + 'cancel_url' => $this->context->link->getAdminLink('AdminTranslations'), + 'limit_warning' => $this->displayLimitPostWarning($count), + 'mod_security_warning' => Tools::apacheModExists('mod_security'), + 'tabsArray' => $tabs_array, + )); + + $this->initToolbar(); + $this->base_tpl_view = 'translation_form.tpl'; + + return parent::renderView(); + } + + /** + * This method generate the form for back translations. + */ + public function initFormBack() + { + $name_var = $this->translations_informations[$this->type_selected]['var']; + $GLOBALS[$name_var] = $this->fileExists(); + $missing_translations_back = array(); + + // Get all types of file (PHP, TPL...) and a list of files to parse by folder + $files_per_directory = $this->getFileToParseByTypeTranslation(); + + //Parse SF2 php files + $regexSf2Php = [ + '/->trans\(([\'\"])' . _PS_TRANS_PATTERN_ . '([\'\"])(,\s*?[\[|array\(](.*)[\]|\)])(,\s*?([\'\"])(.*)([\'\"]))?\)/Us', + '/->transchoice\(([\'\"])' . _PS_TRANS_PATTERN_ . '([\'\"])(,\s*?(.*))(,\s*?[\[|array\(](.*)[\]|\)])(,\s*?([\'\"])(.*)([\'\"]))?\)/Us', + ]; + + foreach ($files_per_directory['php-sf2'] as $dir => $files) { + foreach ($files as $file) { + // Get content for this file + $content = file_get_contents($dir . $file); + if (!$content) { + continue; + } + + // Parse this content + foreach ($regexSf2Php as $reg) { + preg_match_all($reg, $content, $matches); + foreach ($matches[0] as $key => $match) { + $domainKey = strpos($match, 'trans(') !== false ? 8 : 10; + $stringToTranslate = $matches[2][$key]; + $prefix_key = $matches[$domainKey][$key]; + + if ($prefix_key && $stringToTranslate) { + if (isset($GLOBALS[$name_var][$prefix_key . md5($stringToTranslate)])) { + $tabs_array[$prefix_key][$stringToTranslate]['trad'] = stripslashes(html_entity_decode($GLOBALS[$name_var][$prefix_key . md5($stringToTranslate)], ENT_COMPAT, 'UTF-8')); + } else { + if (!isset($tabs_array[$prefix_key][$stringToTranslate]['trad'])) { + $tabs_array[$prefix_key][$stringToTranslate]['trad'] = ''; + if (!isset($missing_translations_back[$prefix_key])) { + $missing_translations_back[$prefix_key] = 1; + } else { + ++$missing_translations_back[$prefix_key]; + } + } + } + $tabs_array[$prefix_key][$stringToTranslate]['use_sprintf'] = $this->checkIfKeyUseSprintf($stringToTranslate); + } + } + } + } + } + + //Parse SF2/Twig files + $regexSf2Tpl = [ + '/trans\(([\'\"])' . _PS_TRANS_PATTERN_ . '([\'\"])(,\s*?[\{\[](.*)[\}\]])(,\s*?([\'\"])(.*)([\'\"]))?\)/Us', + '/transchoice\(([\'\"])' . _PS_TRANS_PATTERN_ . '([\'\"])(,\s*?(.*))(,\s*?[\{\[](.*)[\}\]])(,\s*?([\'\"])(.*)([\'\"]))?\)/Us', + ]; + + foreach ($files_per_directory['tpl-sf2'] as $file) { + // Get content for this file + $content = file_get_contents(_PS_ROOT_DIR_ . '/src/PrestaShopBundle/Resources/views/' . $file); + if (!$content) { + continue; + } + + // Parse this content + foreach ($regexSf2Tpl as $reg) { + preg_match_all($reg, $content, $matches); + foreach ($matches[0] as $key => $match) { + $domainKey = strpos($match, 'trans(') !== false ? 8 : 10; + $stringToTranslate = $matches[2][$key]; + $prefix_key = $matches[$domainKey][$key]; + + if ($prefix_key && $stringToTranslate) { + if (isset($GLOBALS[$name_var][$prefix_key . md5($stringToTranslate)])) { + $tabs_array[$prefix_key][$stringToTranslate]['trad'] = stripslashes(html_entity_decode($GLOBALS[$name_var][$prefix_key . md5($stringToTranslate)], ENT_COMPAT, 'UTF-8')); + } else { + if (!isset($tabs_array[$prefix_key][$stringToTranslate]['trad'])) { + $tabs_array[$prefix_key][$stringToTranslate]['trad'] = ''; + if (!isset($missing_translations_back[$prefix_key])) { + $missing_translations_back[$prefix_key] = 1; + } else { + ++$missing_translations_back[$prefix_key]; + } + } + } + $tabs_array[$prefix_key][$stringToTranslate]['use_sprintf'] = $this->checkIfKeyUseSprintf($stringToTranslate); + } + } + } + } + + //Parse ps PHP files + foreach ($files_per_directory['php'] as $dir => $files) { + foreach ($files as $file) { + // Check if is a PHP file and if the override file exists + if (preg_match('/^(.*)\.php$/', $file) && Tools::file_exists_cache($file_path = $dir . $file) && !in_array($file, self::$ignore_folder)) { + $prefix_key = basename($file); + // -4 becomes -14 to remove the ending "Controller.php" from the filename + if (strpos($file, 'Controller.php') !== false) { + $prefix_key = basename(substr($file, 0, -14)); + } elseif (strpos($file, 'Helper') !== false) { + $prefix_key = 'Helper'; + } + + if ($prefix_key == 'Admin') { + $prefix_key = 'AdminController'; + } + + if ($prefix_key == 'PaymentModule.php') { + $prefix_key = 'PaymentModule'; + } + + // Get content for this file + $content = file_get_contents($file_path); + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected, 'php'); + + foreach ($matches as $key) { + // Caution ! front has underscore between prefix key and md5, back has not + if (isset($GLOBALS[$name_var][$prefix_key . md5($key)])) { + $tabs_array[$prefix_key][$key]['trad'] = stripslashes(html_entity_decode($GLOBALS[$name_var][$prefix_key . md5($key)], ENT_COMPAT, 'UTF-8')); + } else { + if (!isset($tabs_array[$prefix_key][$key]['trad'])) { + $tabs_array[$prefix_key][$key]['trad'] = ''; + if (!isset($missing_translations_back[$prefix_key])) { + $missing_translations_back[$prefix_key] = 1; + } else { + ++$missing_translations_back[$prefix_key]; + } + } + } + $tabs_array[$prefix_key][$key]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + } + } + } + + foreach ($files_per_directory['specific'] as $dir => $files) { + foreach ($files as $file) { + if (Tools::file_exists_cache($file_path = $dir . $file) && !in_array($file, self::$ignore_folder)) { + $prefix_key = 'index'; + + // Get content for this file + $content = file_get_contents($file_path); + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected, 'specific'); + + foreach ($matches as $key) { + // Caution ! front has underscore between prefix key and md5, back has not + if (isset($GLOBALS[$name_var][$prefix_key . md5($key)])) { + $tabs_array[$prefix_key][$key]['trad'] = stripslashes(html_entity_decode($GLOBALS[$name_var][$prefix_key . md5($key)], ENT_COMPAT, 'UTF-8')); + } else { + if (!isset($tabs_array[$prefix_key][$key]['trad'])) { + $tabs_array[$prefix_key][$key]['trad'] = ''; + if (!isset($missing_translations_back[$prefix_key])) { + $missing_translations_back[$prefix_key] = 1; + } else { + ++$missing_translations_back[$prefix_key]; + } + } + } + $tabs_array[$prefix_key][$key]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + } + } + } + + foreach ($files_per_directory['tpl'] as $dir => $files) { + foreach ($files as $file) { + if (preg_match('/^(.*).tpl$/', $file) && Tools::file_exists_cache($file_path = $dir . $file)) { + // get controller name instead of file name + $prefix_key = Tools::toCamelCase(str_replace(_PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR . 'themes', '', $file_path), true); + $pos = strrpos($prefix_key, DIRECTORY_SEPARATOR); + $tmp = substr($prefix_key, 0, $pos); + + if (preg_match('#controllers#', $tmp)) { + $parent_class = explode(DIRECTORY_SEPARATOR, str_replace('/', DIRECTORY_SEPARATOR, $tmp)); + $override = array_search('override', $parent_class); + if ($override !== false) { + // case override/controllers/admin/templates/controller_name + $prefix_key = 'Admin' . ucfirst($parent_class[$override + 4]); + } else { + // case admin_name/themes/theme_name/template/controllers/controller_name + $key = array_search('controllers', $parent_class); + $prefix_key = 'Admin' . ucfirst($parent_class[$key + 1]); + } + } else { + $prefix_key = 'Admin' . ucfirst(substr($tmp, strrpos($tmp, DIRECTORY_SEPARATOR) + 1, $pos)); + } + + // Adding list, form, option in Helper Translations + $list_prefix_key = array('AdminHelpers', 'AdminList', 'AdminView', 'AdminOptions', 'AdminForm', + 'AdminCalendar', 'AdminTree', 'AdminUploader', 'AdminDataviz', 'AdminKpi', 'AdminModule_list', 'AdminModulesList', ); + if (in_array($prefix_key, $list_prefix_key)) { + $prefix_key = 'Helper'; + } + + // Adding the folder backup/download/ in AdminBackup Translations + if ($prefix_key == 'AdminDownload') { + $prefix_key = 'AdminBackup'; + } + + // use the prefix "AdminController" (like old php files 'header', 'footer.inc', 'index', 'login', 'password', 'functions' + if ($prefix_key == 'Admin' || $prefix_key == 'AdminTemplate') { + $prefix_key = 'AdminController'; + } + + $new_lang = array(); + + // Get content for this file + $content = file_get_contents($file_path); + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected, 'tpl'); + + /* Get string translation for each tpl file */ + foreach ($matches as $english_string) { + if (empty($english_string)) { + $this->errors[] = $this->trans('There is an error in template, an empty string has been found. Please edit: "%file%"', array('%file%' => $file_path), 'Admin.International.Notification'); + $new_lang[$english_string] = ''; + } else { + $trans_key = $prefix_key . md5($english_string); + + if (isset($GLOBALS[$name_var][$trans_key])) { + $new_lang[$english_string]['trad'] = html_entity_decode($GLOBALS[$name_var][$trans_key], ENT_COMPAT, 'UTF-8'); + } else { + if (!isset($new_lang[$english_string]['trad'])) { + $new_lang[$english_string]['trad'] = ''; + if (!isset($missing_translations_back[$prefix_key])) { + $missing_translations_back[$prefix_key] = 1; + } else { + ++$missing_translations_back[$prefix_key]; + } + } + } + $new_lang[$english_string]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + } + if (isset($tabs_array[$prefix_key])) { + $tabs_array[$prefix_key] = array_merge($tabs_array[$prefix_key], $new_lang); + } else { + $tabs_array[$prefix_key] = $new_lang; + } + } + } + } + + // count will contain the number of expressions of the page + $count = 0; + foreach ($tabs_array as $array) { + $count += count($array); + } + + $this->tpl_view_vars = array_merge($this->tpl_view_vars, array( + 'count' => $count, + 'cancel_url' => $this->context->link->getAdminLink('AdminTranslations'), + 'limit_warning' => $this->displayLimitPostWarning($count), + 'mod_security_warning' => Tools::apacheModExists('mod_security'), + 'tabsArray' => $tabs_array, + 'missing_translations' => $missing_translations_back, + )); + + $this->initToolbar(); + $this->base_tpl_view = 'translation_form.tpl'; + + return parent::renderView(); + } + + /** + * Check if directory and file exist and return an list of modules. + * + * @return array List of modules + * + * @throws PrestaShopException + */ + public function getListModules($withInstance = false) + { + if (!Tools::file_exists_cache($this->translations_informations['modules']['dir'])) { + throw new PrestaShopException($this->trans('Fatal error: The module directory does not exist.', array(), 'Admin.Notifications.Error') . '(' . $this->translations_informations['modules']['dir'] . ')'); + } + if (!is_writable($this->translations_informations['modules']['dir'])) { + throw new PrestaShopException($this->trans('The module directory must be writable.', array(), 'Admin.International.Notification')); + } + + $modules = array(); + // Get all module which are installed for to have a minimum of POST + $modules = Module::getModulesInstalled(); + if ($withInstance) { + foreach ($modules as $module) { + if ($tmp_instance = Module::getInstanceById((int) $module['id_module'])) { + // We want to be able to sort modules by display name + $module_instances[$tmp_instance->displayName] = $tmp_instance; + } + } + ksort($module_instances); + + return $module_instances; + } + + foreach ($modules as &$module) { + $module = $module['name']; + } + + return $modules; + } + + /** + * This method generate the form for errors translations. + */ + public function initFormErrors() + { + $name_var = $this->translations_informations[$this->type_selected]['var']; + $GLOBALS[$name_var] = $this->fileExists(); + $count_empty = array(); + + /* List files to parse */ + $string_to_translate = array(); + $file_by_directory = $this->getFileToParseByTypeTranslation(); + + if ($modules = $this->getListModules()) { + foreach ($modules as $module) { + if (is_dir(_PS_MODULE_DIR_ . $module) && !in_array($module, self::$ignore_folder)) { + $file_by_directory['php'] = array_merge($file_by_directory['php'], $this->listFiles(_PS_MODULE_DIR_ . $module . '/', array(), 'php')); + } + } + } + + foreach ($file_by_directory['php'] as $dir => $files) { + foreach ($files as $file) { + if (preg_match('/\.php$/', $file) && Tools::file_exists_cache($file_path = $dir . $file) && !in_array($file, self::$ignore_folder)) { + if (!filesize($file_path)) { + continue; + } + + // Get content for this file + $content = file_get_contents($file_path); + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected); + + foreach ($matches as $key) { + if (array_key_exists(md5($key), $GLOBALS[$name_var])) { + $string_to_translate[$key]['trad'] = html_entity_decode($GLOBALS[$name_var][md5($key)], ENT_COMPAT, 'UTF-8'); + } else { + $string_to_translate[$key]['trad'] = ''; + if (!isset($count_empty[$key])) { + $count_empty[$key] = 1; + } + } + $string_to_translate[$key]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + } + } + } + + //adding sf2 form translations + $sf2_loader = new Symfony\Component\Translation\Loader\XliffFileLoader(); + try { + $sf2_trans = $sf2_loader->load(_PS_VENDOR_DIR_ . '/symfony/symfony/src/Symfony/Component/Validator/Resources/translations/validators.' . $this->lang_selected->iso_code . '.xlf', $this->lang_selected->iso_code); + } catch (\Exception $e) { + $sf2_trans = $sf2_loader->load(_PS_VENDOR_DIR_ . '/symfony/symfony/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf', $this->lang_selected->iso_code); + } + + foreach ($sf2_trans->all()['messages'] as $k => $v) { + if (array_key_exists(md5($k), $GLOBALS[$name_var])) { + $string_to_translate[$k]['trad'] = html_entity_decode($GLOBALS[$name_var][md5($k)], ENT_COMPAT, 'UTF-8'); + } else { + $string_to_translate[$k]['trad'] = ''; + if (!isset($count_empty[$k])) { + $count_empty[$k] = 1; + } + } + $string_to_translate[$k]['use_sprintf'] = false; + } + + $this->tpl_view_vars = array_merge($this->tpl_view_vars, array( + 'count' => count($string_to_translate), + 'cancel_url' => $this->context->link->getAdminLink('AdminTranslations'), + 'limit_warning' => $this->displayLimitPostWarning(count($string_to_translate)), + 'mod_security_warning' => Tools::apacheModExists('mod_security'), + 'errorsArray' => $string_to_translate, + 'missing_translations' => $count_empty, + )); + + $this->initToolbar(); + $this->base_tpl_view = 'translation_errors.tpl'; + + return parent::renderView(); + } + + /** + * This method generate the form for fields translations. + */ + public function initFormFields() + { + $name_var = $this->translations_informations[$this->type_selected]['var']; + $GLOBALS[$name_var] = $this->fileExists(); + $missing_translations_fields = array(); + $class_array = array(); + $tabs_array = array(); + $count = 0; + + $files_by_directory = $this->getFileToParseByTypeTranslation(); + + foreach ($files_by_directory['php'] as $dir => $files) { + foreach ($files as $file) { + $exclude_files = array('index.php', 'PrestaShopAutoload.php', 'StockManagerInterface.php', + 'TaxManagerInterface.php', 'WebserviceOutputInterface.php', 'WebserviceSpecificManagementInterface.php', ); + + if (!preg_match('/\.php$/', $file) || in_array($file, $exclude_files)) { + continue; + } + + $class_name = substr($file, 0, -4); + + if (!class_exists($class_name, false) && !class_exists($class_name . 'Core', false)) { + PrestaShopAutoload::getInstance()->load($class_name); + } + + if (!is_subclass_of($class_name . 'Core', 'ObjectModel')) { + continue; + } + $class_array[$class_name] = call_user_func(array($class_name, 'getValidationRules'), $class_name); + } + } + foreach ($class_array as $prefix_key => $rules) { + if (isset($rules['validate'])) { + foreach ($rules['validate'] as $key => $value) { + if (isset($GLOBALS[$name_var][$prefix_key . '_' . md5($key)])) { + $tabs_array[$prefix_key][$key]['trad'] = html_entity_decode($GLOBALS[$name_var][$prefix_key . '_' . md5($key)], ENT_COMPAT, 'UTF-8'); + ++$count; + } else { + if (!isset($tabs_array[$prefix_key][$key]['trad'])) { + $tabs_array[$prefix_key][$key]['trad'] = ''; + if (!isset($missing_translations_fields[$prefix_key])) { + $missing_translations_fields[$prefix_key] = 1; + } else { + ++$missing_translations_fields[$prefix_key]; + } + ++$count; + } + } + } + } + if (isset($rules['validateLang'])) { + foreach ($rules['validateLang'] as $key => $value) { + if (isset($GLOBALS[$name_var][$prefix_key . '_' . md5($key)])) { + $tabs_array[$prefix_key][$key]['trad'] = ''; + if (array_key_exists($prefix_key . '_' . md5(addslashes($key)), $GLOBALS[$name_var])) { + $tabs_array[$prefix_key][$key]['trad'] = html_entity_decode($GLOBALS[$name_var][$prefix_key . '_' . md5(addslashes($key))], ENT_COMPAT, 'UTF-8'); + } + + ++$count; + } else { + if (!isset($tabs_array[$prefix_key][$key]['trad'])) { + $tabs_array[$prefix_key][$key]['trad'] = ''; + if (!isset($missing_translations_fields[$prefix_key])) { + $missing_translations_fields[$prefix_key] = 1; + } else { + ++$missing_translations_fields[$prefix_key]; + } + ++$count; + } + } + } + } + } + + $this->tpl_view_vars = array_merge($this->tpl_view_vars, array( + 'count' => $count, + 'limit_warning' => $this->displayLimitPostWarning($count), + 'mod_security_warning' => Tools::apacheModExists('mod_security'), + 'tabsArray' => $tabs_array, + 'cancel_url' => $this->context->link->getAdminLink('AdminTranslations'), + 'missing_translations' => $missing_translations_fields, + )); + + $this->initToolbar(); + $this->base_tpl_view = 'translation_form.tpl'; + + return parent::renderView(); + } + + /** + * Get each informations for each mails found in the folder $dir. + * + * @since 1.4.0.14 + * + * @param string $dir + * @param string $group_name + * + * @return array : list of mails + */ + public function getMailFiles($dir, $group_name = 'mail') + { + $arr_return = array(); + if (Language::getIdByIso('en')) { + $default_language = 'en'; + } else { + $default_language = Language::getIsoById((int) Configuration::get('PS_LANG_DEFAULT')); + } + if (!$default_language || !Validate::isLanguageIsoCode($default_language)) { + return false; + } + + // Very usefull to name input and textarea fields + $arr_return['group_name'] = $group_name; + $arr_return['empty_values'] = 0; + $arr_return['total_filled'] = 0; + $arr_return['directory'] = $dir; + + // Get path for english mail directory + $dir_en = str_replace('/' . $this->lang_selected->iso_code . '/', '/' . $default_language . '/', $dir); + + if (Tools::file_exists_cache($dir_en)) { + // Get all english files to compare with the language to translate + foreach (scandir($dir_en, SCANDIR_SORT_NONE) as $email_file) { + if (strripos($email_file, '.html') > 0 || strripos($email_file, '.txt') > 0) { + $email_name = substr($email_file, 0, strripos($email_file, '.')); + $type = substr($email_file, strripos($email_file, '.') + 1); + if (!isset($arr_return['files'][$email_name])) { + $arr_return['files'][$email_name] = array(); + } + // $email_file is from scandir ($dir), so we already know that file exists + $arr_return['files'][$email_name][$type]['en'] = $this->getMailContent($dir_en, $email_file); + + // check if the file exists in the language to translate + if (Tools::file_exists_cache($dir . '/' . $email_file)) { + $arr_return['files'][$email_name][$type][$this->lang_selected->iso_code] = $this->getMailContent($dir, $email_file); + ++$this->total_expression; + } else { + $arr_return['files'][$email_name][$type][$this->lang_selected->iso_code] = ''; + } + + if ($arr_return['files'][$email_name][$type][$this->lang_selected->iso_code] == '') { + ++$arr_return['empty_values']; + } else { + ++$arr_return['total_filled']; + } + } + } + } else { + $this->warnings[] = $this->trans('A mail directory exists for the "%iso_code%" language, but not for the default language (%language%) in %folder%', + array('%iso_code%' => $this->lang_selected->iso_code, '%folder%' => str_replace(_PS_ROOT_DIR_, '', dirname($dir)), '%language%' => $default_language), + 'Admin.International.Notification'); + } + + return $arr_return; + } + + /** + * Get content of the mail file. + * + * @since 1.4.0.14 + * + * @param string $dir + * @param string $file + * + * @return array : content of file + */ + protected function getMailContent($dir, $file) + { + $content = file_get_contents($dir . '/' . $file); + + if (Tools::strlen($content) === 0) { + $content = ''; + } + + return $content; + } + + /** + * Display mails in html format. + * This was create for factorize the html displaying. + * + * @since 1.4.0.14 + * + * @param array $mails + * @param array $all_subject_mail + * @param Language $obj_lang + * @param string $id_html Use for set html id attribute for the block + * @param string $title Set the title for the block + * @param string|bool $name_for_module Is not false define add a name for distinguish mails module + * + * @return string + */ + protected function displayMailContent($mails, $all_subject_mail, $obj_lang, $id_html, $title, $name_for_module = false) + { + $str_return = ''; + $group_name = 'mail'; + if (array_key_exists('group_name', $mails)) { + $group_name = $mails['group_name']; + } + + if ($mails['empty_values'] == 0) { + $translation_missing_badge_type = 'badge-success'; + } else { + $translation_missing_badge_type = 'badge-danger'; + } + $str_return .= '
+

+ ' . ((int) $mails['empty_values'] + (int) $mails['total_filled']) . ' + ' . $title . ' + ' . $mails['empty_values'] . ' ' . $this->trans('missing translation(s)', array(), 'Admin.International.Notification') . ' +

+
'; + + if (!empty($mails['files'])) { + $topic_already_displayed = array(); + foreach ($mails['files'] as $mail_name => $mail_files) { + $str_return .= '
'; + $str_return .= '' . $mail_name . ' '; + $str_return .= ''; + $str_return .= '
'; + } + } + } else { + $str_return .= '

+ ' . $this->trans('There was a problem getting the mail files.', array(), 'Admin.International.Notification') . '
+ ' . $this->trans('English language files must exist in %folder% folder', array( + '%folder%' => '' . preg_replace('@/[a-z]{2}(/?)$@', '/en$1', $mails['directory']) . '', + ), 'Admin.International.Notification') . ' +

'; + } + + $str_return .= '
'; + + return $str_return; + } + + /** + * Just build the html structure for display txt mails. + * + * @since 1.4.0.14 + * + * @param array $content With english and language needed contents + * @param string $lang ISO code of the needed language + * @param string $mail_name Name of the file to translate (same for txt and html files) + * @param string $group_name group name allow to distinguish each block of mail + * @param string|bool $name_for_module Is not false define add a name for distinguish mails module + * + * @return string + */ + protected function displayMailBlockTxt($content, $lang, $mail_name, $group_name, $name_for_module = false) + { + if (!empty($content)) { + $text_content = Tools::htmlentitiesUTF8(stripslashes(strip_tags($content[$lang]))); + } else { + $text_content = ''; + } + + return '
+
+
+
+
'; + } + + /** + * Just build the html structure for display html mails. + * + * @since 1.4.0.14 + * + * @param array $content With english and language needed contents + * @param string $lang ISO code of the needed language + * @param string $url The html page and displaying an outline + * @param string $mail_name Name of the file to translate (same for txt and html files) + * @param string $group_name group name allow to distinguish each block of mail + * @param string|bool $name_for_module Is not false define add a name for distinguish mails module + * + * @return string + */ + protected function displayMailBlockHtml($content, $lang, $url, $mail_name, $group_name, $name_for_module = false) + { + $title = array(); + + if (!empty($content)) { + $this->cleanMailContent($content, $lang, $title); + } + + $name_for_module = $name_for_module ? $name_for_module . '|' : ''; + + return '
+
+ +
+
'; + } + + protected function displayMailEditor($content, $lang, $mail_name, $group_name, $name_for_module = false) + { + $title = array(); + + if (!empty($content)) { + $this->cleanMailContent($content, $lang, $title); + $html_content = $content[$lang]; + } else { + $html_content = ''; + } + + $name_for_module = $name_for_module ? $name_for_module . '|' : ''; + + return ''; + } + + protected function cleanMailContent(&$content, $lang, &$title) + { + if (stripos($content[$lang], '([^<]+)#Ui', $title[$language], $matches); + $title[$language] = empty($matches[1]) ? '' : $matches[1]; + } + } + $content[$lang] = (isset($content[$lang]) ? Tools::htmlentitiesUTF8(stripslashes($content[$lang])) : ''); + } + + /** + * Check in each module if contains mails folder. + * + * @param bool $with_module_name + * + * @return array Array of modules which have mails + */ + public function getModulesHasMails($with_module_name = false) + { + $arr_modules = array(); + if (array_key_exists('dir', $this->translations_informations['modules'])) { + if ($modules_dir = scandir($this->translations_informations['modules']['dir'], SCANDIR_SORT_NONE)) { + foreach ($modules_dir as $module_dir) { + if (!in_array($module_dir, self::$ignore_folder)) { + $dir = false; + if ($this->theme_selected && Tools::file_exists_cache($this->translations_informations['modules']['override']['dir'] . $module_dir . '/mails/')) { + $dir = $this->translations_informations['modules']['override']['dir'] . $module_dir . '/'; + } elseif (Tools::file_exists_cache($this->translations_informations['modules']['dir'] . $module_dir . '/mails/')) { + $dir = $this->translations_informations['modules']['dir'] . $module_dir . '/'; + } + if ($dir !== false) { + if ($with_module_name) { + $arr_modules[$module_dir] = $dir; + } else { + if ($this->theme_selected) { + $dir = $this->translations_informations['modules']['dir'] . $module_dir . '/'; + } + $arr_modules[$dir] = scandir($dir, SCANDIR_SORT_NONE); + } + } + } + } + } + } + + return $arr_modules; + } + + /** + * Check in each module if contains pdf folder. + * + * @param bool $classes + * + * @return array Array of modules which have pdf + */ + public function getModulesHasPDF($classes = false) + { + $arr_modules = array(); + foreach (scandir($this->translations_informations['modules']['dir'], SCANDIR_SORT_NONE) as $module_dir) { + if (!in_array($module_dir, self::$ignore_folder)) { + $dir = false; + if ($classes) { + if ($this->theme_selected && Tools::file_exists_cache($this->translations_informations['modules']['override']['dir'] . $module_dir . '/classes/')) { + $dir = $this->translations_informations['modules']['override']['dir'] . $module_dir . '/classes/'; + } elseif (Tools::file_exists_cache($this->translations_informations['modules']['dir'] . $module_dir . '/classes/')) { + $dir = $this->translations_informations['modules']['dir'] . $module_dir . '/classes/'; + } + if ($dir !== false) { + $arr_modules[$dir] = scandir($dir, SCANDIR_SORT_NONE); + } + } else { + if ($this->theme_selected && Tools::file_exists_cache($this->translations_informations['modules']['override']['dir'] . $module_dir . '/pdf/')) { + $dir = $this->translations_informations['modules']['override']['dir'] . $module_dir . '/pdf/'; + } elseif (Tools::file_exists_cache($this->translations_informations['modules']['dir'] . $module_dir . '/pdf/')) { + $dir = $this->translations_informations['modules']['dir'] . $module_dir . '/pdf/'; + } + if ($dir !== false) { + $arr_modules[$dir] = scandir($dir, SCANDIR_SORT_NONE); + } + } + } + } + + return $arr_modules; + } + + protected function getTinyMCEForMails($iso_lang) + { + // TinyMCE + $iso_tiny_mce = (Tools::file_exists_cache(_PS_ROOT_DIR_ . '/js/tiny_mce/langs/' . $iso_lang . '.js') ? $iso_lang : 'en'); + $ad = __PS_BASE_URI__ . basename(_PS_ADMIN_DIR_); + //return false; + return ' + + + '; + } + + /** + * This method generate the form for mails translations. + * + * @param bool $no_display + * + * @return array|string + */ + public function initFormMails($no_display = false) + { + $module_mails = array(); + + // get all mail subjects, this method parse each files in Prestashop !! + $subject_mail = array(); + + $modules_has_mails = $this->getModulesHasMails(true); + + $files_by_directiories = $this->getFileToParseByTypeTranslation(); + + if (!$this->theme_selected || !@filemtime($this->translations_informations[$this->type_selected]['override']['dir'])) { + $this->copyMailFilesForAllLanguages(); + } + + foreach ($files_by_directiories['php'] as $dir => $files) { + if (!empty($files)) { + foreach ($files as $file) { + // If file exist and is not in ignore_folder, in the next step we check if a folder or mail + if (Tools::file_exists_cache($dir . $file) && !in_array($file, self::$ignore_folder)) { + $subject_mail = $this->getSubjectMail($dir, $file, $subject_mail); + } + } + } + } + + // Get path of directory for find a good path of translation file + if ($this->theme_selected && @filemtime($this->translations_informations[$this->type_selected]['override']['dir'])) { + $i18n_dir = $this->translations_informations[$this->type_selected]['override']['dir']; + } else { + $i18n_dir = $this->translations_informations[$this->type_selected]['dir']; + } + + $core_mails = $this->getMailFiles($i18n_dir, 'core_mail'); + + foreach ($modules_has_mails as $module_name => $module_path) { + $module_path = rtrim($module_path, '/'); + $module_mails[$module_name] = $this->getMailFiles($module_path . '/mails/' . $this->lang_selected->iso_code . '/', 'module_mail'); + $module_mails[$module_name]['display'] = $this->displayMailContent($module_mails[$module_name], $subject_mail, $this->lang_selected, Tools::strtolower($module_name), $module_name, $module_name); + } + + if ($no_display) { + $empty = 0; + $total = 0; + $total += (int) $core_mails['total_filled']; + $empty += (int) $core_mails['empty_values']; + foreach ($module_mails as $mod_infos) { + $total += (int) $mod_infos['total_filled']; + $empty += (int) $mod_infos['empty_values']; + } + + return array('total' => $total, 'empty' => $empty); + } + + $this->tpl_view_vars = array_merge($this->tpl_view_vars, array( + 'limit_warning' => $this->displayLimitPostWarning($this->total_expression), + 'mod_security_warning' => Tools::apacheModExists('mod_security'), + 'tinyMCE' => $this->getTinyMCEForMails($this->lang_selected->iso_code), + 'mail_content' => $this->displayMailContent($core_mails, $subject_mail, $this->lang_selected, 'core', $this->trans('Core emails', array(), 'Admin.International.Feature')), + 'cancel_url' => $this->context->link->getAdminLink('AdminTranslations'), + 'module_mails' => $module_mails, + 'theme_name' => $this->theme_selected, + )); + + $this->initToolbar(); + $this->base_tpl_view = 'translation_mails.tpl'; + + return parent::renderView(); + } + + public function copyMailFilesForAllLanguages() + { + $current_theme = Tools::safeOutput($this->context->shop->theme->getName()); + $languages = Language::getLanguages(); + + foreach ($languages as $key => $lang) { + $dir_to_copy_iso = array(); + $files_to_copy_iso = array(); + $current_iso_code = $lang['iso_code']; + + $dir_to_copy_iso[] = _PS_MAIL_DIR_ . $current_iso_code . '/'; + + $modules_has_mails = $this->getModulesHasMails(true); + foreach ($modules_has_mails as $module_name => $module_path) { + if ($pos = strpos($module_path, '/modules')) { + $dir_to_copy_iso[] = _PS_ROOT_DIR_ . substr($module_path, $pos) . 'mails/' . $current_iso_code . '/'; + } + } + + foreach ($dir_to_copy_iso as $dir) { + if (!empty($dir) && is_dir($dir)) { + if ($scanDir = scandir($dir, SCANDIR_SORT_NONE)) { + foreach ($scanDir as $file) { + if (!in_array($file, self::$ignore_folder)) { + $files_to_copy_iso[] = array( + 'from' => $dir . $file, + 'to' => str_replace((strpos($dir, _PS_CORE_DIR_) !== false) ? _PS_CORE_DIR_ : _PS_ROOT_DIR_, _PS_ROOT_DIR_ . '/themes/' . $current_theme, $dir) . $file, + ); + } + } + } + } + } + + foreach ($files_to_copy_iso as $file) { + if (!file_exists($file['to'])) { + $content = file_get_contents($file['from']); + + $stack = array(); + $folder = dirname($file['to']); + while (!is_dir($folder)) { + $stack[] = $folder; + $folder = dirname($folder); + } + while ($folder = array_pop($stack)) { + mkdir($folder); + } + + $success = file_put_contents($file['to'], $content); + if ($success === false) { + Tools::dieOrLog(sprintf('%s cannot be copied to %s', $file['from'], $file['to']), false); + } + } + } + } + + return true; + } + + /** + * Get list of subjects of mails. + * + * @param $dir + * @param $file + * @param $subject_mail + * + * @return array : list of subjects of mails + */ + protected function getSubjectMail($dir, $file, $subject_mail) + { + $dir = rtrim($dir, '/'); + // If is file and is not in ignore_folder + if (is_file($dir . '/' . $file) && !in_array($file, self::$ignore_folder) && preg_match('/\.php$/', $file)) { + $content = file_get_contents($dir . '/' . $file); + $content = str_replace("\n", ' ', $content); + + // Subject must match with a template, therefore we first grep the Mail::Send() function then the Mail::l() inside. + if (preg_match_all('/Mail::Send([^;]*);/si', $content, $tab)) { + for ($i = 0; isset($tab[1][$i]); ++$i) { + $tab2 = explode(',', $tab[1][$i]); + if (is_array($tab2) && isset($tab2[1])) { + $template = trim(str_replace('\'', '', $tab2[1])); + foreach ($tab2 as $tab3) { + if (preg_match('/Mail::l\(\'' . _PS_TRANS_PATTERN_ . '\'\)/Us', $tab3 . ')', $matches)) { + if (!isset($subject_mail[$template])) { + $subject_mail[$template] = array(); + } + if (!in_array($matches[1], $subject_mail[$template])) { + $subject_mail[$template][] = $matches[1]; + } + } + } + } + } + } + } elseif (!in_array($file, self::$ignore_folder) && is_dir($dir . '/' . $file)) { + // Or if is folder, we scan folder for check if found in folder and subfolder + foreach (scandir($dir . '/' . $file, SCANDIR_SORT_NONE) as $temp) { + if ($temp[0] != '.') { + $subject_mail = $this->getSubjectMail($dir . '/' . $file, $temp, $subject_mail); + } + } + } + + return $subject_mail; + } + + protected function writeSubjectTranslationFile($sub, $path) + { + if (!Tools::file_exists_cache(dirname($path))) { + if (!mkdir(dirname($path), 0700)) { + throw new PrestaShopException('Directory ' . dirname($path) . ' cannot be created.'); + } + } + if ($fd = @fopen($path, 'w')) { + $tab = 'LANGMAIL'; + fwrite($fd, " $value) { + // Magic Quotes shall... not.. PASS! + if (_PS_MAGIC_QUOTES_GPC_) { + $value = stripslashes($value); + } + fwrite($fd, '$_' . $tab . '[\'' . pSQL($key) . '\'] = \'' . pSQL($value) . '\';' . "\n"); + } + + fwrite($fd, "\n?>"); + fclose($fd); + } else { + throw new PrestaShopException($this->trans('Cannot write language file for email subjects. Path is: %folder%', array('%folder%' => $path), 'Admin.International.Notification')); + } + } + + /** + * This get files to translate in module directory. + * Recursive method allow to get each files for a module no matter his depth. + * + * @param string $path directory path to scan + * @param array $array_files by reference - array which saved files to parse + * @param string $module_name module name + * @param string $lang_file full path of translation file + * @param bool $is_default + */ + protected function recursiveGetModuleFiles($path, &$array_files, $module_name, $lang_file, $is_default = false) + { + $files_module = array(); + if (Tools::file_exists_cache($path)) { + $files_module = scandir($path, SCANDIR_SORT_NONE); + } + $files_for_module = $this->clearModuleFiles($files_module, 'file'); + if (!empty($files_for_module)) { + $array_files[] = array( + 'file_name' => $lang_file, + 'dir' => $path, + 'files' => $files_for_module, + 'module' => $module_name, + 'is_default' => $is_default, + 'theme' => $this->theme_selected, + ); + } + + $dir_module = $this->clearModuleFiles($files_module, 'directory', $path); + + if (!empty($dir_module)) { + foreach ($dir_module as $folder) { + $this->recursiveGetModuleFiles($path . $folder . '/', $array_files, $module_name, $lang_file, $is_default); + } + } + } + + /** + * This method get translation in each translations file. + * The file depend on $lang param. + * + * @param array $modules List of modules + * @param string|null $root_dir path where it get each modules + * @param string $lang ISO code of chosen language to translate + * @param bool $is_default Set it if modules are located in root/prestashop/modules folder + * This allow to distinguish overridden prestashop theme and original module + * + * @return array + */ + protected function getAllModuleFiles($modules, $root_dir, $lang, $is_default = false) + { + $array_files = array(); + $initial_root_dir = $root_dir; + foreach ($modules as $module) { + $root_dir = $initial_root_dir; + if (isset($module[0]) && $module[0] == '.') { + continue; + } + + // First we load the default translation file + if ($root_dir == null) { + $i18n_dir = $this->translations_informations[$this->type_selected]['dir']; + if (is_dir($i18n_dir . $module)) { + $root_dir = $i18n_dir; + } + + $lang_file = $root_dir . $module . '/translations/' . $lang . '.php'; + if (!Tools::file_exists_cache($root_dir . $module . '/translations/' . $lang . '.php') && Tools::file_exists_cache($root_dir . $module . '/' . $lang . '.php')) { + $lang_file = $root_dir . $module . '/' . $lang . '.php'; + } + @include $lang_file; + $this->getModuleTranslations(); + // If a theme is selected, then the destination translation file must be in the theme + if ($this->theme_selected) { + $lang_file = $this->translations_informations[$this->type_selected]['override']['dir'] . $module . '/translations/' . $lang . '.php'; + } + $this->recursiveGetModuleFiles($root_dir . $module . '/', $array_files, $module, $lang_file, $is_default); + } + + $root_dir = $initial_root_dir; + // Then we load the overriden translation file + if ($this->theme_selected && isset($this->translations_informations[$this->type_selected]['override'])) { + $i18n_dir = $this->translations_informations[$this->type_selected]['override']['dir']; + if (is_dir($i18n_dir . $module)) { + $root_dir = $i18n_dir; + } + if (Tools::file_exists_cache($root_dir . $module . '/translations/' . $lang . '.php')) { + $lang_file = $root_dir . $module . '/translations/' . $lang . '.php'; + } elseif (Tools::file_exists_cache($root_dir . $module . '/' . $lang . '.php')) { + $lang_file = $root_dir . $module . '/' . $lang . '.php'; + } + @include $lang_file; + $this->getModuleTranslations(); + $this->recursiveGetModuleFiles($root_dir . $module . '/', $array_files, $module, $lang_file, $is_default); + } + } + + return $array_files; + } + + /** + * This method generate the form for modules translations. + */ + public function initFormModules() + { + // Get list of installed modules + $installed_modules = $this->getListModules(); + + // get selected module + $modules[0] = Tools::getValue('module'); + + if (!empty($modules)) { + // Get all modules files and include all translation files + $arr_files = $this->getAllModuleFiles($modules, null, $this->lang_selected->iso_code, true); + foreach ($arr_files as $value) { + $this->findAndFillTranslations($value['files'], $value['theme'], $value['module'], $value['dir']); + } + + $this->tpl_view_vars = array_merge($this->tpl_view_vars, array( + 'default_theme_name' => self::DEFAULT_THEME_NAME, + 'count' => $this->total_expression, + 'limit_warning' => $this->displayLimitPostWarning($this->total_expression), + 'mod_security_warning' => Tools::apacheModExists('mod_security'), + 'textarea_sized' => AdminTranslationsControllerCore::TEXTAREA_SIZED, + 'cancel_url' => $this->context->link->getAdminLink('AdminTranslations'), + 'modules_translations' => isset($this->modules_translations) ? $this->modules_translations : array(), + 'missing_translations' => $this->missing_translations, + 'module_name' => $modules[0], + 'installed_modules' => $installed_modules, + )); + + $this->initToolbar(); + $this->base_tpl_view = 'translation_modules.tpl'; + + return parent::renderView(); + } + } + + /** + * Parse PDF class. + * + * @since 1.4.5.0 + * + * @param string $file_path File to parse + * @param string $file_type Type of file + * @param array $lang_array Contains expression in the chosen language + * @param string $tab name To use with the md5 key + * @param array $tabs_array + * @param array $count_missing + * + * @return array Array Containing all datas needed for building the translation form + */ + protected function parsePdfClass($file_path, $file_type, $lang_array, $tab, $tabs_array, &$count_missing) + { + // Get content for this file + $content = file_get_contents($file_path); + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected, $file_type); + + foreach ($matches as $key) { + if (stripslashes(array_key_exists($tab . md5(addslashes($key)), $lang_array))) { + $tabs_array[$tab][$key]['trad'] = html_entity_decode($lang_array[$tab . md5(addslashes($key))], ENT_COMPAT, 'UTF-8'); + } else { + $tabs_array[$tab][$key]['trad'] = ''; + if (!isset($count_missing[$tab])) { + $count_missing[$tab] = 1; + } else { + ++$count_missing[$tab]; + } + } + $tabs_array[$tab][$key]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + + return $tabs_array; + } + + /** + * This method generate the form for PDF translations. + */ + public function initFormPDF() + { + $name_var = $this->translations_informations[$this->type_selected]['var']; + $GLOBALS[$name_var] = array(); + $missing_translations_pdf = array(); + + $i18n_dir = $this->translations_informations[$this->type_selected]['dir']; + $default_i18n_file = $i18n_dir . $this->translations_informations[$this->type_selected]['file']; + + if (!$this->theme_selected) { + $i18n_file = $default_i18n_file; + } else { + $i18n_dir = $this->translations_informations[$this->type_selected]['override']['dir']; + $i18n_file = $i18n_dir . $this->translations_informations[$this->type_selected]['override']['file']; + } + + $this->checkDirAndCreate($i18n_file); + if ((!file_exists($i18n_file) && !is_writable($i18n_dir)) && !is_writable($i18n_file)) { + $this->errors[] = $this->trans('Cannot write into the "%file%"', array('%file%' => $i18n_file), 'Admin.International.Notification'); + } + + @include $i18n_file; + + // if the override's translation file is empty load the default file + if (!isset($GLOBALS[$name_var]) || count($GLOBALS[$name_var]) == 0) { + @include $default_i18n_file; + } + + $prefix_key = 'PDF'; + $tabs_array = array($prefix_key => array()); + + $files_by_directory = $this->getFileToParseByTypeTranslation(); + + foreach ($files_by_directory as $type => $directories) { + foreach ($directories as $dir => $files) { + foreach ($files as $file) { + if (!in_array($file, self::$ignore_folder) && Tools::file_exists_cache($file_path = $dir . $file)) { + if ($type == 'tpl') { + if (Tools::file_exists_cache($file_path) && is_file($file_path)) { + // Get content for this file + $content = file_get_contents($file_path); + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected, 'tpl'); + + foreach ($matches as $key) { + if (isset($GLOBALS[$name_var][$prefix_key . md5($key)])) { + $tabs_array[$prefix_key][$key]['trad'] = (html_entity_decode($GLOBALS[$name_var][$prefix_key . md5($key)], ENT_COMPAT, 'UTF-8')); + } else { + if (!isset($tabs_array[$prefix_key][$key]['trad'])) { + $tabs_array[$prefix_key][$key]['trad'] = ''; + if (!isset($missing_translations_pdf[$prefix_key])) { + $missing_translations_pdf[$prefix_key] = 1; + } else { + ++$missing_translations_pdf[$prefix_key]; + } + } + } + $tabs_array[$prefix_key][$key]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + } + } elseif (Tools::file_exists_cache($file_path)) { + $tabs_array = $this->parsePdfClass($file_path, 'php', $GLOBALS[$name_var], $prefix_key, $tabs_array, $missing_translations_pdf); + } + } + } + } + } + + $this->tpl_view_vars = array_merge($this->tpl_view_vars, array( + 'count' => count($tabs_array['PDF']), + 'limit_warning' => $this->displayLimitPostWarning(count($tabs_array['PDF'])), + 'mod_security_warning' => Tools::apacheModExists('mod_security'), + 'tabsArray' => $tabs_array, + 'cancel_url' => $this->context->link->getAdminLink('AdminTranslations'), + 'missing_translations' => $missing_translations_pdf, + )); + + $this->initToolbar(); + $this->base_tpl_view = 'translation_form.tpl'; + + return parent::renderView(); + } + + /** + * Recursively list files in directory $dir. + * + * @param string $dir + * @param array $list + * @param string $file_ext + * + * @return array + */ + public function listFiles($dir, $list = array(), $file_ext = 'tpl') + { + $dir = rtrim($dir, '/') . DIRECTORY_SEPARATOR; + + $to_parse = scandir($dir, SCANDIR_SORT_NONE); + // copied (and kind of) adapted from AdminImages.php + foreach ($to_parse as $file) { + if (!in_array($file, self::$ignore_folder)) { + if (preg_match('#' . preg_quote($file_ext, '#') . '$#i', $file)) { + $list[$dir][] = $file; + } elseif (is_dir($dir . $file)) { + $list = $this->listFiles($dir . $file, $list, $file_ext); + } + } + } + + return $list; + } + + /** + * Checks if theme exists. + * + * @param string $theme + * + * @return bool + */ + protected function theme_exists($theme) + { + $theme_exists = false; + foreach ($this->themes as $existing_theme) { + /** @var Theme $existing_theme */ + if ($existing_theme->getName() == $theme) { + return true; + } + } + + return false; + } + + public static function getEmailHTML($email) + { + if (defined('_PS_HOST_MODE_') && strpos($email, _PS_MAIL_DIR_) !== false) { + $email_file = $email; + } elseif (__PS_BASE_URI__ != '/') { + $email_file = str_replace(__PS_BASE_URI__, _PS_ROOT_DIR_ . '/', $email); + } else { + $email_file = _PS_ROOT_DIR_ . $email; + } + + if (file_exists($email_file)) { + $email_html = file_get_contents($email_file); + } else { + $email_html = ''; + } + + return $email_html; + } +} diff --git a/controllers/admin/AdminWebserviceController.php b/controllers/admin/AdminWebserviceController.php new file mode 100644 index 00000000..7f7c269f --- /dev/null +++ b/controllers/admin/AdminWebserviceController.php @@ -0,0 +1,302 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property WebserviceKey $object + */ +class AdminWebserviceControllerCore extends AdminController +{ + /** this will be filled later */ + public $fields_form = array('webservice form'); + protected $toolbar_scroll = false; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'webservice_account'; + $this->className = 'WebserviceKey'; + $this->lang = false; + $this->edit = true; + $this->delete = true; + $this->id_lang_default = Configuration::get('PS_LANG_DEFAULT'); + + parent::__construct(); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + + $this->fields_list = array( + 'key' => array( + 'title' => $this->trans('Key', array(), 'Admin.Advparameters.Feature'), + 'class' => 'fixed-width-md', + ), + 'description' => array( + 'title' => $this->trans('Key description', array(), 'Admin.Advparameters.Feature'), + 'align' => 'left', + 'orderby' => false, + ), + 'active' => array( + 'title' => $this->trans('Enabled', array(), 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + 'class' => 'fixed-width-xs', + ), + ); + + $this->fields_options = array( + 'general' => array( + 'title' => $this->trans('Configuration', array(), 'Admin.Global'), + 'fields' => array( + 'PS_WEBSERVICE' => array('title' => $this->trans('Enable PrestaShop\'s webservice', array(), 'Admin.Advparameters.Feature'), + 'desc' => $this->trans('Before activating the webservice, you must be sure to: ', array(), 'Admin.Advparameters.Help') . + '
    +
  1. ' . $this->trans('Check that URL rewriting is available on this server.', array(), 'Admin.Advparameters.Help') . '
  2. +
  3. ' . $this->trans('Check that the five methods GET, POST, PUT, DELETE and HEAD are supported by this server.', array(), 'Admin.Advparameters.Help') . '
  4. +
', + 'cast' => 'intval', + 'type' => 'bool', ), + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ), + ); + + if (!defined('_PS_HOST_MODE_')) { + $this->fields_options['general']['fields']['PS_WEBSERVICE_CGI_HOST'] = array( + 'title' => $this->trans('Enable CGI mode for PHP', array(), 'Admin.Advparameters.Feature'), + 'desc' => $this->trans('Before choosing "Yes", check that PHP is not configured as an Apache module on your server.', array(), 'Admin.Advparameters.Help'), + 'cast' => 'intval', + 'type' => 'bool', + ); + } + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_webservice'] = array( + 'href' => self::$currentIndex . '&addwebservice_account&token=' . $this->token, + 'desc' => $this->trans('Add new webservice key', array(), 'Admin.Advparameters.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + protected function processUpdateOptions() + { + parent::processUpdateOptions(); + Tools::generateHtaccess(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Webservice Accounts', array(), 'Admin.Advparameters.Feature'), + 'icon' => 'icon-lock', + ), + 'input' => array( + array( + 'type' => 'textbutton', + 'label' => $this->trans('Key', array(), 'Admin.Advparameters.Feature'), + 'name' => 'key', + 'id' => 'code', + 'required' => true, + 'hint' => $this->trans('Webservice account key.', array(), 'Admin.Advparameters.Feature'), + 'button' => array( + 'label' => $this->trans('Generate!', array(), 'Admin.Advparameters.Feature'), + 'attributes' => array( + 'onclick' => 'gencode(32)', + ), + ), + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Key description', array(), 'Admin.Advparameters.Feature'), + 'name' => 'description', + 'rows' => 3, + 'cols' => 110, + 'hint' => $this->trans('Quick description of the key: who it is for, what permissions it has, etc.', array(), 'Admin.Advparameters.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Status', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + ), + array( + 'type' => 'resources', + 'label' => $this->trans('Permissions', array(), 'Admin.Advparameters.Feature'), + 'name' => 'resources', + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + if (!($obj = $this->loadObject(true))) { + return; + } + + $ressources = WebserviceRequest::getResources(); + $permissions = WebserviceKey::getPermissionForAccount($obj->key); + + $this->tpl_form_vars = array( + 'ressources' => $ressources, + 'permissions' => $permissions, + ); + + return parent::renderForm(); + } + + public function initContent() + { + if ($this->display != 'add' && $this->display != 'edit') { + $this->checkForWarning(); + } + + parent::initContent(); + } + + /** + * Function used to render the options for this controller. + */ + public function renderOptions() + { + if ($this->fields_options && is_array($this->fields_options)) { + $helper = new HelperOptions($this); + $this->setHelperDisplay($helper); + $helper->toolbar_scroll = true; + $helper->toolbar_btn = array('save' => array( + 'href' => '#', + 'desc' => $this->trans('Save', array(), 'Admin.Actions'), + )); + $helper->id = $this->id; + $helper->tpl_vars = $this->tpl_option_vars; + $options = $helper->generateOptions($this->fields_options); + + return $options; + } + } + + public function initProcess() + { + parent::initProcess(); + // This is a composite page, we don't want the "options" display mode + if ($this->display == 'options') { + $this->display = ''; + } + } + + public function postProcess() + { + if (Tools::getValue('key') && strlen(Tools::getValue('key')) < 32) { + $this->errors[] = $this->trans('Key length must be 32 character long.', array(), 'Admin.Advparameters.Notification'); + } + if (WebserviceKey::keyExists(Tools::getValue('key')) && !Tools::getValue('id_webservice_account')) { + $this->errors[] = $this->trans('This key already exists.', array(), 'Admin.Advparameters.Notification'); + } + + return parent::postProcess(); + } + + protected function afterAdd($object) + { + Tools::generateHtaccess(); + WebserviceKey::setPermissionForAccount($object->id, Tools::getValue('resources', array())); + } + + protected function afterUpdate($object) + { + Tools::generateHtaccess(); + WebserviceKey::setPermissionForAccount($object->id, Tools::getValue('resources', array())); + } + + public function checkForWarning() + { + if (strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') === false) { + $this->warnings[] = $this->trans('To avoid operating problems, please use an Apache server.', array(), 'Admin.Advparameters.Notification'); + if (function_exists('apache_get_modules')) { + $apache_modules = apache_get_modules(); + if (!in_array('mod_auth_basic', $apache_modules)) { + $this->warnings[] = $this->trans('Please activate the \'mod_auth_basic\' Apache module to allow authentication of PrestaShop\'s webservice.', array(), 'Admin.Advparameters.Notification'); + } + if (!in_array('mod_rewrite', $apache_modules)) { + $this->warnings[] = $this->trans('Please activate the \'mod_rewrite\' Apache module to allow the PrestaShop webservice.', array(), 'Admin.Advparameters.Notification'); + } + } else { + $this->warnings[] = $this->trans('We could not check to see if basic authentication and rewrite extensions have been activated. Please manually check if they\'ve been activated in order to use the PrestaShop webservice.', array(), 'Admin.Advparameters.Notification'); + } + } + if (!extension_loaded('SimpleXML')) { + $this->warnings[] = $this->trans('Please activate the \'SimpleXML\' PHP extension to allow testing of PrestaShop\'s webservice.', array(), 'Admin.Advparameters.Notification'); + } + if (!configuration::get('PS_SSL_ENABLED')) { + $this->warnings[] = $this->trans('It is preferable to use SSL (https:) for webservice calls, as it avoids the "man in the middle" type security issues.', array(), 'Admin.Advparameters.Notification'); + } + + foreach ($this->_list as $k => $item) { + if ($item['is_module'] && $item['class_name'] && $item['module_name'] && + ($instance = Module::getInstanceByName($item['module_name'])) && + !$instance->useNormalPermissionBehaviour()) { + unset($this->_list[$k]); + } + } + + $this->renderList(); + } +} diff --git a/controllers/admin/AdminZonesController.php b/controllers/admin/AdminZonesController.php new file mode 100644 index 00000000..aa4aa4ff --- /dev/null +++ b/controllers/admin/AdminZonesController.php @@ -0,0 +1,143 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Zone $object + */ +class AdminZonesControllerCore extends AdminController +{ + public $asso_type = 'shop'; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'zone'; + $this->className = 'Zone'; + $this->lang = false; + + parent::__construct(); + + $this->fields_list = array( + 'id_zone' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Zone', array(), 'Admin.Global'), + ), + 'active' => array( + 'title' => $this->trans('Enabled', array(), 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + 'class' => 'fixed-width-sm', + ), + ); + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ), + ); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_zone'] = array( + 'href' => self::$currentIndex . '&addzone&token=' . $this->token, + 'desc' => $this->trans('Add new zone', array(), 'Admin.International.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + return parent::renderList(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Zones', array(), 'Admin.International.Feature'), + 'icon' => 'icon-globe', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Name', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'hint' => $this->trans('Zone name (e.g. Africa, West Coast, Neighboring Countries).', array(), 'Admin.International.Help'), + ), + array( + 'type' => 'switch', + 'label' => $this->trans('Active', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + ), + array( + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', array(), 'Admin.Global'), + ), + ), + 'hint' => $this->trans('Allow or disallow shipping to this zone.', array(), 'Admin.International.Help'), + ), + ), + ); + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = array( + 'type' => 'shop', + 'label' => $this->trans('Shop association', array(), 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ); + } + + $this->fields_form['submit'] = array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ); + + return parent::renderForm(); + } +} diff --git a/controllers/admin/BoOrder.php b/controllers/admin/BoOrder.php new file mode 100644 index 00000000..620eca77 --- /dev/null +++ b/controllers/admin/BoOrder.php @@ -0,0 +1,35 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +class BoOrderCore extends PaymentModule +{ + public $active = 1; + public $name = 'bo_order'; + + public function __construct() + { + $this->displayName = $this->trans('Back office order', array(), 'Admin.Orderscustomers.Feature'); + } +} diff --git a/controllers/admin/index.php b/controllers/admin/index.php new file mode 100644 index 00000000..e9bd9beb --- /dev/null +++ b/controllers/admin/index.php @@ -0,0 +1,34 @@ + + * @copyright 2007-2018 PrestaShop SA + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../../'); +exit;