1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26use PrestaShop\PrestaShop\Adapter\SymfonyContainer;
27use PrestaShop\PrestaShop\Core\Feature\TokenInUrls;
28use PrestaShop\PrestaShop\Core\Localization\Locale;
29use PrestaShop\PrestaShop\Core\Localization\Specification\Number as NumberSpecification;
30use PrestaShop\PrestaShop\Core\Localization\Specification\Price as PriceSpecification;
31
32class AdminControllerCore extends Controller
33{
34    /** @var string */
35    public $path;
36
37    /** @var string */
38    public static $currentIndex;
39
40    /** @var string */
41    public $content;
42
43    /** @var array */
44    public $warnings = [];
45
46    /** @var array */
47    public $informations = [];
48
49    /** @var array */
50    public $confirmations = [];
51
52    /** @var string|false */
53    public $shopShareDatas = false;
54
55    /** @var array */
56    public $_languages = [];
57
58    /** @var int */
59    public $default_form_language;
60
61    /** @var bool */
62    public $allow_employee_form_lang;
63
64    /** @var string */
65    public $layout = 'layout.tpl';
66
67    /** @var bool */
68    public $bootstrap = false;
69
70    /** @var string|array */
71    protected $meta_title = [];
72
73    /** @var string */
74    public $template = 'content.tpl';
75
76    /** @var string Associated table name */
77    public $table = 'configuration';
78
79    /** @var string */
80    public $list_id;
81
82    /** @var string|false Object identifier inside the associated table */
83    protected $identifier = false;
84
85    /** @var string */
86    protected $identifier_name = 'name';
87
88    /** @var string Associated object class name */
89    public $className;
90
91    /** @var array */
92    public $tabAccess;
93
94    /** @var int Tab id */
95    public $id = -1;
96
97    /** @var bool */
98    public $required_database = false;
99
100    /** @var string Security token */
101    public $token;
102
103    /** @var string "shop" or "group_shop" */
104    public $shopLinkType;
105
106    /** @var string Default ORDER BY clause when `$_orderBy` is not defined */
107    protected $_defaultOrderBy = false;
108
109    /** @var string */
110    protected $_defaultOrderWay = 'ASC';
111
112    /** @var array */
113    public $tpl_form_vars = [];
114
115    /** @var array */
116    public $tpl_list_vars = [];
117
118    /** @var array */
119    public $tpl_delete_link_vars = [];
120
121    /** @var array */
122    public $tpl_option_vars = [];
123
124    /** @var array */
125    public $tpl_view_vars = [];
126
127    /** @var array */
128    public $tpl_required_fields_vars = [];
129
130    /** @var string|null */
131    public $base_tpl_view = null;
132
133    /** @var string|null */
134    public $base_tpl_form = null;
135
136    /** @var bool If you want more fieldsets in the form */
137    public $multiple_fieldsets = false;
138
139    /** @var array|false */
140    public $fields_value = false;
141
142    /** @var array Errors displayed after post processing */
143    public $errors = [];
144
145    /** @var bool Define if the header of the list contains filter and sorting links or not */
146    protected $list_simple_header;
147
148    /** @var array List to be generated */
149    protected $fields_list;
150
151    /** @var array Modules list filters */
152    protected $filter_modules_list = null;
153
154    /** @var array Modules list filters */
155    protected $modules_list = [];
156
157    /** @var array Edit form to be generated */
158    protected $fields_form;
159
160    /** @var array Override of `$fields_form` */
161    protected $fields_form_override;
162
163    /** @var string Override form action */
164    protected $submit_action;
165
166    /** @var array List of option forms to be generated */
167    protected $fields_options = [];
168
169    /** @var string */
170    protected $shopLink;
171
172    /** @var string SQL query */
173    protected $_listsql = '';
174
175    /** @var array Cache for query results */
176    protected $_list = [];
177
178    /** @var string MySQL error */
179    protected $_list_error;
180
181    /** @var string|array Toolbar title */
182    protected $toolbar_title;
183
184    /** @var array List of toolbar buttons */
185    protected $toolbar_btn = null;
186
187    /** @var bool Scrolling toolbar */
188    protected $toolbar_scroll = true;
189
190    /** @var bool Set to false to hide toolbar and page title */
191    protected $show_toolbar = true;
192
193    /** @var bool Set to true to show toolbar and page title for options */
194    protected $show_toolbar_options = false;
195
196    /** @var int Number of results in list */
197    protected $_listTotal = 0;
198
199    /** @var bool Automatically join language table if true */
200    public $lang = false;
201
202    /** @var array WHERE clause determined by filter fields */
203    protected $_filter;
204
205    /** @var string */
206    protected $_filterHaving;
207
208    /** @var array Temporary SQL table WHERE clause determined by filter fields */
209    protected $_tmpTableFilter = '';
210
211    /** @var array Number of results in list per page (used in select field) */
212    protected $_pagination = [20, 50, 100, 300, 1000];
213
214    /** @var int Default number of results in list per page */
215    protected $_default_pagination = 50;
216
217    /** @var string ORDER BY clause determined by field/arrows in list header */
218    protected $_orderBy;
219
220    /** @var string Order way (ASC, DESC) determined by arrows in list header */
221    protected $_orderWay;
222
223    /** @var array List of available actions for each list row - default actions are view, edit, delete, duplicate */
224    protected $actions_available = ['view', 'edit', 'duplicate', 'delete'];
225
226    /** @var array List of required actions for each list row */
227    protected $actions = [];
228
229    /** @var array List of row ids associated with a given action for witch this action have to not be available */
230    protected $list_skip_actions = [];
231
232    /** @var bool Don't show header & footer */
233    protected $lite_display = false;
234
235    /** @var bool List content lines are clickable if true */
236    protected $list_no_link = false;
237
238    /** @var bool */
239    protected $allow_export = false;
240
241    /** @var array Cache for translations */
242    public static $cache_lang = [];
243
244    /** @var array Required_fields to display in the Required Fields form */
245    public $required_fields = [];
246
247    /** @var HelperList */
248    protected $helper;
249
250    /** @var bool */
251    private $allowAnonymous = false;
252
253    /** @var int DELETE access level */
254    const LEVEL_DELETE = 4;
255
256    /** @var int ADD access level */
257    const LEVEL_ADD = 3;
258
259    /** @var int EDIT access level */
260    const LEVEL_EDIT = 2;
261
262    /** @var int VIEW access level */
263    const LEVEL_VIEW = 1;
264
265    /**
266     * Actions to execute on multiple selections.
267     *
268     * Usage:
269     *
270     * array(
271     *      'actionName' => array(
272     *      'text' => $this->trans('Message displayed on the submit button (mandatory)'),
273     *      'confirm' => $this->trans('If set, this confirmation message will pop-up (optional)')),
274     *      'anotherAction' => array(...)
275     * );
276     *
277     * If your action is named 'actionName', you need to have a method named bulkactionName() that will be executed when the button is clicked.
278     *
279     * @var array
280     */
281    protected $bulk_actions;
282
283    /** @var array Ids of the rows selected */
284    protected $boxes;
285
286    /** @var bool Do not automatically select * anymore but select only what is necessary */
287    protected $explicitSelect = false;
288
289    /** @var string Add fields into data query to display list */
290    protected $_select;
291
292    /** @var string Join tables into data query to display list */
293    protected $_join;
294
295    /** @var string Add conditions into data query to display list */
296    protected $_where;
297
298    /** @var string Group rows into data query to display list */
299    protected $_group;
300
301    /** @var string Having rows into data query to display list */
302    protected $_having;
303
304    /** @var string Use SQL_CALC_FOUND_ROWS / FOUND_ROWS to count the number of records */
305    protected $_use_found_rows = true;
306
307    /** @var bool */
308    protected $is_cms = false;
309
310    /** @var string Identifier to use for changing positions in lists (can be omitted if positions cannot be changed) */
311    protected $position_identifier;
312
313    /** @var string|int */
314    protected $position_group_identifier;
315
316    /** @var bool Table records are not deleted but marked as deleted if set to true */
317    protected $deleted = false;
318
319    /** @var bool Is a list filter set */
320    protected $filter;
321
322    /** @var bool */
323    protected $noLink;
324
325    /** @var bool|null */
326    protected $specificConfirmDelete = null;
327
328    /** @var bool */
329    protected $colorOnBackground;
330
331    /** @var bool If true, activates color on hover */
332    protected $row_hover = true;
333
334    /** @var string Action to perform : 'edit', 'view', 'add', ... */
335    protected $action;
336
337    /** @var string */
338    protected $display;
339
340    /** @var array */
341    protected $tab_modules_list = ['default_list' => [], 'slider_list' => []];
342
343    /** @var string */
344    public $tpl_folder;
345
346    /** @var string */
347    protected $bo_theme;
348
349    /** @var bool Redirect or not after a creation */
350    protected $_redirect = true;
351
352    /** @var array Name and directory where class image are located */
353    public $fieldImageSettings = [];
354
355    /** @var string Image type */
356    public $imageType = 'jpg';
357
358    /** @var ObjectModel Instantiation of the class associated with the AdminController */
359    protected $object;
360
361    /** @var int Current object ID */
362    protected $id_object;
363
364    /** @var string Current controller name without suffix */
365    public $controller_name;
366
367    /** @var int */
368    public $multishop_context = -1;
369
370    /** @var false */
371    public $multishop_context_group = true;
372
373    /** @var array Current breadcrumb position as an array of tab names */
374    protected $breadcrumbs;
375
376    /** @var bool Bootstrap variable */
377    public $show_page_header_toolbar = false;
378
379    /** @var string Bootstrap variable */
380    public $page_header_toolbar_title;
381
382    /** @var array|Traversable Bootstrap variable */
383    public $page_header_toolbar_btn = [];
384
385    /** @var bool Bootstrap variable */
386    public $show_form_cancel_button;
387
388    /** @var string */
389    public $admin_webpath;
390
391    /** @var array */
392    protected $list_natives_modules = [];
393
394    /** @var array */
395    protected $list_partners_modules = [];
396
397    /** @var array */
398    public $modals = [];
399
400    /** @var bool */
401    protected $logged_on_addons = false;
402
403    /** @var bool if logged employee has access to AdminImport */
404    protected $can_import = false;
405
406    /** @var string */
407    protected $tabSlug;
408
409    /** @var int Auth cookie lifetime */
410    const AUTH_COOKIE_LIFETIME = 3600;
411
412    /** @var array */
413    public $_conf;
414
415    /** @var float @var */
416    public $timer_start;
417
418    /** @var bool */
419    protected static $is_prestashop_up = true;
420
421    /** @var array */
422    protected $translationsTab = [];
423
424    public function __construct($forceControllerName = '', $default_theme_name = 'default')
425    {
426        global $timer_start;
427        $this->timer_start = $timer_start;
428
429        $this->controller_type = 'admin';
430        $this->controller_name = !empty($forceControllerName) ? $forceControllerName : get_class($this);
431        if (strpos($this->controller_name, 'ControllerOverride')) {
432            $this->controller_name = substr($this->controller_name, 0, -18);
433        }
434        if (strpos($this->controller_name, 'Controller')) {
435            $this->controller_name = substr($this->controller_name, 0, -10);
436        }
437        parent::__construct();
438
439        if ($this->multishop_context == -1) {
440            $this->multishop_context = Shop::CONTEXT_ALL | Shop::CONTEXT_GROUP | Shop::CONTEXT_SHOP;
441        }
442
443        if (defined('_PS_BO_ALL_THEMES_DIR_')) {
444            if (defined('_PS_BO_DEFAULT_THEME_') && _PS_BO_DEFAULT_THEME_
445                && @filemtime(_PS_BO_ALL_THEMES_DIR_ . _PS_BO_DEFAULT_THEME_ . DIRECTORY_SEPARATOR . 'template')) {
446                $default_theme_name = _PS_BO_DEFAULT_THEME_;
447            }
448
449            $this->bo_theme = $default_theme_name;
450            if (!@filemtime(_PS_BO_ALL_THEMES_DIR_ . $this->bo_theme . DIRECTORY_SEPARATOR . 'template')) {
451                $this->bo_theme = 'default';
452            }
453
454            $this->context->employee->bo_theme = (
455                Validate::isLoadedObject($this->context->employee)
456                && $this->context->employee->bo_theme
457            ) ? $this->context->employee->bo_theme : $this->bo_theme;
458
459            $this->bo_css = (
460                Validate::isLoadedObject($this->context->employee)
461                && $this->context->employee->bo_css
462            ) ? $this->context->employee->bo_css : 'theme.css';
463            $this->context->employee->bo_css = $this->bo_css;
464
465            $adminThemeCSSFile = _PS_BO_ALL_THEMES_DIR_ . $this->bo_theme . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . $this->bo_css;
466
467            if (file_exists($adminThemeCSSFile)) {
468                $this->bo_css = 'theme.css';
469            }
470
471            $this->context->smarty->setTemplateDir([
472                _PS_BO_ALL_THEMES_DIR_ . $this->bo_theme . DIRECTORY_SEPARATOR . 'template',
473                _PS_OVERRIDE_DIR_ . 'controllers' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'templates',
474            ]);
475        }
476
477        $this->id = Tab::getIdFromClassName($this->controller_name);
478        $this->token = Tools::getAdminToken($this->controller_name . (int) $this->id . (int) $this->context->employee->id);
479
480        $this->_conf = [
481            1 => $this->trans('Successful deletion.', [], 'Admin.Notifications.Success'),
482            2 => $this->trans('The selection has been successfully deleted.', [], 'Admin.Notifications.Success'),
483            3 => $this->trans('Successful creation.', [], 'Admin.Notifications.Success'),
484            4 => $this->trans('Successful update.', [], 'Admin.Notifications.Success'),
485            5 => $this->trans('The status has been successfully updated.', [], 'Admin.Notifications.Success'),
486            6 => $this->trans('The settings have been successfully updated.', [], 'Admin.Notifications.Success'),
487            7 => $this->trans('The image was successfully deleted.', [], 'Admin.Notifications.Success'),
488            8 => $this->trans('The module was successfully downloaded.', [], 'Admin.Modules.Notification'),
489            9 => $this->trans('The thumbnails were successfully regenerated.', [], 'Admin.Notifications.Success'),
490            10 => $this->trans('The message was successfully sent to the customer.', [], 'Admin.Orderscustomers.Notification'),
491            11 => $this->trans('Comment successfully added.', [], 'Admin.Notifications.Success'),
492            12 => $this->trans('Module(s) installed successfully.', [], 'Admin.Modules.Notification'),
493            13 => $this->trans('Module(s) uninstalled successfully.', [], 'Admin.Modules.Notification'),
494            14 => $this->trans('The translation was successfully copied.', [], 'Admin.International.Notification'),
495            15 => $this->trans('The translations have been successfully added.', [], 'Admin.International.Notification'),
496            16 => $this->trans('The module transplanted successfully to the hook.', [], 'Admin.Modules.Notification'),
497            17 => $this->trans('The module was successfully removed from the hook.', [], 'Admin.Modules.Notification'),
498            18 => $this->trans('Successful upload.', [], 'Admin.Notifications.Success'),
499            19 => $this->trans('Duplication was completed successfully.', [], 'Admin.Notifications.Success'),
500            20 => $this->trans('The translation was added successfully, but the language has not been created.', [], 'Admin.International.Notification'),
501            21 => $this->trans('Module reset successfully.', [], 'Admin.Modules.Notification'),
502            22 => $this->trans('Module deleted successfully.', [], 'Admin.Modules.Notification'),
503            23 => $this->trans('Localization pack imported successfully.', [], 'Admin.International.Notification'),
504            24 => $this->trans('Localization pack imported successfully.', [], 'Admin.International.Notification'),
505            25 => $this->trans('The selected images have successfully been moved.', [], 'Admin.Notifications.Success'),
506            26 => $this->trans('Your cover image selection has been saved.', [], 'Admin.Notifications.Success'),
507            27 => $this->trans('The image\'s shop association has been modified.', [], 'Admin.Notifications.Success'),
508            28 => $this->trans('A zone has been assigned to the selection successfully.', [], 'Admin.Notifications.Success'),
509            29 => $this->trans('Successful upgrade.', [], 'Admin.Notifications.Success'),
510            30 => $this->trans('A partial refund was successfully created.', [], 'Admin.Orderscustomers.Notification'),
511            31 => $this->trans('The discount was successfully generated.', [], 'Admin.Catalog.Notification'),
512            32 => $this->trans('Successfully signed in to PrestaShop Addons.', [], 'Admin.Modules.Notification'),
513        ];
514
515        $this->_error = [
516            1 => $this->trans(
517                'The root category of the shop %shop% is not associated with the current shop. You can\'t access this page. Please change the root category of the shop.',
518                [
519                    '%shop%' => $this->context->shop->name,
520                ],
521                'Admin.Catalog.Notification'
522            ),
523        ];
524
525        if (!$this->identifier) {
526            $this->identifier = 'id_' . $this->table;
527        }
528        if (!$this->_defaultOrderBy) {
529            $this->_defaultOrderBy = $this->identifier;
530        }
531
532        // Fix for homepage
533        if ($this->controller_name == 'AdminDashboard') {
534            $_POST['token'] = $this->token;
535        }
536
537        if (!Shop::isFeatureActive()) {
538            $this->shopLinkType = '';
539        }
540
541        $this->override_folder = Tools::toUnderscoreCase(substr($this->controller_name, 5)) . '/';
542        // Get the name of the folder containing the custom tpl files
543        $this->tpl_folder = Tools::toUnderscoreCase(substr($this->controller_name, 5)) . '/';
544
545        $this->initShopContext();
546
547        if (defined('_PS_ADMIN_DIR_')) {
548            $this->admin_webpath = str_ireplace(_PS_CORE_DIR_, '', _PS_ADMIN_DIR_);
549            $this->admin_webpath = preg_replace('/^' . preg_quote(DIRECTORY_SEPARATOR, '/') . '/', '', $this->admin_webpath);
550        }
551
552        // Check if logged on Addons
553        $this->logged_on_addons = !empty($this->context->cookie->username_addons) && !empty($this->context->cookie->password_addons);
554
555        // Set context mode
556        if (defined('_PS_HOST_MODE_') && _PS_HOST_MODE_) {
557            if (isset($this->context->cookie->is_contributor) && (int) $this->context->cookie->is_contributor === 1) {
558                $this->context->mode = Context::MODE_HOST_CONTRIB;
559            } else {
560                $this->context->mode = Context::MODE_HOST;
561            }
562        } elseif (isset($this->context->cookie->is_contributor) && (int) $this->context->cookie->is_contributor === 1) {
563            $this->context->mode = Context::MODE_STD_CONTRIB;
564        } else {
565            $this->context->mode = Context::MODE_STD;
566        }
567
568        /* Check if logged employee has access to AdminImport controller */
569        $import_access = Profile::getProfileAccess($this->context->employee->id_profile, Tab::getIdFromClassName('AdminImport'));
570        if (is_array($import_access) && isset($import_access['view']) && $import_access['view'] == 1) {
571            $this->can_import = true;
572        }
573
574        $this->context->smarty->assign([
575            'context_mode' => $this->context->mode,
576            'logged_on_addons' => $this->logged_on_addons,
577            'can_import' => $this->can_import,
578        ]);
579    }
580
581    /**
582     * Gets the multistore header and assigns its html content to a smarty variable
583     *
584     * @see PrestaShopBundle\Controller\Admin\MultistoreController
585     *
586     * (the decision to display it or not is taken by the MultistoreController)
587     */
588    public function initMultistoreHeader(): void
589    {
590        if (!isset($this->lockedToAllShopContext)) {
591            return;
592        }
593
594        $this->context->smarty->assign([
595            'multistore_header' => $this->container->get('prestashop.core.admin.multistore')->header($this->lockedToAllShopContext)->getContent(),
596        ]);
597    }
598
599    /**
600     * Set breadcrumbs array for the controller page.
601     *
602     * @param int|null $tab_id
603     * @param array|null $tabs
604     */
605    public function initBreadcrumbs($tab_id = null, $tabs = null)
606    {
607        if (!is_array($tabs)) {
608            $tabs = [];
609        }
610
611        if (null === $tab_id) {
612            $tab_id = $this->id;
613        }
614
615        $tabs = Tab::recursiveTab($tab_id, $tabs);
616
617        $dummy = ['name' => '', 'href' => '', 'icon' => ''];
618        $breadcrumbs2 = [
619            'container' => $dummy,
620            'tab' => $dummy,
621            'action' => $dummy,
622        ];
623        if (!empty($tabs[0])) {
624            $this->addMetaTitle($tabs[0]['name']);
625            $breadcrumbs2['tab']['name'] = $tabs[0]['name'];
626            $breadcrumbs2['tab']['href'] = $this->context->link->getTabLink($tabs[0]);
627            if (!isset($tabs[1])) {
628                $breadcrumbs2['tab']['icon'] = 'icon-' . $tabs[0]['class_name'];
629            }
630        }
631        if (!empty($tabs[1])) {
632            $breadcrumbs2['container']['name'] = $tabs[1]['name'];
633            $breadcrumbs2['container']['href'] = $this->context->link->getTabLink($tabs[1]);
634            $breadcrumbs2['container']['icon'] = 'icon-' . $tabs[1]['class_name'];
635        }
636
637        /* content, edit, list, add, details, options, view */
638        switch ($this->display) {
639            case 'add':
640                $breadcrumbs2['action']['name'] = $this->trans('Add');
641                $breadcrumbs2['action']['icon'] = 'icon-plus';
642
643                break;
644            case 'edit':
645                $breadcrumbs2['action']['name'] = $this->trans('Edit');
646                $breadcrumbs2['action']['icon'] = 'icon-pencil';
647
648                break;
649            case '':
650            case 'list':
651                $breadcrumbs2['action']['name'] = $this->trans('List');
652                $breadcrumbs2['action']['icon'] = 'icon-th-list';
653
654                break;
655            case 'details':
656            case 'view':
657                $breadcrumbs2['action']['name'] = $this->trans('View details');
658                $breadcrumbs2['action']['icon'] = 'icon-zoom-in';
659
660                break;
661            case 'options':
662                $breadcrumbs2['action']['name'] = $this->trans('Options');
663                $breadcrumbs2['action']['icon'] = 'icon-cogs';
664
665                break;
666            case 'generator':
667                $breadcrumbs2['action']['name'] = $this->trans('Generator');
668                $breadcrumbs2['action']['icon'] = 'icon-flask';
669
670                break;
671        }
672
673        $this->context->smarty->assign([
674            'breadcrumbs2' => $breadcrumbs2,
675            'quick_access_current_link_name' => Tools::safeOutput($breadcrumbs2['tab']['name'] . (isset($breadcrumbs2['action']) ? ' - ' . $breadcrumbs2['action']['name'] : '')),
676            'quick_access_current_link_icon' => $breadcrumbs2['container']['icon'],
677        ]);
678
679        /* BEGIN - Backward compatibility < 1.6.0.3 */
680        $this->breadcrumbs[] = $tabs[0]['name'] ?? '';
681        $navigation_pipe = (Configuration::get('PS_NAVIGATION_PIPE') ? Configuration::get('PS_NAVIGATION_PIPE') : '>');
682        $this->context->smarty->assign('navigationPipe', $navigation_pipe);
683        /* END - Backward compatibility < 1.6.0.3 */
684    }
685
686    /**
687     * Set default toolbar_title to admin breadcrumb.
688     */
689    public function initToolbarTitle()
690    {
691        $this->toolbar_title = is_array($this->breadcrumbs) ? array_unique($this->breadcrumbs) : [$this->breadcrumbs];
692
693        switch ($this->display) {
694            case 'edit':
695                $this->toolbar_title[] = $this->trans('Edit');
696                $this->addMetaTitle($this->trans('Edit'));
697
698                break;
699
700            case 'add':
701                $this->toolbar_title[] = $this->trans('Add new');
702                $this->addMetaTitle($this->trans('Add new'));
703
704                break;
705
706            case 'view':
707                $this->toolbar_title[] = $this->trans('View');
708                $this->addMetaTitle($this->trans('View'));
709
710                break;
711        }
712
713        if ($filter = $this->addFiltersToBreadcrumbs()) {
714            $this->toolbar_title[] = $filter;
715        }
716    }
717
718    /**
719     * @return string|void
720     */
721    public function addFiltersToBreadcrumbs()
722    {
723        if ($this->filter && is_array($this->fields_list)) {
724            $filters = [];
725
726            foreach ($this->fields_list as $field => $t) {
727                if (isset($t['filter_key'])) {
728                    $field = $t['filter_key'];
729                }
730
731                if (($val = Tools::getValue($this->table . 'Filter_' . $field)) || $val = $this->context->cookie->{$this->getCookieFilterPrefix() . $this->table . 'Filter_' . $field}) {
732                    if (!is_array($val)) {
733                        $filter_value = '';
734                        if (isset($t['type']) && $t['type'] == 'bool') {
735                            $filter_value = ((bool) $val) ? $this->trans('yes') : $this->trans('no');
736                        } elseif (isset($t['type']) && $t['type'] == 'date' || isset($t['type']) && $t['type'] == 'datetime') {
737                            $date = json_decode($val, true);
738                            if (isset($date[0])) {
739                                $filter_value = $date[0];
740                                if (isset($date[1]) && !empty($date[1])) {
741                                    $filter_value .= ' - ' . $date[1];
742                                }
743                            }
744                        } elseif (is_string($val)) {
745                            $filter_value = htmlspecialchars($val, ENT_QUOTES, 'UTF-8');
746                        }
747                        if (!empty($filter_value)) {
748                            $filters[] = $this->trans('%s: %s', [$t['title'], $filter_value]);
749                        }
750                    } else {
751                        $filter_value = '';
752                        foreach ($val as $v) {
753                            if (is_string($v) && !empty($v)) {
754                                $filter_value .= ' - ' . htmlspecialchars($v, ENT_QUOTES, 'UTF-8');
755                            }
756                        }
757                        $filter_value = ltrim($filter_value, ' -');
758                        if (!empty($filter_value)) {
759                            $filters[] = $this->trans('%s: %s', [$t['title'], $filter_value]);
760                        }
761                    }
762                }
763            }
764
765            if (count($filters)) {
766                return $this->trans('filter by %s', [implode(', ', $filters)]);
767            }
768        }
769    }
770
771    /**
772     * @param string $action
773     * @param bool $disable
774     */
775    public function access($action, $disable = false)
776    {
777        if (empty($this->tabAccess[$action])) {
778            $slugs = [];
779
780            foreach ((array) Access::getAuthorizationFromLegacy($action) as $roleSuffix) {
781                $slugs[] = $this->getTabSlug() . $roleSuffix;
782            }
783
784            $this->tabAccess[$action] = Access::isGranted(
785                $slugs,
786                $this->context->employee->id_profile
787            );
788        }
789
790        return $this->tabAccess[$action];
791    }
792
793    /**
794     * Check rights to view the current tab.
795     *
796     * @param bool $disable
797     *
798     * @return bool
799     */
800    public function viewAccess($disable = false)
801    {
802        return $this->access('view', $disable);
803    }
804
805    /**
806     * Check for security token.
807     *
808     * @return bool
809     */
810    public function checkToken()
811    {
812        if (TokenInUrls::isDisabled() || $this->isAnonymousAllowed()) {
813            return true;
814        }
815
816        $token = Tools::getValue('token');
817        if ($token === $this->token) {
818            return true;
819        }
820
821        if (count($_POST) || !isset($_GET['controller']) || !Validate::isControllerName($_GET['controller']) || !$token) {
822            return false;
823        }
824
825        foreach ($_GET as $key => $value) {
826            if (is_array($value) || !in_array($key, ['controller', 'controllerUri'])) {
827                return false;
828            }
829        }
830
831        $cookie = Context::getContext()->cookie;
832        $whitelist = ['date_add', 'id_lang', 'id_employee', 'email', 'profile', 'passwd', 'remote_addr', 'shopContext', 'collapse_menu', 'checksum'];
833        foreach ($cookie->getAll() as $key => $value) {
834            if (!in_array($key, $whitelist)) {
835                unset($cookie->$key);
836            }
837        }
838
839        $cookie->write();
840
841        return true;
842    }
843
844    /**
845     * Set the filters used for the list display.
846     */
847    protected function getCookieFilterPrefix()
848    {
849        return str_replace(['admin', 'controller'], '', Tools::strtolower(get_class($this)));
850    }
851
852    public function processFilter()
853    {
854        Hook::exec('action' . $this->controller_name . 'ListingFieldsModifier', [
855            'fields' => &$this->fields_list,
856        ]);
857
858        if (!isset($this->list_id)) {
859            $this->list_id = $this->table;
860        }
861
862        $prefix = $this->getCookieFilterPrefix();
863
864        if (isset($this->list_id)) {
865            foreach ($_POST as $key => $value) {
866                if ($value === '') {
867                    unset($this->context->cookie->{$prefix . $key});
868                } elseif (stripos($key, $this->list_id . 'Filter_') === 0) {
869                    $this->context->cookie->{$prefix . $key} = !is_array($value) ? $value : json_encode($value);
870                } elseif (stripos($key, 'submitFilter') === 0) {
871                    $this->context->cookie->$key = !is_array($value) ? $value : json_encode($value);
872                }
873            }
874
875            foreach ($_GET as $key => $value) {
876                if (stripos($key, $this->list_id . 'Filter_') === 0) {
877                    $this->context->cookie->{$prefix . $key} = !is_array($value) ? $value : json_encode($value);
878                } elseif (stripos($key, 'submitFilter') === 0) {
879                    $this->context->cookie->$key = !is_array($value) ? $value : json_encode($value);
880                }
881                if (stripos($key, $this->list_id . 'Orderby') === 0 && Validate::isOrderBy($value)) {
882                    if ($value === '' || $value == $this->_defaultOrderBy) {
883                        unset($this->context->cookie->{$prefix . $key});
884                    } else {
885                        $this->context->cookie->{$prefix . $key} = $value;
886                    }
887                } elseif (stripos($key, $this->list_id . 'Orderway') === 0 && Validate::isOrderWay($value)) {
888                    if ($value === '' || $value == $this->_defaultOrderWay) {
889                        unset($this->context->cookie->{$prefix . $key});
890                    } else {
891                        $this->context->cookie->{$prefix . $key} = $value;
892                    }
893                }
894            }
895        }
896
897        $filters = $this->context->cookie->getFamily($prefix . $this->list_id . 'Filter_');
898        $definition = false;
899        if (isset($this->className) && $this->className) {
900            $definition = ObjectModel::getDefinition($this->className);
901        }
902
903        foreach ($filters as $key => $value) {
904            /* Extracting filters from $_POST on key filter_ */
905            if ($value != null && !strncmp($key, $prefix . $this->list_id . 'Filter_', 7 + Tools::strlen($prefix . $this->list_id))) {
906                $key = Tools::substr($key, 7 + Tools::strlen($prefix . $this->list_id));
907                /* Table alias could be specified using a ! eg. alias!field */
908                $tmp_tab = explode('!', $key);
909                $filter = count($tmp_tab) > 1 ? $tmp_tab[1] : $tmp_tab[0];
910
911                if ($field = $this->filterToField($key, $filter)) {
912                    $type = (array_key_exists('filter_type', $field) ? $field['filter_type'] : (array_key_exists('type', $field) ? $field['type'] : false));
913                    if (($type == 'date' || $type == 'datetime') && is_string($value)) {
914                        $value = json_decode($value, true);
915                    }
916                    $key = isset($tmp_tab[1]) ? $tmp_tab[0] . '.`' . $tmp_tab[1] . '`' : '`' . $tmp_tab[0] . '`';
917
918                    // Assignment by reference
919                    if (array_key_exists('tmpTableFilter', $field)) {
920                        $sql_filter = &$this->_tmpTableFilter;
921                    } elseif (array_key_exists('havingFilter', $field)) {
922                        $sql_filter = &$this->_filterHaving;
923                    } else {
924                        $sql_filter = &$this->_filter;
925                    }
926
927                    /* Only for date filtering (from, to) */
928                    if (is_array($value)) {
929                        if (isset($value[0]) && !empty($value[0])) {
930                            if (!Validate::isDate($value[0])) {
931                                $this->errors[] = $this->trans('The \'From\' date format is invalid (YYYY-MM-DD)', [], 'Admin.Notifications.Error');
932                            } else {
933                                $sql_filter .= ' AND ' . pSQL($key) . ' >= \'' . pSQL(Tools::dateFrom($value[0])) . '\'';
934                            }
935                        }
936
937                        if (isset($value[1]) && !empty($value[1])) {
938                            if (!Validate::isDate($value[1])) {
939                                $this->errors[] = $this->trans('The \'To\' date format is invalid (YYYY-MM-DD)', [], 'Admin.Notifications.Error');
940                            } else {
941                                $sql_filter .= ' AND ' . pSQL($key) . ' <= \'' . pSQL(Tools::dateTo($value[1])) . '\'';
942                            }
943                        }
944                    } else {
945                        $sql_filter .= ' AND ';
946                        $check_key = ($key == $this->identifier || $key == '`' . $this->identifier . '`');
947                        $alias = ($definition && !empty($definition['fields'][$filter]['shop'])) ? 'sa' : 'a';
948
949                        if ($type == 'int' || $type == 'bool') {
950                            $sql_filter .= (($check_key || $key == '`active`') ? $alias . '.' : '') . pSQL($key) . ' = ' . (int) $value . ' ';
951                        } elseif ($type == 'decimal') {
952                            $sql_filter .= ($check_key ? $alias . '.' : '') . pSQL($key) . ' = ' . (float) $value . ' ';
953                        } elseif ($type == 'select') {
954                            $sql_filter .= ($check_key ? $alias . '.' : '') . pSQL($key) . ' = \'' . pSQL($value) . '\' ';
955                        } elseif ($type == 'price') {
956                            $value = (float) str_replace(',', '.', $value);
957                            $sql_filter .= ($check_key ? $alias . '.' : '') . pSQL($key) . ' = ' . pSQL(trim($value)) . ' ';
958                        } else {
959                            $sql_filter .= ($check_key ? $alias . '.' : '') . pSQL($key) . ' LIKE \'%' . pSQL(trim($value)) . '%\' ';
960                        }
961                    }
962                }
963            }
964        }
965    }
966
967    /**
968     * @TODO uses redirectAdmin only if !$this->ajax
969     *
970     * @return ObjectModel|bool
971     */
972    public function postProcess()
973    {
974        try {
975            if ($this->ajax) {
976                // from ajax-tab.php
977                $action = Tools::getValue('action');
978                // no need to use displayConf() here
979                if (!empty($action) && method_exists($this, 'ajaxProcess' . Tools::toCamelCase($action))) {
980                    Hook::exec('actionAdmin' . ucfirst($this->action) . 'Before', ['controller' => $this]);
981                    Hook::exec('action' . get_class($this) . ucfirst($this->action) . 'Before', ['controller' => $this]);
982
983                    $return = $this->{'ajaxProcess' . Tools::toCamelCase($action)}();
984
985                    Hook::exec('actionAdmin' . ucfirst($this->action) . 'After', ['controller' => $this, 'return' => $return]);
986                    Hook::exec('action' . get_class($this) . ucfirst($this->action) . 'After', ['controller' => $this, 'return' => $return]);
987
988                    return $return;
989                } elseif (!empty($action) && $this->controller_name == 'AdminModules' && Tools::getIsset('configure')) {
990                    $module_obj = Module::getInstanceByName(Tools::getValue('configure'));
991                    if (Validate::isLoadedObject($module_obj) && method_exists($module_obj, 'ajaxProcess' . $action)) {
992                        return $module_obj->{'ajaxProcess' . $action}();
993                    }
994                } elseif (method_exists($this, 'ajaxProcess')) {
995                    return $this->ajaxProcess();
996                }
997            } else {
998                // Process list filtering
999                if ($this->filter && $this->action != 'reset_filters') {
1000                    $this->processFilter();
1001                }
1002
1003                if (isset($_POST) && count($_POST) && (int) Tools::getValue('submitFilter' . $this->list_id) || Tools::isSubmit('submitReset' . $this->list_id)) {
1004                    $this->setRedirectAfter(self::$currentIndex . '&token=' . $this->token . (Tools::isSubmit('submitFilter' . $this->list_id) ? '&submitFilter' . $this->list_id . '=' . (int) Tools::getValue('submitFilter' . $this->list_id) : ''));
1005                }
1006
1007                // If the method named after the action exists, call "before" hooks, then call action method, then call "after" hooks
1008                if (!empty($this->action) && method_exists($this, 'process' . ucfirst(Tools::toCamelCase($this->action)))) {
1009                    // Hook before action
1010                    Hook::exec('actionAdmin' . ucfirst($this->action) . 'Before', ['controller' => $this]);
1011                    Hook::exec('action' . get_class($this) . ucfirst($this->action) . 'Before', ['controller' => $this]);
1012                    // Call process
1013                    $return = $this->{'process' . Tools::toCamelCase($this->action)}();
1014
1015                    // Hook After Action
1016                    Hook::exec('actionAdmin' . ucfirst($this->action) . 'After', ['controller' => $this, 'return' => $return]);
1017                    Hook::exec('action' . get_class($this) . ucfirst($this->action) . 'After', ['controller' => $this, 'return' => $return]);
1018
1019                    return $return;
1020                }
1021            }
1022        } catch (PrestaShopException $e) {
1023            $this->errors[] = $e->getMessage();
1024        }
1025
1026        return false;
1027    }
1028
1029    /**
1030     * Object Delete images.
1031     *
1032     * @return ObjectModel|false
1033     */
1034    public function processDeleteImage()
1035    {
1036        if (Validate::isLoadedObject($object = $this->loadObject())) {
1037            if (($object->deleteImage())) {
1038                $redirect = self::$currentIndex . '&update' . $this->table . '&' . $this->identifier . '=' . (int) Tools::getValue($this->identifier) . '&conf=7&token=' . $this->token;
1039                if (!$this->ajax) {
1040                    $this->redirect_after = $redirect;
1041                } else {
1042                    $this->content = 'ok';
1043                }
1044            }
1045        }
1046        $this->errors[] = $this->trans('An error occurred while attempting to delete the image. (cannot load object).', [], 'Admin.Notifications.Error');
1047
1048        return $object;
1049    }
1050
1051    /**
1052     * @param string $text_delimiter
1053     *
1054     * @throws PrestaShopException
1055     */
1056    public function processExport($text_delimiter = '"')
1057    {
1058        // clean buffer
1059        if (ob_get_level() && ob_get_length() > 0) {
1060            ob_clean();
1061        }
1062        $this->getList($this->context->language->id, null, null, 0, false);
1063        if (!count($this->_list)) {
1064            return;
1065        }
1066
1067        header('Content-type: text/csv');
1068        header('Content-Type: application/force-download; charset=UTF-8');
1069        header('Cache-Control: no-store, no-cache');
1070        header('Content-disposition: attachment; filename="' . $this->table . '_' . date('Y-m-d_His') . '.csv"');
1071
1072        $fd = fopen('php://output', 'wb');
1073        $headers = [];
1074        foreach ($this->fields_list as $key => $datas) {
1075            if ('PDF' === $datas['title']) {
1076                unset($this->fields_list[$key]);
1077            } else {
1078                if ('ID' === $datas['title']) {
1079                    $headers[] = strtolower(Tools::htmlentitiesDecodeUTF8($datas['title']));
1080                } else {
1081                    $headers[] = Tools::htmlentitiesDecodeUTF8($datas['title']);
1082                }
1083            }
1084        }
1085        fputcsv($fd, $headers, ';', $text_delimiter);
1086
1087        foreach ($this->_list as $i => $row) {
1088            $content = [];
1089            $path_to_image = false;
1090            foreach ($this->fields_list as $key => $params) {
1091                $field_value = isset($row[$key]) ? Tools::htmlentitiesDecodeUTF8(Tools::nl2br($row[$key])) : '';
1092                if ($key == 'image') {
1093                    if ($params['image'] != 'p' || Configuration::get('PS_LEGACY_IMAGES')) {
1094                        $path_to_image = Tools::getShopDomain(true) . _PS_IMG_ . $params['image'] . '/' . $row['id_' . $this->table] . (isset($row['id_image']) ? '-' . (int) $row['id_image'] : '') . '.' . $this->imageType;
1095                    } else {
1096                        $path_to_image = Tools::getShopDomain(true) . _PS_IMG_ . $params['image'] . '/' . Image::getImgFolderStatic($row['id_image']) . (int) $row['id_image'] . '.' . $this->imageType;
1097                    }
1098                    if ($path_to_image) {
1099                        $field_value = $path_to_image;
1100                    }
1101                }
1102                if (isset($params['callback'])) {
1103                    $callback_obj = (isset($params['callback_object'])) ? $params['callback_object'] : $this->context->controller;
1104                    if (!preg_match('/<([a-z]+)([^<]+)*(?:>(.*)<\/\1>|\s+\/>)/ism', call_user_func_array([$callback_obj, $params['callback']], [$field_value, $row]))) {
1105                        $field_value = call_user_func_array([$callback_obj, $params['callback']], [$field_value, $row]);
1106                    }
1107                }
1108                $content[] = $field_value;
1109            }
1110            fputcsv($fd, $content, ';', $text_delimiter);
1111        }
1112        @fclose($fd);
1113        die;
1114    }
1115
1116    /**
1117     * Object Delete.
1118     *
1119     * @return ObjectModel|false
1120     *
1121     * @throws PrestaShopException
1122     */
1123    public function processDelete()
1124    {
1125        if (Validate::isLoadedObject($object = $this->loadObject())) {
1126            $res = true;
1127            // check if request at least one object with noZeroObject
1128            if (isset($object->noZeroObject) && count(call_user_func([$this->className, $object->noZeroObject])) <= 1) {
1129                $this->errors[] = $this->trans('You need at least one object.', [], 'Admin.Notifications.Error') .
1130                    ' <b>' . $this->table . '</b><br />' .
1131                    $this->trans('You cannot delete all of the items.', [], 'Admin.Notifications.Error');
1132            } elseif (array_key_exists('delete', $this->list_skip_actions) && in_array($object->id, $this->list_skip_actions['delete'])) { //check if some ids are in list_skip_actions and forbid deletion
1133                $this->errors[] = $this->trans('You cannot delete this item.', [], 'Admin.Notifications.Error');
1134            } else {
1135                if ($this->deleted) {
1136                    if (!empty($this->fieldImageSettings)) {
1137                        $res = $object->deleteImage();
1138                    }
1139
1140                    if (!$res) {
1141                        $this->errors[] = $this->trans('Unable to delete associated images.', [], 'Admin.Notifications.Error');
1142                    }
1143
1144                    $object->deleted = true;
1145                    if ($res = $object->update()) {
1146                        $this->redirect_after = self::$currentIndex . '&conf=1&token=' . $this->token;
1147                    }
1148                } elseif ($res = $object->delete()) {
1149                    $this->redirect_after = self::$currentIndex . '&conf=1&token=' . $this->token;
1150                }
1151                $this->errors[] = $this->trans('An error occurred during deletion.', [], 'Admin.Notifications.Error');
1152                if ($res) {
1153                    PrestaShopLogger::addLog(
1154                        $this->trans('%s deletion', [$this->className]),
1155                        1,
1156                        null,
1157                        $this->className,
1158                        (int) $this->object->id,
1159                        true,
1160                        (int) $this->context->employee->id
1161                    );
1162                }
1163            }
1164        } else {
1165            $this->errors[] = $this->trans('An error occurred while deleting the object.', [], 'Admin.Notifications.Error') .
1166                ' <b>' . $this->table . '</b> ' .
1167                $this->trans('(cannot load object)', [], 'Admin.Notifications.Error');
1168        }
1169
1170        return $object;
1171    }
1172
1173    /**
1174     * Call the right method for creating or updating object.
1175     *
1176     * @return ObjectModel|false|void
1177     */
1178    public function processSave()
1179    {
1180        if ($this->id_object) {
1181            $this->object = $this->loadObject();
1182
1183            return $this->processUpdate();
1184        } else {
1185            return $this->processAdd();
1186        }
1187    }
1188
1189    /**
1190     * Object creation.
1191     *
1192     * @return ObjectModel|false
1193     *
1194     * @throws PrestaShopException
1195     */
1196    public function processAdd()
1197    {
1198        if (!isset($this->className) || empty($this->className)) {
1199            return false;
1200        }
1201
1202        $this->validateRules();
1203        if (count($this->errors) <= 0) {
1204            $this->object = new $this->className();
1205
1206            $this->copyFromPost($this->object, $this->table);
1207            $this->beforeAdd($this->object);
1208            if (method_exists($this->object, 'add') && !$this->object->add()) {
1209                $this->errors[] = $this->trans('An error occurred while creating an object.', [], 'Admin.Notifications.Error') .
1210                    ' <b>' . $this->table . ' (' . Db::getInstance()->getMsgError() . ')</b>';
1211            } elseif (($_POST[$this->identifier] = $this->object->id /* voluntary do affectation here */) && $this->postImage($this->object->id) && !count($this->errors) && $this->_redirect) {
1212                PrestaShopLogger::addLog(
1213                    $this->trans('%s addition', [$this->className]),
1214                    1,
1215                    null,
1216                    $this->className,
1217                    (int) $this->object->id,
1218                    true,
1219                    (int) $this->context->employee->id
1220                );
1221                $parent_id = (int) Tools::getValue('id_parent', 1);
1222                $this->afterAdd($this->object);
1223                $this->updateAssoShop($this->object->id);
1224                // Save and stay on same form
1225                if (empty($this->redirect_after) && $this->redirect_after !== false && Tools::isSubmit('submitAdd' . $this->table . 'AndStay')) {
1226                    $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=' . $this->object->id . '&conf=3&update' . $this->table . '&token=' . $this->token;
1227                }
1228                // Save and back to parent
1229                if (empty($this->redirect_after) && $this->redirect_after !== false && Tools::isSubmit('submitAdd' . $this->table . 'AndBackToParent')) {
1230                    $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=' . $parent_id . '&conf=3&token=' . $this->token;
1231                }
1232                // Default behavior (save and back)
1233                if (empty($this->redirect_after) && $this->redirect_after !== false) {
1234                    $this->redirect_after = self::$currentIndex . ($parent_id ? '&' . $this->identifier . '=' . $this->object->id : '') . '&conf=3&token=' . $this->token;
1235                }
1236            }
1237        }
1238
1239        $this->errors = array_unique($this->errors);
1240        if (!empty($this->errors)) {
1241            // if we have errors, we stay on the form instead of going back to the list
1242            $this->display = 'edit';
1243
1244            return false;
1245        }
1246
1247        return $this->object;
1248    }
1249
1250    /**
1251     * Object update.
1252     *
1253     * @return ObjectModel|false|void
1254     *
1255     * @throws PrestaShopException
1256     */
1257    public function processUpdate()
1258    {
1259        /* Checking fields validity */
1260        $this->validateRules();
1261        if (empty($this->errors)) {
1262            $id = (int) Tools::getValue($this->identifier);
1263
1264            /* Object update */
1265            if (isset($id) && !empty($id)) {
1266                /** @var ObjectModel $object */
1267                $object = new $this->className($id);
1268                if (Validate::isLoadedObject($object)) {
1269                    /* Specific to objects which must not be deleted */
1270                    if ($this->deleted && $this->beforeDelete($object)) {
1271                        // Create new one with old objet values
1272                        /** @var ObjectModel $object_new */
1273                        $object_new = $object->duplicateObject();
1274                        if (Validate::isLoadedObject($object_new)) {
1275                            // Update old object to deleted
1276                            $object->deleted = true;
1277                            $object->update();
1278
1279                            // Update new object with post values
1280                            $this->copyFromPost($object_new, $this->table);
1281                            $result = $object_new->update();
1282                            if (Validate::isLoadedObject($object_new)) {
1283                                $this->afterDelete($object_new, $object->id);
1284                            }
1285                        }
1286                    } else {
1287                        $this->copyFromPost($object, $this->table);
1288                        $result = $object->update();
1289                        $this->afterUpdate($object);
1290                    }
1291
1292                    if ($object->id) {
1293                        $this->updateAssoShop($object->id);
1294                    }
1295
1296                    if (!$result) {
1297                        $this->errors[] = $this->trans('An error occurred while updating an object.', [], 'Admin.Notifications.Error') .
1298                            ' <b>' . $this->table . '</b> (' . Db::getInstance()->getMsgError() . ')';
1299                    } elseif ($this->postImage($object->id) && !count($this->errors) && $this->_redirect) {
1300                        $parent_id = (int) Tools::getValue('id_parent', 1);
1301                        // Specific back redirect
1302                        if ($back = Tools::getValue('back')) {
1303                            $this->redirect_after = rawurldecode($back) . '&conf=4';
1304                        }
1305                        // Save and stay on same form
1306                        // @todo on the to following if, we may prefer to avoid override redirect_after previous value
1307                        if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay')) {
1308                            $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=' . $object->id . '&conf=4&update' . $this->table . '&token=' . $this->token;
1309                        }
1310                        // Save and back to parent
1311                        if (Tools::isSubmit('submitAdd' . $this->table . 'AndBackToParent')) {
1312                            $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=' . $parent_id . '&conf=4&token=' . $this->token;
1313                        }
1314
1315                        // Default behavior (save and back)
1316                        if (empty($this->redirect_after) && $this->redirect_after !== false) {
1317                            $this->redirect_after = self::$currentIndex . ($parent_id ? '&' . $this->identifier . '=' . $object->id : '') . '&conf=4&token=' . $this->token;
1318                        }
1319                    }
1320                    PrestaShopLogger::addLog(
1321                        $this->trans('%s modification', [$this->className]),
1322                        1,
1323                        null,
1324                        $this->className,
1325                        (int) $object->id,
1326                        true,
1327                        (int) $this->context->employee->id
1328                    );
1329                } else {
1330                    $this->errors[] = $this->trans('An error occurred while updating an object.', [], 'Admin.Notifications.Error') .
1331                        ' <b>' . $this->table . '</b> ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error');
1332                }
1333            }
1334        }
1335        $this->errors = array_unique($this->errors);
1336        if (!empty($this->errors)) {
1337            // if we have errors, we stay on the form instead of going back to the list
1338            $this->display = 'edit';
1339
1340            return false;
1341        }
1342
1343        if (isset($object)) {
1344            return $object;
1345        }
1346    }
1347
1348    /**
1349     * Change object required fields.
1350     *
1351     * @return ObjectModel
1352     */
1353    public function processUpdateFields()
1354    {
1355        if (!is_array($fields = Tools::getValue('fieldsBox'))) {
1356            $fields = [];
1357        }
1358
1359        /** @var $object ObjectModel */
1360        $object = new $this->className();
1361
1362        if (!$object->addFieldsRequiredDatabase($fields)) {
1363            $this->errors[] = $this->trans('An error occurred when attempting to update the required fields.', [], 'Admin.Notifications.Error');
1364        } else {
1365            $this->redirect_after = self::$currentIndex . '&conf=4&token=' . $this->token;
1366        }
1367
1368        return $object;
1369    }
1370
1371    /**
1372     * Change object status (active, inactive).
1373     *
1374     * @return ObjectModel|false
1375     *
1376     * @throws PrestaShopException
1377     */
1378    public function processStatus()
1379    {
1380        if (Validate::isLoadedObject($object = $this->loadObject())) {
1381            if ($object->toggleStatus()) {
1382                $matches = [];
1383                if (preg_match('/[\?|&]controller=([^&]*)/', (string) $_SERVER['HTTP_REFERER'], $matches) !== false
1384                    && strtolower($matches[1]) != strtolower(preg_replace('/controller/i', '', get_class($this)))) {
1385                    $this->redirect_after = preg_replace('/[\?|&]conf=([^&]*)/i', '', (string) $_SERVER['HTTP_REFERER']);
1386                } else {
1387                    $this->redirect_after = self::$currentIndex . '&token=' . $this->token;
1388                }
1389
1390                $id_category = (($id_category = (int) Tools::getValue('id_category')) && Tools::getValue('id_product')) ? '&id_category=' . $id_category : '';
1391
1392                $page = (int) Tools::getValue('page');
1393                $page = $page > 1 ? '&submitFilter' . $this->table . '=' . (int) $page : '';
1394                $this->redirect_after .= '&conf=5' . $id_category . $page;
1395            } else {
1396                $this->errors[] = $this->trans('An error occurred while updating the status.', [], 'Admin.Notifications.Error');
1397            }
1398        } else {
1399            $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') .
1400                ' <b>' . $this->table . '</b> ' .
1401                $this->trans('(cannot load object)', [], 'Admin.Notifications.Error');
1402        }
1403
1404        return $object;
1405    }
1406
1407    /**
1408     * Change object position.
1409     *
1410     * @return ObjectModel|false
1411     */
1412    public function processPosition()
1413    {
1414        if (!Validate::isLoadedObject($object = $this->loadObject())) {
1415            $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') .
1416                ' <b>' . $this->table . '</b> ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error');
1417        } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) {
1418            $this->errors[] = $this->trans('Failed to update the position.', [], 'Admin.Notifications.Error');
1419        } else {
1420            $id_identifier_str = ($id_identifier = (int) Tools::getValue($this->identifier)) ? '&' . $this->identifier . '=' . $id_identifier : '';
1421            $redirect = self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=5' . $id_identifier_str . '&token=' . $this->token;
1422            $this->redirect_after = $redirect;
1423        }
1424
1425        return $object;
1426    }
1427
1428    /**
1429     * Cancel all filters for this tab.
1430     *
1431     * @param int|null $list_id
1432     */
1433    public function processResetFilters($list_id = null)
1434    {
1435        if ($list_id === null) {
1436            $list_id = isset($this->list_id) ? $this->list_id : $this->table;
1437        }
1438
1439        $prefix = $this->getCookieOrderByPrefix();
1440        $filters = $this->context->cookie->getFamily($prefix . $list_id . 'Filter_');
1441        foreach ($filters as $cookie_key => $filter) {
1442            if (strncmp($cookie_key, $prefix . $list_id . 'Filter_', 7 + Tools::strlen($prefix . $list_id)) == 0) {
1443                $key = substr($cookie_key, 7 + Tools::strlen($prefix . $list_id));
1444                if (is_array($this->fields_list) && array_key_exists($key, $this->fields_list)) {
1445                    $this->context->cookie->$cookie_key = null;
1446                }
1447                unset($this->context->cookie->$cookie_key);
1448            }
1449        }
1450
1451        if (isset($this->context->cookie->{'submitFilter' . $list_id})) {
1452            unset($this->context->cookie->{'submitFilter' . $list_id});
1453        }
1454        if (isset($this->context->cookie->{$prefix . $list_id . 'Orderby'})) {
1455            unset($this->context->cookie->{$prefix . $list_id . 'Orderby'});
1456        }
1457        if (isset($this->context->cookie->{$prefix . $list_id . 'Orderway'})) {
1458            unset($this->context->cookie->{$prefix . $list_id . 'Orderway'});
1459        }
1460
1461        $_POST = [];
1462        $this->_filter = false;
1463        unset(
1464            $this->_filterHaving,
1465            $this->_having
1466        );
1467    }
1468
1469    /**
1470     * Update options and preferences.
1471     */
1472    protected function processUpdateOptions()
1473    {
1474        $this->beforeUpdateOptions();
1475
1476        $languages = Language::getLanguages(false);
1477
1478        $hide_multishop_checkbox = (Shop::getTotalShops(false, null) < 2) ? true : false;
1479        foreach ($this->fields_options as $category_data) {
1480            if (!isset($category_data['fields'])) {
1481                continue;
1482            }
1483
1484            $fields = $category_data['fields'];
1485
1486            foreach ($fields as $field => $values) {
1487                if (isset($values['type']) && $values['type'] == 'selectLang') {
1488                    foreach ($languages as $lang) {
1489                        if (Tools::getValue($field . '_' . strtoupper($lang['iso_code']))) {
1490                            $fields[$field . '_' . strtoupper($lang['iso_code'])] = [
1491                                'type' => 'select',
1492                                'cast' => 'strval',
1493                                'identifier' => 'mode',
1494                                'list' => $values['list'],
1495                            ];
1496                        }
1497                    }
1498                }
1499            }
1500
1501            // Validate fields
1502            foreach ($fields as $field => $values) {
1503                // We don't validate fields with no visibility
1504                if (!$hide_multishop_checkbox && Shop::isFeatureActive() && isset($values['visibility']) && $values['visibility'] > Shop::getContext()) {
1505                    continue;
1506                }
1507
1508                // Check if field is required
1509                if ((!Shop::isFeatureActive() && !empty($values['required']))
1510                    || (Shop::isFeatureActive() && isset($_POST['multishopOverrideOption'][$field]) && !empty($values['required']))) {
1511                    if (isset($values['type']) && $values['type'] == 'textLang') {
1512                        foreach ($languages as $language) {
1513                            if (($value = Tools::getValue($field . '_' . $language['id_lang'])) == false && (string) $value != '0') {
1514                                $this->errors[] = $this->trans('field %s is required.', [$values['title']], 'Admin.Notifications.Error');
1515                            }
1516                        }
1517                    } elseif (($value = Tools::getValue($field)) == false && (string) $value != '0') {
1518                        $this->errors[] = $this->trans('field %s is required.', [$values['title']], 'Admin.Notifications.Error');
1519                    }
1520                }
1521
1522                // Check field validator
1523                if (isset($values['type']) && $values['type'] == 'textLang') {
1524                    foreach ($languages as $language) {
1525                        if (Tools::getValue($field . '_' . $language['id_lang']) && isset($values['validation'])) {
1526                            $values_validation = $values['validation'];
1527                            if (!Validate::$values_validation(Tools::getValue($field . '_' . $language['id_lang']))) {
1528                                $this->errors[] = $this->trans('The %s field is invalid.', [$values['title']], 'Admin.Notifications.Error');
1529                            }
1530                        }
1531                    }
1532                } elseif (Tools::getValue($field) && isset($values['validation'])) {
1533                    $values_validation = $values['validation'];
1534                    if (!Validate::$values_validation(Tools::getValue($field))) {
1535                        $this->errors[] = $this->trans('The %s field is invalid.', [$values['title']], 'Admin.Notifications.Error');
1536                    }
1537                }
1538
1539                // Set default value
1540                if (Tools::getValue($field) === false && isset($values['default'])) {
1541                    $_POST[$field] = $values['default'];
1542                }
1543            }
1544
1545            if (!count($this->errors)) {
1546                foreach ($fields as $key => $options) {
1547                    if (Shop::isFeatureActive() && isset($options['visibility']) && $options['visibility'] > Shop::getContext()) {
1548                        continue;
1549                    }
1550
1551                    if (!$hide_multishop_checkbox && Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_ALL && empty($options['no_multishop_checkbox']) && empty($_POST['multishopOverrideOption'][$key])) {
1552                        Configuration::deleteFromContext($key);
1553
1554                        continue;
1555                    }
1556
1557                    // check if a method updateOptionFieldName is available
1558                    $method_name = 'updateOption' . Tools::toCamelCase($key, true);
1559                    if (method_exists($this, $method_name)) {
1560                        $this->$method_name(Tools::getValue($key));
1561                    } elseif (isset($options['type']) && in_array($options['type'], ['textLang', 'textareaLang'])) {
1562                        $list = [];
1563                        foreach ($languages as $language) {
1564                            $key_lang = Tools::getValue($key . '_' . $language['id_lang']);
1565                            $val = (isset($options['cast']) ? $options['cast']($key_lang) : $key_lang);
1566                            if ($this->validateField($val, $options)) {
1567                                if (Validate::isCleanHtml($val)) {
1568                                    $list[$language['id_lang']] = $val;
1569                                } else {
1570                                    $this->errors[] = $this->trans('Cannot add configuration %1$s for %2$s language', [$key, Language::getIsoById((int) $language['id_lang'])], 'Admin.International.Notification');
1571                                }
1572                            }
1573                        }
1574                        Configuration::updateValue($key, $list, isset($options['validation']) && $options['validation'] == 'isCleanHtml' ? true : false);
1575                    } else {
1576                        $val = (isset($options['cast']) ? $options['cast'](Tools::getValue($key)) : Tools::getValue($key));
1577                        if ($this->validateField($val, $options)) {
1578                            if (Validate::isCleanHtml($val)) {
1579                                Configuration::updateValue($key, $val);
1580                            } else {
1581                                $this->errors[] = $this->trans('Cannot add configuration %s', [$key], 'Admin.Notifications.Error');
1582                            }
1583                        }
1584                    }
1585                }
1586            }
1587        }
1588
1589        $this->display = 'list';
1590        if (empty($this->errors)) {
1591            $this->confirmations[] = $this->_conf[6];
1592        }
1593    }
1594
1595    public function initPageHeaderToolbar()
1596    {
1597        if (empty($this->toolbar_title)) {
1598            $this->initToolbarTitle();
1599        }
1600
1601        if (!is_array($this->toolbar_title)) {
1602            $this->toolbar_title = [$this->toolbar_title];
1603        }
1604
1605        switch ($this->display) {
1606            case 'view':
1607                // Default cancel button - like old back link
1608                $back = Tools::safeOutput(Tools::getValue('back', ''));
1609                if (empty($back)) {
1610                    $back = self::$currentIndex . '&token=' . $this->token;
1611                }
1612                if (!Validate::isCleanHtml($back)) {
1613                    die(Tools::displayError());
1614                }
1615                if (!$this->lite_display) {
1616                    $this->page_header_toolbar_btn['back'] = [
1617                        'href' => $back,
1618                        'desc' => $this->trans('Back to list'),
1619                    ];
1620                }
1621                $obj = $this->loadObject(true);
1622                if (Validate::isLoadedObject($obj) && isset($obj->{$this->identifier_name}) && !empty($obj->{$this->identifier_name})) {
1623                    array_pop($this->toolbar_title);
1624                    array_pop($this->meta_title);
1625                    $this->toolbar_title[] = is_array($obj->{$this->identifier_name}) ? $obj->{$this->identifier_name}[$this->context->employee->id_lang] : $obj->{$this->identifier_name};
1626                    $this->addMetaTitle($this->toolbar_title[count($this->toolbar_title) - 1]);
1627                }
1628
1629                break;
1630            case 'edit':
1631                $obj = $this->loadObject(true);
1632                if (Validate::isLoadedObject($obj) && isset($obj->{$this->identifier_name}) && !empty($obj->{$this->identifier_name})) {
1633                    array_pop($this->toolbar_title);
1634                    array_pop($this->meta_title);
1635                    $this->toolbar_title[] = $this->trans(
1636                        'Edit: %s',
1637                        [
1638                            (is_array($obj->{$this->identifier_name})
1639                                && isset($obj->{$this->identifier_name}[$this->context->employee->id_lang])
1640                            )
1641                                ? $obj->{$this->identifier_name}[$this->context->employee->id_lang]
1642                                : $obj->{$this->identifier_name},
1643                        ]
1644                    );
1645                    $this->addMetaTitle($this->toolbar_title[count($this->toolbar_title) - 1]);
1646                }
1647
1648                break;
1649        }
1650
1651        if (is_array($this->page_header_toolbar_btn)
1652            && $this->page_header_toolbar_btn instanceof Traversable
1653            || count($this->toolbar_title)) {
1654            $this->show_page_header_toolbar = true;
1655        }
1656
1657        if (empty($this->page_header_toolbar_title)) {
1658            $this->page_header_toolbar_title = $this->toolbar_title[count($this->toolbar_title) - 1];
1659        }
1660
1661        $this->context->smarty->assign('help_link', 'https://help.prestashop.com/' . Language::getIsoById($this->context->employee->id_lang) . '/doc/'
1662            . Tools::getValue('controller') . '?version=' . _PS_VERSION_ . '&country=' . Language::getIsoById($this->context->employee->id_lang));
1663    }
1664
1665    /**
1666     * assign default action in toolbar_btn smarty var, if they are not set.
1667     * uses override to specifically add, modify or remove items.
1668     */
1669    public function initToolbar()
1670    {
1671        switch ($this->display) {
1672            case 'add':
1673            case 'edit':
1674                // Default save button - action dynamically handled in javascript
1675                $this->toolbar_btn['save'] = [
1676                    'href' => '#',
1677                    'desc' => $this->trans('Save'),
1678                ];
1679                $back = Tools::safeOutput(Tools::getValue('back', ''));
1680                if (empty($back)) {
1681                    $back = self::$currentIndex . '&token=' . $this->token;
1682                }
1683                if (!Validate::isCleanHtml($back)) {
1684                    die(Tools::displayError());
1685                }
1686                if (!$this->lite_display) {
1687                    $this->toolbar_btn['cancel'] = [
1688                        'href' => $back,
1689                        'desc' => $this->trans('Cancel'),
1690                    ];
1691                }
1692
1693                break;
1694            case 'view':
1695                // Default cancel button - like old back link
1696                $back = Tools::safeOutput(Tools::getValue('back', ''));
1697                if (empty($back)) {
1698                    $back = self::$currentIndex . '&token=' . $this->token;
1699                }
1700                if (!Validate::isCleanHtml($back)) {
1701                    die(Tools::displayError());
1702                }
1703                if (!$this->lite_display) {
1704                    $this->toolbar_btn['back'] = [
1705                        'href' => $back,
1706                        'desc' => $this->trans('Back to list'),
1707                    ];
1708                }
1709
1710                break;
1711            case 'options':
1712                $this->toolbar_btn['save'] = [
1713                    'href' => '#',
1714                    'desc' => $this->trans('Save'),
1715                ];
1716
1717                break;
1718            default:
1719                // list
1720                $this->toolbar_btn['new'] = [
1721                    'href' => self::$currentIndex . '&add' . $this->table . '&token=' . $this->token,
1722                    'desc' => $this->trans('Add new'),
1723                ];
1724                if ($this->allow_export) {
1725                    $this->toolbar_btn['export'] = [
1726                        'href' => self::$currentIndex . '&export' . $this->table . '&token=' . $this->token,
1727                        'desc' => $this->trans('Export'),
1728                    ];
1729                }
1730        }
1731    }
1732
1733    /**
1734     * Load class object using identifier in $_GET (if possible)
1735     * otherwise return an empty object, or die.
1736     *
1737     * @param bool $opt Return an empty object if load fail
1738     *
1739     * @return ObjectModel|false
1740     */
1741    protected function loadObject($opt = false)
1742    {
1743        if (!isset($this->className) || empty($this->className)) {
1744            return true;
1745        }
1746
1747        $id = (int) Tools::getValue($this->identifier);
1748        if ($id && Validate::isUnsignedId($id)) {
1749            if (!$this->object) {
1750                $this->object = new $this->className($id);
1751            }
1752            if (Validate::isLoadedObject($this->object)) {
1753                return $this->object;
1754            }
1755            // throw exception
1756            $this->errors[] = $this->trans('The object cannot be loaded (or found)', [], 'Admin.Notifications.Error');
1757
1758            return false;
1759        } elseif ($opt) {
1760            if (!$this->object) {
1761                $this->object = new $this->className();
1762            }
1763
1764            return $this->object;
1765        } else {
1766            $this->errors[] = $this->trans('The object cannot be loaded (the identifier is missing or invalid)', [], 'Admin.Notifications.Error');
1767
1768            return false;
1769        }
1770    }
1771
1772    /**
1773     * Check if the token is valid, else display a warning page.
1774     *
1775     * @return bool
1776     */
1777    public function checkAccess()
1778    {
1779        if (!$this->checkToken()) {
1780            // If this is an XSS attempt, then we should only display a simple, secure page
1781            // ${1} in the replacement string of the regexp is required,
1782            // because the token may begin with a number and mix up with it (e.g. $17)
1783            $url = preg_replace('/([&?]token=)[^&]*(&.*)?$/', '${1}' . $this->token . '$2', $_SERVER['REQUEST_URI']);
1784            if (false === strpos($url, '?token=') && false === strpos($url, '&token=')) {
1785                $url .= '&token=' . $this->token;
1786            }
1787            if (strpos($url, '?') === false) {
1788                $url = str_replace('&token', '?controller=AdminDashboard&token', $url);
1789            }
1790
1791            $this->context->smarty->assign('url', htmlentities($url));
1792
1793            return false;
1794        }
1795
1796        return true;
1797    }
1798
1799    /**
1800     * @param string $key
1801     * @param string $filter
1802     *
1803     * @return array|false
1804     */
1805    protected function filterToField($key, $filter)
1806    {
1807        if (!isset($this->fields_list)) {
1808            return false;
1809        }
1810
1811        foreach ($this->fields_list as $field) {
1812            if (array_key_exists('filter_key', $field) && $field['filter_key'] == $key) {
1813                return $field;
1814            }
1815        }
1816        if (array_key_exists($filter, $this->fields_list)) {
1817            return $this->fields_list[$filter];
1818        }
1819
1820        return false;
1821    }
1822
1823    public function displayAjax()
1824    {
1825        if ($this->json) {
1826            $this->context->smarty->assign([
1827                'json' => true,
1828                'status' => $this->status,
1829            ]);
1830        }
1831        $this->layout = 'layout-ajax.tpl';
1832        $this->display_header = false;
1833        $this->display_header_javascript = false;
1834        $this->display_footer = false;
1835
1836        return $this->display();
1837    }
1838
1839    protected function redirect()
1840    {
1841        Tools::redirectAdmin($this->redirect_after);
1842    }
1843
1844    /**
1845     * @throws Exception
1846     * @throws SmartyException
1847     */
1848    public function display()
1849    {
1850        $this->context->smarty->assign([
1851            'display_header' => $this->display_header,
1852            'display_header_javascript' => $this->display_header_javascript,
1853            'display_footer' => $this->display_footer,
1854            'js_def' => Media::getJsDef(),
1855            'toggle_navigation_url' => $this->context->link->getAdminLink('AdminEmployees', true, [], [
1856                'action' => 'toggleMenu',
1857            ]),
1858        ]);
1859
1860        // Use page title from meta_title if it has been set else from the breadcrumbs array
1861        if (!$this->meta_title) {
1862            $this->meta_title = $this->toolbar_title;
1863        }
1864        if (is_array($this->meta_title)) {
1865            $this->meta_title = strip_tags(implode(' ' . Configuration::get('PS_NAVIGATION_PIPE') . ' ', $this->meta_title));
1866        }
1867        $this->context->smarty->assign('meta_title', $this->meta_title);
1868
1869        $template_dirs = $this->context->smarty->getTemplateDir();
1870
1871        // Check if header/footer have been overridden
1872        $dir = $this->context->smarty->getTemplateDir(0) . 'controllers' . DIRECTORY_SEPARATOR . trim($this->override_folder, '\\/') . DIRECTORY_SEPARATOR;
1873        $module_list_dir = $this->context->smarty->getTemplateDir(0) . 'helpers' . DIRECTORY_SEPARATOR . 'modules_list' . DIRECTORY_SEPARATOR;
1874
1875        $header_tpl = file_exists($dir . 'header.tpl') ? $dir . 'header.tpl' : 'header.tpl';
1876        $page_header_toolbar = file_exists($dir . 'page_header_toolbar.tpl') ? $dir . 'page_header_toolbar.tpl' : 'page_header_toolbar.tpl';
1877        $footer_tpl = file_exists($dir . 'footer.tpl') ? $dir . 'footer.tpl' : 'footer.tpl';
1878        $modal_module_list = file_exists($module_list_dir . 'modal.tpl') ? $module_list_dir . 'modal.tpl' : '';
1879        $tpl_action = $this->tpl_folder . $this->display . '.tpl';
1880
1881        // Check if action template has been overridden
1882        foreach ($template_dirs as $template_dir) {
1883            if (file_exists($template_dir . DIRECTORY_SEPARATOR . $tpl_action) && $this->display != 'view' && $this->display != 'options') {
1884                if (method_exists($this, $this->display . Tools::toCamelCase($this->className))) {
1885                    $this->{$this->display . Tools::toCamelCase($this->className)}();
1886                }
1887                $this->context->smarty->assign('content', $this->context->smarty->fetch($tpl_action));
1888
1889                break;
1890            }
1891        }
1892
1893        if (!$this->ajax) {
1894            $template = $this->createTemplate($this->template);
1895            $page = $template->fetch();
1896        } else {
1897            $page = $this->content;
1898        }
1899
1900        if ($conf = Tools::getValue('conf')) {
1901            $this->context->smarty->assign('conf', $this->json ? json_encode($this->_conf[(int) $conf]) : $this->_conf[(int) $conf]);
1902        }
1903
1904        if ($error = Tools::getValue('error')) {
1905            $this->context->smarty->assign('error', $this->json ? json_encode($this->_error[(int) $error]) : $this->_error[(int) $error]);
1906        }
1907
1908        foreach (['errors', 'warnings', 'informations', 'confirmations'] as $type) {
1909            if (!is_array($this->$type)) {
1910                $this->$type = (array) $this->$type;
1911            }
1912            $this->context->smarty->assign($type, $this->json ? json_encode(array_unique($this->$type)) : array_unique($this->$type));
1913        }
1914
1915        if ($this->show_page_header_toolbar && !$this->lite_display) {
1916            $this->context->smarty->assign(
1917                [
1918                    'page_header_toolbar' => $this->context->smarty->fetch($page_header_toolbar),
1919                ]
1920            );
1921            if (!empty($modal_module_list)) {
1922                $this->context->smarty->assign(
1923                    [
1924                        'modal_module_list' => $this->context->smarty->fetch($modal_module_list),
1925                    ]
1926                );
1927            }
1928        }
1929
1930        $this->context->smarty->assign('baseAdminUrl', __PS_BASE_URI__ . basename(_PS_ADMIN_DIR_) . '/');
1931
1932        $this->context->smarty->assign(
1933            [
1934                'page' => $this->json ? json_encode($page) : $page,
1935                'header' => $this->context->smarty->fetch($header_tpl),
1936                'footer' => $this->context->smarty->fetch($footer_tpl),
1937            ]
1938        );
1939
1940        $this->smartyOutputContent($this->layout);
1941    }
1942
1943    /**
1944     * Add a warning message to display at the top of the page.
1945     *
1946     * @param string $msg
1947     */
1948    protected function displayWarning($msg)
1949    {
1950        $this->warnings[] = $msg;
1951    }
1952
1953    /**
1954     * Add a info message to display at the top of the page.
1955     *
1956     * @param string $msg
1957     */
1958    protected function displayInformation($msg)
1959    {
1960        $this->informations[] = $msg;
1961    }
1962
1963    /**
1964     * Assign smarty variables for the header.
1965     */
1966    public function initHeader()
1967    {
1968        header('Cache-Control: no-store, no-cache');
1969
1970        $this->context->smarty->assign([
1971            'table' => $this->table,
1972            'current' => self::$currentIndex,
1973            'token' => $this->token,
1974            'host_mode' => (int) defined('_PS_HOST_MODE_'),
1975            'stock_management' => (int) Configuration::get('PS_STOCK_MANAGEMENT'),
1976            'no_order_tip' => $this->getNotificationTip('order'),
1977            'no_customer_tip' => $this->getNotificationTip('customer'),
1978            'no_customer_message_tip' => $this->getNotificationTip('customer_message'),
1979        ]);
1980
1981        if ($this->display_header) {
1982            $this->context->smarty->assign(
1983                'displayBackOfficeHeader',
1984                Hook::exec('displayBackOfficeHeader')
1985            );
1986        }
1987
1988        $this->context->smarty->assign([
1989            'displayBackOfficeTop' => Hook::exec('displayBackOfficeTop'),
1990            'submit_form_ajax' => (int) Tools::getValue('submitFormAjax'),
1991        ]);
1992
1993        // Multishop
1994        $is_multishop = Shop::isFeatureActive();
1995
1996        // Quick access
1997        if ((int) $this->context->employee->id) {
1998            $quick_access = QuickAccess::getQuickAccessesWithToken($this->context->language->id, (int) $this->context->employee->id);
1999        }
2000
2001        $tabs = $this->getTabs();
2002        $currentTabLevel = 0;
2003        foreach ($tabs as $tab) {
2004            $currentTabLevel = isset($tab['current_level']) ? $tab['current_level'] : $currentTabLevel;
2005        }
2006
2007        if (Validate::isLoadedObject($this->context->employee)) {
2008            $accesses = Profile::getProfileAccesses($this->context->employee->id_profile, 'class_name');
2009            $helperShop = new HelperShop();
2010            /* Hooks are voluntary out the initialize array (need those variables already assigned) */
2011            $bo_color = empty($this->context->employee->bo_color) ? '#FFFFFF' : $this->context->employee->bo_color;
2012            $this->context->smarty->assign([
2013                'help_box' => Configuration::get('PS_HELPBOX'),
2014                'round_mode' => Configuration::get('PS_PRICE_ROUND_MODE'),
2015                'brightness' => Tools::getBrightness($bo_color) < 128 ? 'white' : '#383838',
2016                'bo_width' => (int) $this->context->employee->bo_width,
2017                'bo_color' => isset($this->context->employee->bo_color) ? Tools::htmlentitiesUTF8($this->context->employee->bo_color) : null,
2018                'show_new_orders' => Configuration::get('PS_SHOW_NEW_ORDERS') && isset($accesses['AdminOrders']) && $accesses['AdminOrders']['view'],
2019                'show_new_customers' => Configuration::get('PS_SHOW_NEW_CUSTOMERS') && isset($accesses['AdminCustomers']) && $accesses['AdminCustomers']['view'],
2020                'show_new_messages' => Configuration::get('PS_SHOW_NEW_MESSAGES') && isset($accesses['AdminCustomerThreads']) && $accesses['AdminCustomerThreads']['view'],
2021                'employee' => $this->context->employee,
2022                'search_type' => Tools::getValue('bo_search_type'),
2023                'bo_query' => Tools::safeOutput(Tools::stripslashes(Tools::getValue('bo_query'))),
2024                'quick_access' => empty($quick_access) ? false : $quick_access,
2025                'multi_shop' => Shop::isFeatureActive(),
2026                'shop_list' => $helperShop->getRenderedShopList(),
2027                'current_shop_name' => $helperShop->getCurrentShopName(),
2028                'shop' => $this->context->shop,
2029                'shop_group' => new ShopGroup((int) Shop::getContextShopGroupID()),
2030                'is_multishop' => $is_multishop,
2031                'multishop_context' => $this->multishop_context,
2032                'default_tab_link' => $this->context->link->getAdminLink(Tab::getClassNameById((int) Context::getContext()->employee->default_tab)),
2033                'login_link' => $this->context->link->getAdminLink('AdminLogin'),
2034                'logout_link' => $this->context->link->getAdminLink('AdminLogin', true, [], ['logout' => 1]),
2035                'collapse_menu' => isset($this->context->cookie->collapse_menu) ? (int) $this->context->cookie->collapse_menu : 0,
2036            ]);
2037        } else {
2038            $this->context->smarty->assign('default_tab_link', $this->context->link->getAdminLink('AdminDashboard'));
2039        }
2040
2041        // Shop::initialize() in config.php may empty $this->context->shop->virtual_uri so using a new shop instance for getBaseUrl()
2042        $this->context->shop = new Shop((int) $this->context->shop->id);
2043
2044        $this->context->smarty->assign([
2045            'img_dir' => _PS_IMG_,
2046            'iso' => $this->context->language->iso_code,
2047            'class_name' => $this->className,
2048            'iso_user' => $this->context->language->iso_code,
2049            'lang_is_rtl' => $this->context->language->is_rtl,
2050            'country_iso_code' => $this->context->country->iso_code,
2051            'version' => _PS_VERSION_,
2052            'lang_iso' => $this->context->language->iso_code,
2053            'full_language_code' => $this->context->language->language_code,
2054            'full_cldr_language_code' => $this->context->getCurrentLocale()->getCode(),
2055            'link' => $this->context->link,
2056            'shop_name' => Configuration::get('PS_SHOP_NAME'),
2057            'base_url' => $this->context->shop->getBaseURL(true),
2058            'current_parent_id' => (int) Tab::getCurrentParentId(),
2059            'tabs' => $tabs,
2060            'current_tab_level' => $currentTabLevel,
2061            'install_dir_exists' => file_exists(_PS_ADMIN_DIR_ . '/../install'),
2062            'pic_dir' => _THEME_PROD_PIC_DIR_,
2063            'controller_name' => htmlentities(Tools::getValue('controller')),
2064            'currentIndex' => self::$currentIndex,
2065            'bootstrap' => $this->bootstrap,
2066            'default_language' => (int) Configuration::get('PS_LANG_DEFAULT'),
2067            'display_addons_connection' => Tab::checkTabRights(Tab::getIdFromClassName('AdminModulesController')),
2068        ]);
2069    }
2070
2071    private function getNotificationTip($type)
2072    {
2073        $tips = [
2074            'order' => [
2075                $this->trans(
2076                    'Have you checked your [1][2]abandoned carts[/2][/1]?[3]Your next order could be hiding there!',
2077                        [
2078                            '[1]' => '<strong>',
2079                            '[/1]' => '</strong>',
2080                            '[2]' => '<a href="' . $this->context->link->getAdminLink('AdminCarts', true, [], ['action' => 'filterOnlyAbandonedCarts']) . '">',
2081                            '[/2]' => '</a>',
2082                            '[3]' => '<br>',
2083                        ],
2084                        'Admin.Navigation.Notification'
2085                ),
2086            ],
2087            'customer' => [
2088                $this->trans('Are you active on social media these days?', [], 'Admin.Navigation.Notification'),
2089            ],
2090            'customer_message' => [
2091                $this->trans('Seems like all your customers are happy :)', [], 'Admin.Navigation.Notification'),
2092            ],
2093        ];
2094
2095        if (!isset($tips[$type])) {
2096            return '';
2097        }
2098
2099        return $tips[$type][array_rand($tips[$type])];
2100    }
2101
2102    private function getTabs($parentId = 0, $level = 0)
2103    {
2104        $tabs = Tab::getTabs($this->context->language->id, $parentId);
2105        $current_id = Tab::getCurrentParentId($this->controller_name ? $this->controller_name : '');
2106
2107        foreach ($tabs as $index => $tab) {
2108            if (!Tab::checkTabRights($tab['id_tab'])
2109                || !$tab['enabled']
2110                || ($tab['class_name'] == 'AdminStock' && Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') == 0)
2111                || $tab['class_name'] == 'AdminCarrierWizard') {
2112                unset($tabs[$index]);
2113
2114                continue;
2115            }
2116
2117            // tab[class_name] does not contains the "Controller" suffix
2118            if (($tab['class_name'] . 'Controller' == get_class($this)) || ($current_id == $tab['id_tab']) || $tab['class_name'] == $this->controller_name) {
2119                $tabs[$index]['current'] = true;
2120                $tabs[$index]['current_level'] = $level;
2121            } else {
2122                $tabs[$index]['current'] = false;
2123            }
2124            $tabs[$index]['img'] = null;
2125            $tabs[$index]['href'] = $this->context->link->getTabLink($tab);
2126            $tabs[$index]['sub_tabs'] = array_values($this->getTabs($tab['id_tab'], $level + 1));
2127
2128            $subTabHref = $this->getTabLinkFromSubTabs($tabs[$index]['sub_tabs']);
2129            if (!empty($subTabHref)) {
2130                $tabs[$index]['href'] = $subTabHref;
2131            } elseif (0 == $tabs[$index]['id_parent'] && '' == $tabs[$index]['icon']) {
2132                unset($tabs[$index]);
2133            } elseif (empty($tabs[$index]['icon'])) {
2134                $tabs[$index]['icon'] = 'extension';
2135            }
2136
2137            if (array_key_exists($index, $tabs) && array_key_exists('sub_tabs', $tabs[$index])) {
2138                foreach ($tabs[$index]['sub_tabs'] as $sub_tab) {
2139                    if ((int) $sub_tab['current'] == true) {
2140                        $tabs[$index]['current'] = true;
2141                        $tabs[$index]['current_level'] = $sub_tab['current_level'];
2142                    }
2143                }
2144            }
2145        }
2146
2147        return $tabs;
2148    }
2149
2150    /**
2151     * Declare an action to use for each row in the list.
2152     *
2153     * @param string $action
2154     */
2155    public function addRowAction($action)
2156    {
2157        $action = strtolower($action);
2158        $this->actions[] = $action;
2159    }
2160
2161    /**
2162     * Add an action to use for each row in the list.
2163     *
2164     * @param string $action
2165     * @param array $list
2166     */
2167    public function addRowActionSkipList($action, $list)
2168    {
2169        $action = strtolower($action);
2170        $list = (array) $list;
2171
2172        if (array_key_exists($action, $this->list_skip_actions)) {
2173            $this->list_skip_actions[$action] = array_merge($this->list_skip_actions[$action], $list);
2174        } else {
2175            $this->list_skip_actions[$action] = $list;
2176        }
2177    }
2178
2179    /**
2180     * Assign smarty variables for all default views, list and form, then call other init functions.
2181     */
2182    public function initContent()
2183    {
2184        if (!$this->viewAccess()) {
2185            $this->errors[] = $this->trans('You do not have permission to view this.', [], 'Admin.Notifications.Error');
2186
2187            return;
2188        }
2189
2190        if ($this->display == 'edit' || $this->display == 'add') {
2191            if (!$this->loadObject(true)) {
2192                return;
2193            }
2194
2195            $this->content .= $this->renderForm();
2196        } elseif ($this->display == 'view') {
2197            // Some controllers use the view action without an object
2198            if ($this->className) {
2199                $this->loadObject(true);
2200            }
2201            $this->content .= $this->renderView();
2202        } elseif ($this->display == 'details') {
2203            $this->content .= $this->renderDetails();
2204        } elseif (!$this->ajax) {
2205            // FIXME: Sorry. I'm not very proud of this, but no choice... Please wait sf refactoring to solve this.
2206            if (get_class($this) != 'AdminCarriersController') {
2207                $this->content .= $this->renderModulesList();
2208            }
2209            $this->content .= $this->renderKpis();
2210            $this->content .= $this->renderList();
2211            $this->content .= $this->renderOptions();
2212
2213            // if we have to display the required fields form
2214            if ($this->required_database) {
2215                $this->content .= $this->displayRequiredFields();
2216            }
2217        }
2218
2219        $this->context->smarty->assign([
2220            'content' => $this->content,
2221        ]);
2222    }
2223
2224    public function initToolbarFlags()
2225    {
2226        $this->getLanguages();
2227
2228        $this->initToolbar();
2229        $this->initPageHeaderToolbar();
2230
2231        $this->context->smarty->assign([
2232            'maintenance_mode' => !(bool) Configuration::get('PS_SHOP_ENABLE'),
2233            'debug_mode' => (bool) _PS_MODE_DEV_,
2234            'lite_display' => $this->lite_display,
2235            'url_post' => self::$currentIndex . '&token=' . $this->token,
2236            'show_page_header_toolbar' => $this->show_page_header_toolbar,
2237            'page_header_toolbar_title' => $this->page_header_toolbar_title,
2238            'title' => $this->page_header_toolbar_title,
2239            'toolbar_btn' => $this->page_header_toolbar_btn,
2240            'page_header_toolbar_btn' => $this->page_header_toolbar_btn,
2241        ]);
2242    }
2243
2244    /**
2245     * Init tab modules list and add button in toolbar.
2246     *
2247     * @deprecated since 1.7.7.0
2248     */
2249    protected function initTabModuleList()
2250    {
2251        @trigger_error(sprintf('The "%s()" method is deprecated and has no effect since 1.7.7.0', __METHOD__), E_USER_DEPRECATED);
2252    }
2253
2254    /**
2255     * @deprecated since 1.7.7.0
2256     */
2257    protected function addPageHeaderToolBarModulesListButton()
2258    {
2259        @trigger_error(sprintf('The "%s()" method is deprecated and has no effect since 1.7.7.0', __METHOD__), E_USER_DEPRECATED);
2260    }
2261
2262    /**
2263     * @deprecated since 1.7.7.0
2264     */
2265    protected function addToolBarModulesListButton()
2266    {
2267        @trigger_error(sprintf('The "%s()" method is deprecated and has no effect since 1.7.7.0', __METHOD__), E_USER_DEPRECATED);
2268    }
2269
2270    protected function getAdminModulesUrl()
2271    {
2272        return $this->context->link->getAdminLink('AdminModulesCatalog');
2273    }
2274
2275    /**
2276     * @deprecated since 1.7.7.0
2277     */
2278    protected function filterTabModuleList()
2279    {
2280        @trigger_error(sprintf('The "%s()" method is deprecated and has no effect since 1.7.7.0', __METHOD__), E_USER_DEPRECATED);
2281    }
2282
2283    /**
2284     * Initialize the invalid doom page of death.
2285     */
2286    public function initCursedPage()
2287    {
2288        $this->layout = 'invalid_token.tpl';
2289    }
2290
2291    /**
2292     * Assign smarty variables for the footer.
2293     */
2294    public function initFooter()
2295    {
2296        //RTL Support
2297        //rtl.js overrides inline styles
2298        //iso_code.css overrides default fonts for every language (optional)
2299        if ($this->context->language->is_rtl) {
2300            $this->addJS(_PS_JS_DIR_ . 'rtl.js');
2301            $this->addCSS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/css/' . $this->context->language->iso_code . '.css', 'all', false);
2302        }
2303
2304        // We assign js and css files on the last step before display template, because controller can add many js and css files
2305        $this->context->smarty->assign('css_files', $this->css_files);
2306        $this->context->smarty->assign('js_files', array_unique($this->js_files));
2307
2308        $this->context->smarty->assign([
2309            'ps_version' => _PS_VERSION_,
2310            'timer_start' => $this->timer_start,
2311            'iso_is_fr' => strtoupper($this->context->language->iso_code) == 'FR',
2312            'modals' => $this->renderModal(),
2313        ]);
2314    }
2315
2316    /**
2317     * @throws Exception
2318     * @throws SmartyException
2319     */
2320    public function initModal()
2321    {
2322        if ($this->logged_on_addons) {
2323            $this->context->smarty->assign([
2324                'logged_on_addons' => 1,
2325                'username_addons' => $this->context->cookie->username_addons,
2326            ]);
2327        }
2328
2329        $this->context->smarty->assign([
2330            'img_base_path' => __PS_BASE_URI__ . basename(_PS_ADMIN_DIR_) . '/',
2331            'check_url_fopen' => (ini_get('allow_url_fopen') ? 'ok' : 'ko'),
2332            'check_openssl' => (extension_loaded('openssl') ? 'ok' : 'ko'),
2333            'add_permission' => 1,
2334            'addons_register_link' => 'https://addons.prestashop.com/' . $this->context->language->iso_code . '/login?'
2335                . 'email=' . urlencode($this->context->employee->email)
2336                . '&firstname=' . urlencode($this->context->employee->firstname)
2337                . '&lastname=' . urlencode($this->context->employee->lastname)
2338                . '&website=' . urlencode($this->context->shop->getBaseURL())
2339                . '&utm_source=back-office&utm_medium=connect-to-addons'
2340                . '&utm_campaign=back-office-' . Tools::strtoupper($this->context->language->iso_code)
2341                . '&utm_content=' . (defined('_PS_HOST_MODE_') ? 'cloud' : 'download') . '#createnow',
2342            'addons_forgot_password_link' => '//addons.prestashop.com/' . $this->context->language->iso_code . '/forgot-your-password',
2343        ]);
2344
2345        $this->modals[] = [
2346            'modal_id' => 'modal_addons_connect',
2347            'modal_class' => 'modal-md',
2348            'modal_title' => '<i class="icon-puzzle-piece"></i> <a target="_blank" href="https://addons.prestashop.com/'
2349            . '?utm_source=back-office&utm_medium=modules'
2350            . '&utm_campaign=back-office-' . Tools::strtoupper($this->context->language->iso_code)
2351            . '&utm_content=' . (defined('_PS_HOST_MODE_') ? 'cloud' : 'download') . '">PrestaShop Addons</a>',
2352            'modal_content' => $this->context->smarty->fetch('controllers/modules/login_addons.tpl'),
2353        ];
2354    }
2355
2356    /**
2357     * @return string
2358     *
2359     * @throws Exception
2360     * @throws SmartyException
2361     */
2362    public function renderModal()
2363    {
2364        $modal_render = '';
2365        if (is_array($this->modals) && count($this->modals)) {
2366            foreach ($this->modals as $modal) {
2367                $this->context->smarty->assign($modal);
2368                $modal_render .= $this->context->smarty->fetch('modal.tpl');
2369            }
2370        }
2371
2372        return $modal_render;
2373    }
2374
2375    /**
2376     * Was used to display a list of recommended modules.
2377     *
2378     * @param string|bool $tracking_source Source information for URL used by "Install" button
2379     *
2380     * @return string Empty
2381     *
2382     * @deprecated since 1.7.4.0
2383     */
2384    public function renderModulesList($tracking_source = false)
2385    {
2386        return '';
2387    }
2388
2389    /**
2390     * Function used to render the list to display for this controller.
2391     *
2392     * @return string|false
2393     *
2394     * @throws PrestaShopException
2395     */
2396    public function renderList()
2397    {
2398        if (!($this->fields_list && is_array($this->fields_list))) {
2399            return false;
2400        }
2401        $this->getList($this->context->language->id);
2402
2403        // If list has 'active' field, we automatically create bulk action
2404        if (isset($this->fields_list) && is_array($this->fields_list) && array_key_exists('active', $this->fields_list)
2405            && !empty($this->fields_list['active'])) {
2406            if (!is_array($this->bulk_actions)) {
2407                $this->bulk_actions = [];
2408            }
2409
2410            $this->bulk_actions = array_merge([
2411                'enableSelection' => [
2412                    'text' => $this->trans('Enable selection'),
2413                    'icon' => 'icon-power-off text-success',
2414                ],
2415                'disableSelection' => [
2416                    'text' => $this->trans('Disable selection'),
2417                    'icon' => 'icon-power-off text-danger',
2418                ],
2419                'divider' => [
2420                    'text' => 'divider',
2421                ],
2422            ], $this->bulk_actions);
2423        }
2424
2425        $helper = new HelperList();
2426
2427        // Empty list is ok
2428        if (!is_array($this->_list)) {
2429            $this->displayWarning($this->trans('Bad SQL query') . '<br />' . htmlspecialchars($this->_list_error));
2430
2431            return false;
2432        }
2433
2434        $this->setHelperDisplay($helper);
2435        $helper->_default_pagination = $this->_default_pagination;
2436        $helper->_pagination = $this->_pagination;
2437        $helper->tpl_vars = $this->getTemplateListVars();
2438        $helper->tpl_delete_link_vars = $this->tpl_delete_link_vars;
2439
2440        // For compatibility reasons, we have to check standard actions in class attributes
2441        foreach ($this->actions_available as $action) {
2442            if (!in_array($action, $this->actions) && isset($this->$action) && $this->$action) {
2443                $this->actions[] = $action;
2444            }
2445        }
2446
2447        $helper->is_cms = $this->is_cms;
2448        $helper->sql = $this->_listsql;
2449        $list = $helper->generateList($this->_list, $this->fields_list);
2450
2451        return $list;
2452    }
2453
2454    public function getTemplateListVars()
2455    {
2456        return $this->tpl_list_vars;
2457    }
2458
2459    /**
2460     * Override to render the view page.
2461     *
2462     * @return string
2463     */
2464    public function renderView()
2465    {
2466        $helper = new HelperView($this);
2467        $this->setHelperDisplay($helper);
2468        $helper->tpl_vars = $this->getTemplateViewVars();
2469        if (null !== $this->base_tpl_view) {
2470            $helper->base_tpl = $this->base_tpl_view;
2471        }
2472        $view = $helper->generateView();
2473
2474        return $view;
2475    }
2476
2477    public function getTemplateViewVars()
2478    {
2479        return $this->tpl_view_vars;
2480    }
2481
2482    /**
2483     * Override to render the view page.
2484     *
2485     * @return string|false
2486     */
2487    public function renderDetails()
2488    {
2489        return $this->renderList();
2490    }
2491
2492    /**
2493     * Function used to render the form for this controller.
2494     *
2495     * @return string
2496     *
2497     * @throws Exception
2498     * @throws SmartyException
2499     */
2500    public function renderForm()
2501    {
2502        if (!$this->default_form_language) {
2503            $this->getLanguages();
2504        }
2505
2506        if (Tools::getValue('submitFormAjax')) {
2507            $this->content .= $this->context->smarty->fetch('form_submit_ajax.tpl');
2508        }
2509
2510        if ($this->fields_form && is_array($this->fields_form)) {
2511            if (!$this->multiple_fieldsets) {
2512                $this->fields_form = [['form' => $this->fields_form]];
2513            }
2514
2515            // For add a fields via an override of $fields_form, use $fields_form_override
2516            if (is_array($this->fields_form_override) && !empty($this->fields_form_override)) {
2517                $this->fields_form[0]['form']['input'] = array_merge($this->fields_form[0]['form']['input'], $this->fields_form_override);
2518            }
2519
2520            $fields_value = $this->getFieldsValue($this->object);
2521
2522            Hook::exec('action' . $this->controller_name . 'FormModifier', [
2523                'object' => &$this->object,
2524                'fields' => &$this->fields_form,
2525                'fields_value' => &$fields_value,
2526                'form_vars' => &$this->tpl_form_vars,
2527            ]);
2528
2529            $helper = new HelperForm($this);
2530            $this->setHelperDisplay($helper);
2531            $helper->fields_value = $fields_value;
2532            $helper->submit_action = $this->submit_action;
2533            $helper->tpl_vars = $this->getTemplateFormVars();
2534            $helper->show_cancel_button = (isset($this->show_form_cancel_button)) ? $this->show_form_cancel_button : ($this->display == 'add' || $this->display == 'edit');
2535
2536            $back = rawurldecode(Tools::getValue('back', ''));
2537            if (empty($back)) {
2538                $back = self::$currentIndex . '&token=' . $this->token;
2539            }
2540            if (!Validate::isCleanHtml($back)) {
2541                die(Tools::displayError());
2542            }
2543
2544            $helper->back_url = $back;
2545            null !== $this->base_tpl_form ? $helper->base_tpl = $this->base_tpl_form : '';
2546            if ($this->access('view')) {
2547                if (Tools::getValue('back')) {
2548                    $helper->tpl_vars['back'] = Tools::safeOutput(Tools::getValue('back'));
2549                } else {
2550                    $helper->tpl_vars['back'] = Tools::safeOutput(self::$currentIndex . '&token=' . $this->token);
2551                }
2552            }
2553            $form = $helper->generateForm($this->fields_form);
2554
2555            return $form;
2556        }
2557    }
2558
2559    public function getTemplateFormVars()
2560    {
2561        return $this->tpl_form_vars;
2562    }
2563
2564    public function renderKpis()
2565    {
2566    }
2567
2568    /**
2569     * Function used to render the options for this controller.
2570     *
2571     * @return string
2572     */
2573    public function renderOptions()
2574    {
2575        Hook::exec('action' . $this->controller_name . 'OptionsModifier', [
2576            'options' => &$this->fields_options,
2577            'option_vars' => &$this->tpl_option_vars,
2578        ]);
2579
2580        if ($this->fields_options && is_array($this->fields_options)) {
2581            if (isset($this->display) && $this->display != 'options' && $this->display != 'list') {
2582                $this->show_toolbar = false;
2583            } else {
2584                $this->display = 'options';
2585            }
2586
2587            unset($this->toolbar_btn);
2588            $this->initToolbar();
2589            $helper = new HelperOptions($this);
2590            $this->setHelperDisplay($helper);
2591            $helper->id = $this->id;
2592            $helper->tpl_vars = $this->tpl_option_vars;
2593            $options = $helper->generateOptions($this->fields_options);
2594
2595            return $options;
2596        }
2597    }
2598
2599    /**
2600     * This function sets various display options for helper list.
2601     *
2602     * @param Helper $helper
2603     */
2604    public function setHelperDisplay(Helper $helper)
2605    {
2606        if (empty($this->toolbar_title)) {
2607            $this->initToolbarTitle();
2608        }
2609        // tocheck
2610        if ($this->object && $this->object->id) {
2611            $helper->id = $this->object->id;
2612        }
2613
2614        // @todo : move that in Helper
2615        $helper->title = is_array($this->toolbar_title) ? implode(' ' . Configuration::get('PS_NAVIGATION_PIPE') . ' ', $this->toolbar_title) : $this->toolbar_title;
2616        $helper->toolbar_btn = $this->toolbar_btn;
2617        $helper->show_toolbar = $this->show_toolbar;
2618        $helper->toolbar_scroll = $this->toolbar_scroll;
2619        $helper->override_folder = $this->tpl_folder;
2620        $helper->actions = $this->actions;
2621        $helper->simple_header = $this->list_simple_header;
2622        $helper->bulk_actions = $this->bulk_actions;
2623        $helper->currentIndex = self::$currentIndex;
2624        $helper->className = $this->className;
2625        $helper->table = $this->table;
2626        $helper->name_controller = Tools::getValue('controller');
2627        $helper->orderBy = $this->_orderBy;
2628        $helper->orderWay = $this->_orderWay;
2629        $helper->listTotal = $this->_listTotal;
2630        $helper->shopLink = $this->shopLink;
2631        $helper->shopLinkType = $this->shopLinkType;
2632        $helper->identifier = $this->identifier;
2633        $helper->token = $this->token;
2634        $helper->languages = $this->_languages;
2635        $helper->specificConfirmDelete = $this->specificConfirmDelete;
2636        $helper->imageType = $this->imageType;
2637        $helper->no_link = $this->list_no_link;
2638        $helper->colorOnBackground = $this->colorOnBackground;
2639        $helper->ajax_params = (isset($this->ajax_params) ? $this->ajax_params : null);
2640        $helper->default_form_language = $this->default_form_language;
2641        $helper->allow_employee_form_lang = $this->allow_employee_form_lang;
2642        $helper->multiple_fieldsets = $this->multiple_fieldsets;
2643        $helper->row_hover = $this->row_hover;
2644        $helper->position_identifier = $this->position_identifier;
2645        $helper->position_group_identifier = $this->position_group_identifier;
2646        $helper->controller_name = $this->controller_name;
2647        $helper->list_id = isset($this->list_id) ? $this->list_id : $this->table;
2648        $helper->bootstrap = $this->bootstrap;
2649
2650        // For each action, try to add the corresponding skip elements list
2651        $helper->list_skip_actions = $this->list_skip_actions;
2652
2653        $this->helper = $helper;
2654    }
2655
2656    /**
2657     * @deprecated 1.6.0
2658     */
2659    public function setDeprecatedMedia()
2660    {
2661    }
2662
2663    public function setMedia($isNewTheme = false)
2664    {
2665        if ($isNewTheme) {
2666            $this->addCSS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/new-theme/public/theme.css', 'all', 1);
2667            $this->addJS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/new-theme/public/main.bundle.js');
2668
2669            // the multistore dropdown should be called only once, and only if multistore is used
2670            if ($this->container->get('prestashop.adapter.multistore_feature')->isUsed()) {
2671                $this->addJs(__PS_BASE_URI__ . $this->admin_webpath . '/themes/new-theme/public/multistore_dropdown.bundle.js');
2672            }
2673            $this->addJqueryPlugin(['chosen', 'fancybox']);
2674        } else {
2675            //Bootstrap
2676            $this->addCSS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/css/' . $this->bo_css, 'all', 0);
2677            $this->addCSS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/css/vendor/titatoggle-min.css', 'all', 0);
2678            $this->addCSS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/public/theme.css', 'all', 0);
2679
2680            // add Jquery 3 and its migration script
2681            $this->addJs(_PS_JS_DIR_ . 'jquery/jquery-3.5.1.min.js');
2682            $this->addJs(_PS_JS_DIR_ . 'jquery/bo-migrate-mute.min.js');
2683            $this->addJs(_PS_JS_DIR_ . 'jquery/jquery-migrate-3.1.0.min.js');
2684            // implement $.browser object and live method, that has been removed since jquery 1.9
2685            $this->addJs(_PS_JS_DIR_ . 'jquery/jquery.browser-0.1.0.min.js');
2686            $this->addJs(_PS_JS_DIR_ . 'jquery/jquery.live-polyfill-1.1.2.min.js');
2687
2688            $this->addJqueryPlugin(['scrollTo', 'alerts', 'chosen', 'autosize', 'fancybox']);
2689            $this->addJqueryPlugin('growl', null, false);
2690            $this->addJqueryUI(['ui.slider', 'ui.datepicker']);
2691
2692            $this->addJS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/js/vendor/bootstrap.min.js');
2693            $this->addJS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/js/vendor/modernizr.min.js');
2694            $this->addJS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/js/modernizr-loads.js');
2695            $this->addJS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/js/vendor/moment-with-langs.min.js');
2696            $this->addJS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/public/bundle.js');
2697
2698            $this->addJS(_PS_JS_DIR_ . 'jquery/plugins/timepicker/jquery-ui-timepicker-addon.js');
2699
2700            if (!$this->lite_display) {
2701                $this->addJS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/js/help.js');
2702            }
2703
2704            if (!Tools::getValue('submitFormAjax')) {
2705                $this->addJS(_PS_JS_DIR_ . 'admin/notifications.js');
2706            }
2707
2708            if (defined('_PS_HOST_MODE_') && _PS_HOST_MODE_) {
2709                $this->addJS('https://cdn.statuspage.io/se-v2.js');
2710
2711                Media::addJsDefL('status_operational', $this->trans('Operational'));
2712                Media::addJsDefL('status_degraded_performance', $this->trans('Degraded Performance'));
2713                Media::addJsDefL('status_partial_outage', $this->trans('Partial Outage'));
2714                Media::addJsDefL('status_major_outage', $this->trans('Major Outage'));
2715                Media::addJsDef(['host_cluster' => defined('_PS_HOST_CLUSTER_') ? _PS_HOST_CLUSTER_ : 'fr1']);
2716            }
2717
2718            // Specific Admin Theme
2719            $this->addCSS(__PS_BASE_URI__ . $this->admin_webpath . '/themes/' . $this->bo_theme . '/css/overrides.css', 'all', PHP_INT_MAX);
2720        }
2721
2722        $this->addJS([
2723            _PS_JS_DIR_ . 'admin.js?v=' . _PS_VERSION_, // TODO: SEE IF REMOVABLE
2724            __PS_BASE_URI__ . $this->admin_webpath . '/themes/new-theme/public/cldr.bundle.js',
2725            _PS_JS_DIR_ . 'tools.js?v=' . _PS_VERSION_,
2726            __PS_BASE_URI__ . $this->admin_webpath . '/public/bundle.js',
2727        ]);
2728
2729        Media::addJsDef([
2730            'changeFormLanguageUrl' => $this->context->link->getAdminLink(
2731                'AdminEmployees',
2732                true,
2733                [],
2734                ['action' => 'formLanguage']
2735            ),
2736        ]);
2737        Media::addJsDef(['host_mode' => (defined('_PS_HOST_MODE_') && _PS_HOST_MODE_)]);
2738        Media::addJsDef(['baseDir' => __PS_BASE_URI__]);
2739        Media::addJsDef(['baseAdminDir' => __PS_BASE_URI__ . basename(_PS_ADMIN_DIR_) . '/']);
2740        Media::addJsDef(['currency' => [
2741            'iso_code' => Context::getContext()->currency->iso_code,
2742            'sign' => Context::getContext()->currency->sign,
2743            'name' => Context::getContext()->currency->name,
2744            'format' => Context::getContext()->currency->format,
2745        ]]);
2746        Media::addJsDef(
2747            [
2748                'currency_specifications' => $this->preparePriceSpecifications($this->context),
2749                'number_specifications' => $this->prepareNumberSpecifications($this->context),
2750            ]
2751        );
2752
2753        Media::addJsDef([
2754            'prestashop' => [
2755                'debug' => _PS_MODE_DEV_,
2756            ],
2757        ]);
2758
2759        // Execute Hook AdminController SetMedia
2760        Hook::exec('actionAdminControllerSetMedia');
2761    }
2762
2763    /**
2764     * Non-static method which uses AdminController::translate().
2765     *
2766     * @deprecated use Context::getContext()->getTranslator()->trans($id, $parameters, $domain, $locale); instead
2767     *
2768     * @param string $string Term or expression in english
2769     * @param string|null $class Name of the class
2770     * @param bool $addslashes If set to true, the return value will pass through addslashes(). Otherwise, stripslashes().
2771     * @param bool $htmlentities If set to true(default), the return value will pass through htmlentities($string, ENT_QUOTES, 'utf-8')
2772     *
2773     * @return string the translation if available, or the english default text
2774     */
2775    protected function l($string, $class = null, $addslashes = false, $htmlentities = true)
2776    {
2777        $translated = $this->translator->trans($string);
2778        if ($translated !== $string) {
2779            return $translated;
2780        }
2781
2782        if ($class === null || $class == 'AdminTab') {
2783            $class = substr(get_class($this), 0, -10);
2784        } elseif (strtolower(substr($class, -10)) == 'controller') {
2785            /* classname has changed, from AdminXXX to AdminXXXController, so we remove 10 characters and we keep same keys */
2786            $class = substr($class, 0, -10);
2787        }
2788
2789        return Translate::getAdminTranslation($string, $class, $addslashes, $htmlentities);
2790    }
2791
2792    /**
2793     * Init context and dependencies, handles POST and GET.
2794     */
2795    public function init()
2796    {
2797        Hook::exec(
2798            'actionAdminControllerInitBefore',
2799            [
2800                'controller' => $this,
2801            ]
2802        );
2803        parent::init();
2804
2805        if (Tools::getValue('ajax')) {
2806            $this->ajax = '1';
2807        }
2808
2809        if (null === $this->context->link) {
2810            $protocol_link = (Tools::usingSecureMode() && Configuration::get('PS_SSL_ENABLED')) ? 'https://' : 'http://';
2811            $protocol_content = (Tools::usingSecureMode() && Configuration::get('PS_SSL_ENABLED')) ? 'https://' : 'http://';
2812            $this->context->link = new Link($protocol_link, $protocol_content);
2813        }
2814
2815        if (isset($_GET['logout'])) {
2816            $this->context->employee->logout();
2817        }
2818        if (isset(Context::getContext()->cookie->last_activity)) {
2819            if ($this->context->cookie->last_activity + self::AUTH_COOKIE_LIFETIME < time()) {
2820                $this->context->employee->logout();
2821            } else {
2822                $this->context->cookie->last_activity = time();
2823            }
2824        }
2825
2826        if (
2827            !$this->isAnonymousAllowed()
2828            && (
2829                $this->controller_name != 'AdminLogin'
2830                && (
2831                    !isset($this->context->employee)
2832                    || !$this->context->employee->isLoggedBack()
2833                )
2834            )
2835        ) {
2836            if (isset($this->context->employee)) {
2837                $this->context->employee->logout();
2838            }
2839            $email = false;
2840            if (Tools::getValue('email') && Validate::isEmail(Tools::getValue('email'))) {
2841                $email = Tools::getValue('email');
2842            }
2843            Tools::redirectAdmin($this->context->link->getAdminLink('AdminLogin') . ((!isset($_GET['logout']) && $this->controller_name != 'AdminNotFound' && Tools::getValue('controller')) ? '&redirect=' . $this->controller_name : '') . ($email ? '&email=' . $email : ''));
2844        }
2845
2846        // Set current index
2847        $current_index = 'index.php' . (($controller = Tools::getValue('controller')) ? '?controller=' . $controller : '');
2848        if ($back = Tools::getValue('back')) {
2849            $current_index .= '&back=' . urlencode($back);
2850        }
2851        self::$currentIndex = $current_index;
2852
2853        if ((int) Tools::getValue('liteDisplaying')) {
2854            $this->display_header = false;
2855            $this->display_header_javascript = true;
2856            $this->display_footer = false;
2857            $this->content_only = false;
2858            $this->lite_display = true;
2859        }
2860
2861        if ($this->ajax && method_exists($this, 'ajaxPreprocess')) {
2862            $this->ajaxPreProcess();
2863        }
2864
2865        Employee::setLastConnectionDate($this->context->employee->id);
2866
2867        $this->initProcess();
2868        $this->initMultistoreHeader();
2869        $this->initBreadcrumbs();
2870        $this->initModal();
2871        $this->initToolbarFlags();
2872        $this->initNotifications();
2873        Hook::exec(
2874            'actionAdminControllerInitAfter',
2875            [
2876                'controller' => $this,
2877            ]
2878        );
2879    }
2880
2881    /**
2882     * Sets the smarty variables and js defs used to show / hide some notifications.
2883     */
2884    public function initNotifications()
2885    {
2886        $notificationsSettings = [
2887            'show_new_orders' => Configuration::get('PS_SHOW_NEW_ORDERS'),
2888            'show_new_customers' => Configuration::get('PS_SHOW_NEW_CUSTOMERS'),
2889            'show_new_messages' => Configuration::get('PS_SHOW_NEW_MESSAGES'),
2890        ];
2891        $this->context->smarty->assign($notificationsSettings);
2892
2893        Media::addJsDef($notificationsSettings);
2894    }
2895
2896    /**
2897     * @throws PrestaShopException
2898     */
2899    public function initShopContext()
2900    {
2901        // Do not initialize context when the shop is not installed
2902        if (defined('PS_INSTALLATION_IN_PROGRESS')) {
2903            return;
2904        }
2905
2906        // Change shop context ?
2907        if (Shop::isFeatureActive() && Tools::getValue('setShopContext') !== false) {
2908            $this->context->cookie->shopContext = Tools::getValue('setShopContext');
2909            $url = parse_url($_SERVER['REQUEST_URI']);
2910            $query = (isset($url['query'])) ? $url['query'] : '';
2911            parse_str($query, $parse_query);
2912            unset($parse_query['setShopContext'], $parse_query['conf']);
2913            $http_build_query = http_build_query($parse_query, '', '&');
2914            $this->redirect_after = $url['path'] . ($http_build_query ? '?' . $http_build_query : '');
2915        } elseif (!Shop::isFeatureActive()) {
2916            $this->context->cookie->shopContext = 's-' . (int) Configuration::get('PS_SHOP_DEFAULT');
2917        } elseif (Shop::getTotalShops(false, null) < 2 && $this->context->employee->isLoggedBack()) {
2918            $this->context->cookie->shopContext = 's-' . (int) $this->context->employee->getDefaultShopID();
2919        }
2920
2921        $shop_id = null;
2922        Shop::setContext(Shop::CONTEXT_ALL);
2923        if ($this->context->cookie->shopContext && $this->context->employee->isLoggedBack()) {
2924            $split = explode('-', $this->context->cookie->shopContext);
2925            if (count($split) == 2) {
2926                if ($split[0] == 'g') {
2927                    if ($this->context->employee->hasAuthOnShopGroup((int) $split[1])) {
2928                        Shop::setContext(Shop::CONTEXT_GROUP, (int) $split[1]);
2929                    } else {
2930                        $shop_id = (int) $this->context->employee->getDefaultShopID();
2931                        Shop::setContext(Shop::CONTEXT_SHOP, $shop_id);
2932                    }
2933                } elseif (Shop::getShop($split[1]) && $this->context->employee->hasAuthOnShop($split[1])) {
2934                    $shop_id = (int) $split[1];
2935                    Shop::setContext(Shop::CONTEXT_SHOP, $shop_id);
2936                } else {
2937                    $shop_id = (int) $this->context->employee->getDefaultShopID();
2938                    Shop::setContext(Shop::CONTEXT_SHOP, $shop_id);
2939                }
2940            }
2941        }
2942
2943        // Check multishop context and set right context if need
2944        if (!($this->multishop_context & Shop::getContext())) {
2945            if (Shop::getContext() == Shop::CONTEXT_SHOP && !($this->multishop_context & Shop::CONTEXT_SHOP)) {
2946                Shop::setContext(Shop::CONTEXT_GROUP, Shop::getContextShopGroupID());
2947            }
2948            if (Shop::getContext() == Shop::CONTEXT_GROUP && !($this->multishop_context & Shop::CONTEXT_GROUP)) {
2949                Shop::setContext(Shop::CONTEXT_ALL);
2950            }
2951        }
2952
2953        // Replace existing shop if necessary
2954        if (!$shop_id) {
2955            $this->context->shop = new Shop((int) Configuration::get('PS_SHOP_DEFAULT'));
2956        } elseif ($this->context->shop->id != $shop_id) {
2957            $this->context->shop = new Shop((int) $shop_id);
2958        }
2959
2960        // Replace current default country
2961        $this->context->country = new Country((int) Configuration::get('PS_COUNTRY_DEFAULT'));
2962        $this->context->currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT'));
2963    }
2964
2965    /**
2966     * Retrieve GET and POST value and translate them to actions.
2967     */
2968    public function initProcess()
2969    {
2970        if (!isset($this->list_id)) {
2971            $this->list_id = $this->table;
2972        }
2973
2974        // Manage list filtering
2975        if (Tools::isSubmit('submitFilter' . $this->list_id)
2976            || $this->context->cookie->{'submitFilter' . $this->list_id} !== false
2977            || Tools::getValue($this->list_id . 'Orderby')
2978            || Tools::getValue($this->list_id . 'Orderway')) {
2979            $this->filter = true;
2980        }
2981
2982        $this->id_object = (int) Tools::getValue($this->identifier);
2983
2984        /* Delete object image */
2985        if (isset($_GET['deleteImage'])) {
2986            if ($this->access('delete')) {
2987                $this->action = 'delete_image';
2988            } else {
2989                $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error');
2990            }
2991        } elseif (isset($_GET['delete' . $this->table])) {
2992            /* Delete object */
2993            if ($this->access('delete')) {
2994                $this->action = 'delete';
2995            } else {
2996                $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error');
2997            }
2998        } elseif ((isset($_GET['status' . $this->table]) || isset($_GET['status'])) && Tools::getValue($this->identifier)) {
2999            /* Change object status (active, inactive) */
3000            if ($this->access('edit')) {
3001                $this->action = 'status';
3002            } else {
3003                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
3004            }
3005        } elseif (isset($_GET['position'])) {
3006            /* Move an object */
3007            if ($this->access('edit') == '1') {
3008                $this->action = 'position';
3009            } else {
3010                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
3011            }
3012        } elseif (Tools::isSubmit('submitAdd' . $this->table)
3013                 || Tools::isSubmit('submitAdd' . $this->table . 'AndStay')
3014                 || Tools::isSubmit('submitAdd' . $this->table . 'AndPreview')
3015                 || Tools::isSubmit('submitAdd' . $this->table . 'AndBackToParent')) {
3016            // case 1: updating existing entry
3017            if ($this->id_object) {
3018                if ($this->access('edit')) {
3019                    $this->action = 'save';
3020                    if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay')) {
3021                        $this->display = 'edit';
3022                    } else {
3023                        $this->display = 'list';
3024                    }
3025                } else {
3026                    $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
3027                }
3028            } else {
3029                // case 2: creating new entry
3030                if ($this->access('add')) {
3031                    $this->action = 'save';
3032                    if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay')) {
3033                        $this->display = 'edit';
3034                    } else {
3035                        $this->display = 'list';
3036                    }
3037                } else {
3038                    $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error');
3039                }
3040            }
3041        } elseif (isset($_GET['add' . $this->table])) {
3042            if ($this->access('add')) {
3043                $this->action = 'new';
3044                $this->display = 'add';
3045            } else {
3046                $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error');
3047            }
3048        } elseif (isset($_GET['update' . $this->table], $_GET[$this->identifier])) {
3049            $this->display = 'edit';
3050            if (!$this->access('edit')) {
3051                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
3052            }
3053        } elseif (isset($_GET['view' . $this->table])) {
3054            if ($this->access('view')) {
3055                $this->display = 'view';
3056                $this->action = 'view';
3057            } else {
3058                $this->errors[] = $this->trans('You do not have permission to view this.', [], 'Admin.Notifications.Error');
3059            }
3060        } elseif (isset($_GET['details' . $this->table])) {
3061            if ($this->access('view')) {
3062                $this->display = 'details';
3063                $this->action = 'details';
3064            } else {
3065                $this->errors[] = $this->trans('You do not have permission to view this.', [], 'Admin.Notifications.Error');
3066            }
3067        } elseif (isset($_GET['export' . $this->table])) {
3068            if ($this->access('view')) {
3069                $this->action = 'export';
3070            }
3071        } elseif (isset($_POST['submitReset' . $this->list_id])) {
3072            /* Cancel all filters for this tab */
3073            $this->action = 'reset_filters';
3074        } elseif (Tools::isSubmit('submitOptions' . $this->table) || Tools::isSubmit('submitOptions')) {
3075            /* Submit options list */
3076            $this->display = 'options';
3077            if ($this->access('edit')) {
3078                $this->action = 'update_options';
3079            } else {
3080                $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
3081            }
3082        } elseif (Tools::getValue('action') && method_exists($this, 'process' . ucfirst(Tools::toCamelCase(Tools::getValue('action'))))) {
3083            $this->action = Tools::getValue('action');
3084        } elseif (Tools::isSubmit('submitFields') && $this->required_database && $this->access('add') && $this->access('delete')) {
3085            $this->action = 'update_fields';
3086        } elseif (is_array($this->bulk_actions)) {
3087            $submit_bulk_actions = array_merge([
3088                'enableSelection' => [
3089                    'text' => $this->trans('Enable selection'),
3090                    'icon' => 'icon-power-off text-success',
3091                ],
3092                'disableSelection' => [
3093                    'text' => $this->trans('Disable selection'),
3094                    'icon' => 'icon-power-off text-danger',
3095                ],
3096            ], $this->bulk_actions);
3097            foreach ($submit_bulk_actions as $bulk_action => $params) {
3098                if (Tools::isSubmit('submitBulk' . $bulk_action . $this->table) || Tools::isSubmit('submitBulk' . $bulk_action)) {
3099                    if ($bulk_action === 'delete') {
3100                        if ($this->access('delete')) {
3101                            $this->action = 'bulk' . $bulk_action;
3102                            $this->boxes = Tools::getValue($this->table . 'Box');
3103                            if (empty($this->boxes) && $this->table == 'attribute') {
3104                                $this->boxes = Tools::getValue($this->table . '_valuesBox');
3105                            }
3106                        } else {
3107                            $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error');
3108                        }
3109
3110                        break;
3111                    } elseif ($this->access('edit')) {
3112                        $this->action = 'bulk' . $bulk_action;
3113                        $this->boxes = Tools::getValue($this->table . 'Box');
3114                    } else {
3115                        $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
3116                    }
3117
3118                    break;
3119                } elseif (Tools::isSubmit('submitBulk')) {
3120                    if ($bulk_action === 'delete') {
3121                        if ($this->access('delete')) {
3122                            $this->action = 'bulk' . $bulk_action;
3123                            $this->boxes = Tools::getValue($this->table . 'Box');
3124                        } else {
3125                            $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error');
3126                        }
3127
3128                        break;
3129                    } elseif ($this->access('edit')) {
3130                        $this->action = 'bulk' . Tools::getValue('select_submitBulk');
3131                        $this->boxes = Tools::getValue($this->table . 'Box');
3132                    } else {
3133                        $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error');
3134                    }
3135
3136                    break;
3137                }
3138            }
3139        } elseif (!empty($this->fields_options) && empty($this->fields_list)) {
3140            $this->display = 'options';
3141        }
3142    }
3143
3144    /**
3145     * Get the current objects' list form the database.
3146     *
3147     * @param int $id_lang Language used for display
3148     * @param string|null $order_by ORDER BY clause
3149     * @param string|null $order_way Order way (ASC, DESC)
3150     * @param int $start Offset in LIMIT clause
3151     * @param int|null $limit Row count in LIMIT clause
3152     * @param int|bool $id_lang_shop
3153     *
3154     * @throws PrestaShopDatabaseException
3155     * @throws PrestaShopException
3156     */
3157    public function getList(
3158        $id_lang,
3159        $order_by = null,
3160        $order_way = null,
3161        $start = 0,
3162        $limit = null,
3163        $id_lang_shop = false
3164    ) {
3165        Hook::exec('action' . $this->controller_name . 'ListingFieldsModifier', [
3166            'select' => &$this->_select,
3167            'join' => &$this->_join,
3168            'where' => &$this->_where,
3169            'group_by' => &$this->_group,
3170            'order_by' => &$this->_orderBy,
3171            'order_way' => &$this->_orderWay,
3172            'fields' => &$this->fields_list,
3173        ]);
3174
3175        if (!isset($this->list_id)) {
3176            $this->list_id = $this->table;
3177        }
3178
3179        if (!Validate::isTableOrIdentifier($this->table)) {
3180            throw new PrestaShopException(sprintf('Table name %s is invalid:', $this->table));
3181        }
3182
3183        /* Check params validity */
3184        if (!is_numeric($start) || !Validate::isUnsignedId($id_lang)) {
3185            throw new PrestaShopException('get list params is not valid');
3186        }
3187
3188        $limit = $this->checkSqlLimit($limit);
3189
3190        /* Determine offset from current page */
3191        $start = 0;
3192        if ((int) Tools::getValue('submitFilter' . $this->list_id)) {
3193            $start = ((int) Tools::getValue('submitFilter' . $this->list_id) - 1) * $limit;
3194        } elseif (
3195            empty($start)
3196            && isset($this->context->cookie->{$this->list_id . '_start'})
3197            && Tools::isSubmit('export' . $this->table)
3198        ) {
3199            $start = $this->context->cookie->{$this->list_id . '_start'};
3200        }
3201
3202        // Either save or reset the offset in the cookie
3203        if ($start) {
3204            $this->context->cookie->{$this->list_id . '_start'} = $start;
3205        } elseif (isset($this->context->cookie->{$this->list_id . '_start'})) {
3206            unset($this->context->cookie->{$this->list_id . '_start'});
3207        }
3208
3209        /* Cache */
3210        $this->_lang = (int) $id_lang;
3211
3212        // Add SQL shop restriction
3213        $select_shop = '';
3214        if ($this->shopLinkType) {
3215            $select_shop = ', shop.name as shop_name ';
3216        }
3217
3218        if ($this->multishop_context && Shop::isTableAssociated($this->table) && !empty($this->className)) {
3219            if (Shop::getContext() != Shop::CONTEXT_ALL || !$this->context->employee->isSuperAdmin()) {
3220                $test_join = !preg_match('#`?' . preg_quote(_DB_PREFIX_ . $this->table . '_shop') . '`? *sa#', $this->_join);
3221                if (Shop::isFeatureActive() && $test_join && Shop::isTableAssociated($this->table)) {
3222                    $this->_where .= ' AND EXISTS (
3223                        SELECT 1
3224                        FROM `' . _DB_PREFIX_ . $this->table . '_shop` sa
3225                        WHERE a.`' . bqSQL($this->identifier) . '` = sa.`' . bqSQL($this->identifier) . '`
3226                         AND sa.id_shop IN (' . implode(', ', Shop::getContextListShopID()) . ')
3227                    )';
3228                }
3229            }
3230        }
3231
3232        $fromClause = $this->getFromClause();
3233        $joinClause = $this->getJoinClause($id_lang, $id_lang_shop);
3234        $whereClause = $this->getWhereClause();
3235        $orderByClause = $this->getOrderByClause($order_by, $order_way);
3236
3237        $shouldLimitSqlResults = $this->shouldLimitSqlResults($limit);
3238
3239        do {
3240            $this->_listsql = '';
3241
3242            if ($this->explicitSelect) {
3243                foreach ($this->fields_list as $key => $array_value) {
3244                    // Add it only if it is not already in $this->_select
3245                    if (isset($this->_select) && preg_match('/[\s]`?' . preg_quote($key, '/') . '`?\s*,/', $this->_select)) {
3246                        continue;
3247                    }
3248
3249                    if (isset($array_value['filter_key'])) {
3250                        $this->_listsql .= str_replace('!', '.`', $array_value['filter_key']) . '` AS `' . $key . '`, ';
3251                    } elseif ($key == 'id_' . $this->table) {
3252                        $this->_listsql .= 'a.`' . bqSQL($key) . '`, ';
3253                    } elseif ($key != 'image' && !preg_match('/' . preg_quote($key, '/') . '/i', $this->_select)) {
3254                        $this->_listsql .= '`' . bqSQL($key) . '`, ';
3255                    }
3256                }
3257                $this->_listsql = rtrim(trim($this->_listsql), ',');
3258            } else {
3259                $this->_listsql .= ($this->lang ? 'b.*,' : '') . ' a.*';
3260            }
3261
3262            $this->_listsql .= "\n" . (isset($this->_select) ? ', ' . rtrim($this->_select, ', ') : '') . $select_shop;
3263
3264            $limitClause = ' ' . (($shouldLimitSqlResults) ? ' LIMIT ' . (int) $start . ', ' . (int) $limit : '');
3265
3266            if ($this->_use_found_rows || isset($this->_filterHaving) || isset($this->_having)) {
3267                $this->_listsql = 'SELECT SQL_CALC_FOUND_ROWS ' . ($this->_tmpTableFilter ? ' * FROM (SELECT ' : '') .
3268                    $this->_listsql .
3269                    $fromClause .
3270                    $joinClause .
3271                    $whereClause .
3272                    $orderByClause .
3273                    $limitClause;
3274
3275                $list_count = 'SELECT FOUND_ROWS() AS `' . _DB_PREFIX_ . $this->table . '`';
3276            } else {
3277                $this->_listsql = 'SELECT ' . ($this->_tmpTableFilter ? ' * FROM (SELECT ' : '') .
3278                    $this->_listsql .
3279                    $fromClause .
3280                    $joinClause .
3281                    $whereClause .
3282                    $orderByClause .
3283                    $limitClause;
3284
3285                $list_count = 'SELECT COUNT(*) AS `' . _DB_PREFIX_ . $this->table . '` ' .
3286                    $fromClause .
3287                    $joinClause .
3288                    $whereClause;
3289            }
3290
3291            $this->_list = Db::getInstance()->executeS($this->_listsql, true, false);
3292
3293            if ($this->_list === false) {
3294                $this->_list_error = Db::getInstance()->getMsgError();
3295
3296                break;
3297            }
3298
3299            $this->_listTotal = Db::getInstance()->getValue($list_count, false);
3300
3301            if ($shouldLimitSqlResults) {
3302                $start = (int) $start - (int) $limit;
3303                if ($start < 0) {
3304                    break;
3305                }
3306            } else {
3307                break;
3308            }
3309        } while (empty($this->_list));
3310
3311        Hook::exec('action' . $this->controller_name . 'ListingResultsModifier', [
3312            'list' => &$this->_list,
3313            'list_total' => &$this->_listTotal,
3314        ]);
3315    }
3316
3317    /**
3318     * @return string
3319     */
3320    protected function getFromClause()
3321    {
3322        $sql_table = $this->table == 'order' ? 'orders' : $this->table;
3323
3324        return "\n" . 'FROM `' . _DB_PREFIX_ . $sql_table . '` a ';
3325    }
3326
3327    /**
3328     * @param $id_lang
3329     * @param $id_lang_shop
3330     *
3331     * @return string
3332     */
3333    protected function getJoinClause($id_lang, $id_lang_shop)
3334    {
3335        $shopJoinClause = '';
3336        if ($this->shopLinkType) {
3337            $shopJoinClause = ' LEFT JOIN `' . _DB_PREFIX_ . bqSQL($this->shopLinkType) . '` shop
3338                            ON a.`id_' . bqSQL($this->shopLinkType) . '` = shop.`id_' . bqSQL($this->shopLinkType) . '`';
3339        }
3340
3341        return "\n" . $this->getLanguageJoinClause($id_lang, $id_lang_shop) .
3342            "\n" . (isset($this->_join) ? $this->_join . ' ' : '') .
3343            "\n" . $shopJoinClause;
3344    }
3345
3346    /**
3347     * @param $idLang
3348     * @param $idLangShop
3349     *
3350     * @return string
3351     */
3352    protected function getLanguageJoinClause($idLang, $idLangShop)
3353    {
3354        $languageJoinClause = '';
3355        if ($this->lang) {
3356            $languageJoinClause = 'LEFT JOIN `' . _DB_PREFIX_ . bqSQL($this->table) . '_lang` b
3357                ON (b.`' . bqSQL($this->identifier) . '` = a.`' . bqSQL($this->identifier) . '` AND b.`id_lang` = ' . (int) $idLang;
3358
3359            if ($idLangShop) {
3360                if (!Shop::isFeatureActive()) {
3361                    $languageJoinClause .= ' AND b.`id_shop` = ' . (int) Configuration::get('PS_SHOP_DEFAULT');
3362                } elseif (Shop::getContext() == Shop::CONTEXT_SHOP) {
3363                    $languageJoinClause .= ' AND b.`id_shop` = ' . (int) $idLangShop;
3364                } else {
3365                    $languageJoinClause .= ' AND b.`id_shop` = a.id_shop_default';
3366                }
3367            }
3368            $languageJoinClause .= ')';
3369        }
3370
3371        return $languageJoinClause;
3372    }
3373
3374    /**
3375     * @return string
3376     */
3377    protected function getWhereClause()
3378    {
3379        $whereShop = '';
3380        if ($this->shopLinkType) {
3381            $whereShop = Shop::addSqlRestriction($this->shopShareDatas, 'a', $this->shopLinkType);
3382        }
3383        $whereClause = ' WHERE 1 ' . (isset($this->_where) ? $this->_where . ' ' : '') .
3384            ($this->deleted ? 'AND a.`deleted` = 0 ' : '') .
3385            (isset($this->_filter) ? $this->_filter : '') . $whereShop . "\n" .
3386            (isset($this->_group) ? $this->_group . ' ' : '') . "\n" .
3387            $this->getHavingClause();
3388
3389        return $whereClause;
3390    }
3391
3392    /**
3393     * @param string|null $orderBy
3394     * @param string|null $orderDirection
3395     *
3396     * @return string
3397     */
3398    protected function getOrderByClause($orderBy, $orderDirection)
3399    {
3400        $this->_orderBy = $this->checkOrderBy($orderBy);
3401        $this->_orderWay = $this->checkOrderDirection($orderDirection);
3402
3403        return ' ORDER BY ' . ((str_replace('`', '', $this->_orderBy) == $this->identifier) ? 'a.' : '') .
3404            $this->_orderBy . ' ' . $this->_orderWay .
3405            ($this->_tmpTableFilter ? ') tmpTable WHERE 1' . $this->_tmpTableFilter : '');
3406    }
3407
3408    /**
3409     * @param string|null $orderBy
3410     *
3411     * @return false|string
3412     */
3413    protected function checkOrderBy($orderBy)
3414    {
3415        if (empty($orderBy)) {
3416            $prefix = $this->getCookieFilterPrefix();
3417
3418            if ($this->context->cookie->{$prefix . $this->list_id . 'Orderby'}) {
3419                $orderBy = $this->context->cookie->{$prefix . $this->list_id . 'Orderby'};
3420            } elseif ($this->_orderBy) {
3421                $orderBy = $this->_orderBy;
3422            } else {
3423                $orderBy = $this->_defaultOrderBy;
3424            }
3425        }
3426
3427        /* Check params validity */
3428        if (!Validate::isOrderBy($orderBy)) {
3429            throw new PrestaShopException('Invalid "order by" clause.');
3430        }
3431
3432        if (!isset($this->fields_list[$orderBy]['order_key']) && isset($this->fields_list[$orderBy]['filter_key'])) {
3433            $this->fields_list[$orderBy]['order_key'] = $this->fields_list[$orderBy]['filter_key'];
3434        }
3435
3436        if (isset($this->fields_list[$orderBy]['order_key'])) {
3437            $orderBy = $this->fields_list[$orderBy]['order_key'];
3438        }
3439
3440        if (preg_match('/[.!]/', $orderBy)) {
3441            $orderBySplit = preg_split('/[.!]/', $orderBy);
3442            $orderBy = bqSQL($orderBySplit[0]) . '.`' . bqSQL($orderBySplit[1]) . '`';
3443        } elseif ($orderBy) {
3444            $orderBy = bqSQL($orderBy);
3445        }
3446
3447        return $orderBy;
3448    }
3449
3450    /**
3451     * @param string|null $orderDirection
3452     *
3453     * @return string
3454     */
3455    protected function checkOrderDirection($orderDirection)
3456    {
3457        $prefix = $this->getCookieOrderByPrefix();
3458        if (empty($orderDirection)) {
3459            if ($this->context->cookie->{$prefix . $this->list_id . 'Orderway'}) {
3460                $orderDirection = $this->context->cookie->{$prefix . $this->list_id . 'Orderway'};
3461            } elseif ($this->_orderWay) {
3462                $orderDirection = $this->_orderWay;
3463            } else {
3464                $orderDirection = $this->_defaultOrderWay;
3465            }
3466        }
3467
3468        if (!Validate::isOrderWay($orderDirection)) {
3469            throw new PrestaShopException('Invalid order direction.');
3470        }
3471
3472        return pSQL(Tools::strtoupper($orderDirection));
3473    }
3474
3475    /**
3476     * @return mixed
3477     */
3478    protected function getCookieOrderByPrefix()
3479    {
3480        return str_replace(['admin', 'controller'], '', Tools::strtolower(get_class($this)));
3481    }
3482
3483    /**
3484     * @return string
3485     */
3486    protected function getHavingClause()
3487    {
3488        $havingClause = '';
3489        if (isset($this->_filterHaving) || isset($this->_having)) {
3490            $havingClause = ' HAVING ';
3491            if (isset($this->_filterHaving)) {
3492                $havingClause .= ltrim($this->_filterHaving, ' AND ');
3493            }
3494            if (isset($this->_having)) {
3495                $havingClause .= $this->_having . ' ';
3496            }
3497        }
3498
3499        return $havingClause;
3500    }
3501
3502    /**
3503     * @param $limit
3504     *
3505     * @return bool
3506     */
3507    protected function shouldLimitSqlResults($limit)
3508    {
3509        return $limit !== false;
3510    }
3511
3512    /**
3513     * @param $limit
3514     *
3515     * @return int
3516     */
3517    protected function checkSqlLimit($limit)
3518    {
3519        if (empty($limit)) {
3520            if (
3521                isset($this->context->cookie->{$this->list_id . '_pagination'}) &&
3522                $this->context->cookie->{$this->list_id . '_pagination'}
3523            ) {
3524                $limit = $this->context->cookie->{$this->list_id . '_pagination'};
3525            } else {
3526                $limit = $this->_default_pagination;
3527            }
3528        }
3529
3530        $limit = (int) Tools::getValue($this->list_id . '_pagination', $limit);
3531        if (in_array($limit, $this->_pagination) && $limit != $this->_default_pagination) {
3532            $this->context->cookie->{$this->list_id . '_pagination'} = $limit;
3533        } else {
3534            unset($this->context->cookie->{$this->list_id . '_pagination'});
3535        }
3536
3537        if (!is_numeric($limit)) {
3538            throw new PrestaShopException('Invalid limit. It should be a numeric.');
3539        }
3540
3541        return $limit;
3542    }
3543
3544    /**
3545     * @param array|string $filter_modules_list
3546     * @param string|bool $tracking_source
3547     *
3548     * @return bool
3549     *
3550     * @throws PrestaShopException
3551     */
3552    public function getModulesList($filter_modules_list, $tracking_source = false)
3553    {
3554        if (!is_array($filter_modules_list) && null !== $filter_modules_list) {
3555            $filter_modules_list = [$filter_modules_list];
3556        }
3557
3558        if (null === $filter_modules_list || !count($filter_modules_list)) {
3559            return false;
3560        } //if there is no modules to display just return false;
3561
3562        $all_modules = Module::getModulesOnDisk(true);
3563        $this->modules_list = [];
3564        foreach ($all_modules as $module) {
3565            $perm = true;
3566            if ($module->id) {
3567                $perm &= Module::getPermissionStatic($module->id, 'configure');
3568            } else {
3569                $id_admin_module = Tab::getIdFromClassName('AdminModules');
3570                $access = Profile::getProfileAccess($this->context->employee->id_profile, $id_admin_module);
3571                if (!$access['edit']) {
3572                    $perm &= false;
3573                }
3574            }
3575
3576            if (in_array($module->name, $filter_modules_list) && $perm) {
3577                $this->fillModuleData($module, 'array', null, $tracking_source);
3578                $this->modules_list[array_search($module->name, $filter_modules_list)] = $module;
3579            }
3580        }
3581        ksort($this->modules_list);
3582
3583        if (count($this->modules_list)) {
3584            return true;
3585        }
3586
3587        return false; //no module found on disk just return false;
3588    }
3589
3590    /**
3591     * @return array
3592     */
3593    public function getLanguages()
3594    {
3595        $cookie = $this->context->cookie;
3596        $this->allow_employee_form_lang = (int) Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG');
3597        if ($this->allow_employee_form_lang && !$cookie->employee_form_lang) {
3598            $cookie->employee_form_lang = (int) Configuration::get('PS_LANG_DEFAULT');
3599        }
3600
3601        $lang_exists = false;
3602        $this->_languages = Language::getLanguages(false);
3603        foreach ($this->_languages as $lang) {
3604            if (isset($cookie->employee_form_lang) && $cookie->employee_form_lang == $lang['id_lang']) {
3605                $lang_exists = true;
3606            }
3607        }
3608
3609        $this->default_form_language = $lang_exists ? (int) $cookie->employee_form_lang : (int) Configuration::get('PS_LANG_DEFAULT');
3610
3611        foreach ($this->_languages as $k => $language) {
3612            $this->_languages[$k]['is_default'] = (int) ($language['id_lang'] == $this->default_form_language);
3613        }
3614
3615        return $this->_languages;
3616    }
3617
3618    /**
3619     * Return the list of fields value.
3620     *
3621     * @param ObjectModel $obj Object
3622     *
3623     * @return array
3624     */
3625    public function getFieldsValue($obj)
3626    {
3627        foreach ($this->fields_form as $fieldset) {
3628            if (isset($fieldset['form']['input'])) {
3629                foreach ($fieldset['form']['input'] as $input) {
3630                    if (!isset($this->fields_value[$input['name']])) {
3631                        if (isset($input['type']) && $input['type'] == 'shop') {
3632                            if ($obj->id) {
3633                                $result = Shop::getShopById((int) $obj->id, $this->identifier, $this->table);
3634                                foreach ($result as $row) {
3635                                    $this->fields_value['shop'][$row['id_' . $input['type']]][] = $row['id_shop'];
3636                                }
3637                            }
3638                        } elseif (isset($input['lang']) && $input['lang']) {
3639                            foreach ($this->_languages as $language) {
3640                                $field_value = $this->getFieldValue($obj, $input['name'], $language['id_lang']);
3641                                if (empty($field_value)) {
3642                                    if (isset($input['default_value']) && is_array($input['default_value']) && isset($input['default_value'][$language['id_lang']])) {
3643                                        $field_value = $input['default_value'][$language['id_lang']];
3644                                    } elseif (isset($input['default_value'])) {
3645                                        $field_value = $input['default_value'];
3646                                    }
3647                                }
3648                                $this->fields_value[$input['name']][$language['id_lang']] = $field_value;
3649                            }
3650                        } else {
3651                            $field_value = $this->getFieldValue($obj, $input['name']);
3652                            if ($field_value === false && isset($input['default_value'])) {
3653                                $field_value = $input['default_value'];
3654                            }
3655                            $this->fields_value[$input['name']] = $field_value;
3656                        }
3657                    }
3658                }
3659            }
3660        }
3661
3662        return $this->fields_value;
3663    }
3664
3665    /**
3666     * Return field value if possible (both classical and multilingual fields).
3667     *
3668     * Case 1 : Return value if present in $_POST / $_GET
3669     * Case 2 : Return object value
3670     *
3671     * @param ObjectModel $obj Object
3672     * @param string $key Field name
3673     * @param int|null $id_lang Language id (optional)
3674     *
3675     * @return string
3676     */
3677    public function getFieldValue($obj, $key, $id_lang = null)
3678    {
3679        if ($id_lang) {
3680            $default_value = (isset($obj->id) && $obj->id && isset($obj->{$key}[$id_lang])) ? $obj->{$key}[$id_lang] : false;
3681        } else {
3682            $default_value = isset($obj->{$key}) ? $obj->{$key} : false;
3683        }
3684
3685        return Tools::getValue($key . ($id_lang ? '_' . $id_lang : ''), $default_value);
3686    }
3687
3688    /**
3689     * Manage page display (form, list...).
3690     *
3691     * @param string|bool $class_name Allow to validate a different class than the current one
3692     *
3693     * @throws PrestaShopException
3694     */
3695    public function validateRules($class_name = false)
3696    {
3697        if (!$class_name) {
3698            $class_name = $this->className;
3699        }
3700
3701        /** @var $object ObjectModel */
3702        $object = new $class_name();
3703
3704        if (method_exists($this, 'getValidationRules')) {
3705            $definition = $this->getValidationRules();
3706        } else {
3707            $definition = ObjectModel::getDefinition($class_name);
3708        }
3709
3710        $default_language = new Language((int) Configuration::get('PS_LANG_DEFAULT'));
3711        $languages = Language::getLanguages(false);
3712
3713        foreach ($definition['fields'] as $field => $def) {
3714            $skip = [];
3715            if (in_array($field, ['passwd', 'no-picture'])) {
3716                $skip = ['required'];
3717            }
3718
3719            if (isset($def['lang']) && $def['lang']) {
3720                if (isset($def['required']) && $def['required']) {
3721                    $value = Tools::getValue($field . '_' . $default_language->id);
3722                    // !isset => not exist || "" == $value can be === 0 (before, empty $value === 0 returned true)
3723                    if (!isset($value) || '' == $value) {
3724                        $this->errors[$field . '_' . $default_language->id] = $this->trans(
3725                            'The field %field_name% is required at least in %lang%.',
3726                            ['%field_name%' => $object->displayFieldName($field, $class_name), '%lang%' => $default_language->name],
3727                            'Admin.Notifications.Error'
3728                        );
3729                    }
3730                }
3731
3732                foreach ($languages as $language) {
3733                    $value = Tools::getValue($field . '_' . $language['id_lang']);
3734                    if (!empty($value)) {
3735                        if (($error = $object->validateField($field, $value, $language['id_lang'], $skip, true)) !== true) {
3736                            $this->errors[$field . '_' . $language['id_lang']] = $error;
3737                        }
3738                    }
3739                }
3740            } elseif (($error = $object->validateField($field, Tools::getValue($field), null, $skip, true)) !== true) {
3741                $this->errors[$field] = $error;
3742            }
3743        }
3744
3745        /* Overload this method for custom checking */
3746        $this->_childValidation();
3747
3748        /* Checking for multilingual fields validity */
3749        if (isset($rules['validateLang']) && is_array($rules['validateLang'])) {
3750            foreach ($rules['validateLang'] as $field_lang => $function) {
3751                foreach ($languages as $language) {
3752                    if (($value = Tools::getValue($field_lang . '_' . $language['id_lang'])) !== false && !empty($value)) {
3753                        if (Tools::strtolower($function) == 'iscleanhtml' && Configuration::get('PS_ALLOW_HTML_IFRAME')) {
3754                            $res = Validate::$function($value, true);
3755                        } else {
3756                            $res = Validate::$function($value);
3757                        }
3758                        if (!$res) {
3759                            $this->errors[$field_lang . '_' . $language['id_lang']] = $this->trans(
3760                                'The %field_name% field (%lang%) is invalid.',
3761                                ['%field_name%' => call_user_func([$class_name, 'displayFieldName'], $field_lang, $class_name), '%lang%' => $language['name']],
3762                                'Admin.Notifications.Error'
3763                            );
3764                        }
3765                    }
3766                }
3767            }
3768        }
3769    }
3770
3771    /**
3772     * Overload this method for custom checking.
3773     */
3774    protected function _childValidation()
3775    {
3776    }
3777
3778    /**
3779     * Display object details.
3780     */
3781    public function viewDetails()
3782    {
3783    }
3784
3785    /**
3786     * Called before deletion.
3787     *
3788     * @param ObjectModel $object Object
3789     *
3790     * @return bool
3791     */
3792    protected function beforeDelete($object)
3793    {
3794        return false;
3795    }
3796
3797    /**
3798     * Called before deletion.
3799     *
3800     * @param ObjectModel $object Object
3801     * @param int $old_id
3802     *
3803     * @return bool
3804     */
3805    protected function afterDelete($object, $old_id)
3806    {
3807        return true;
3808    }
3809
3810    /**
3811     * @param ObjectModel $object
3812     *
3813     * @return bool
3814     */
3815    protected function afterAdd($object)
3816    {
3817        return true;
3818    }
3819
3820    /**
3821     * @param ObjectModel $object
3822     *
3823     * @return bool
3824     */
3825    protected function afterUpdate($object)
3826    {
3827        return true;
3828    }
3829
3830    /**
3831     * Check rights to view the current tab.
3832     *
3833     * @return bool
3834     */
3835    protected function afterImageUpload()
3836    {
3837        return true;
3838    }
3839
3840    /**
3841     * Copy data values from $_POST to object.
3842     *
3843     * @param ObjectModel &$object Object
3844     * @param string $table Object table
3845     */
3846    protected function copyFromPost(&$object, $table)
3847    {
3848        /* Classical fields */
3849        foreach ($_POST as $key => $value) {
3850            if (array_key_exists($key, get_object_vars($object)) && $key != 'id_' . $table) {
3851                /* Do not take care of password field if empty */
3852                if ($key == 'passwd' && Tools::getValue('id_' . $table) && empty($value)) {
3853                    continue;
3854                }
3855                /* Automatically hash password in MD5 */
3856                if ($key == 'passwd' && !empty($value)) {
3857                    $value = $this->get('hashing')->hash($value, _COOKIE_KEY_);
3858                }
3859                $object->{$key} = $value;
3860            }
3861        }
3862
3863        /* Multilingual fields */
3864        $class_vars = get_class_vars(get_class($object));
3865        $fields = [];
3866        if (isset($class_vars['definition']['fields'])) {
3867            $fields = $class_vars['definition']['fields'];
3868        }
3869
3870        foreach ($fields as $field => $params) {
3871            if (array_key_exists('lang', $params) && $params['lang']) {
3872                foreach (Language::getIDs(false) as $id_lang) {
3873                    if (Tools::isSubmit($field . '_' . (int) $id_lang)) {
3874                        $object->{$field}[(int) $id_lang] = Tools::getValue($field . '_' . (int) $id_lang);
3875                    }
3876                }
3877            }
3878        }
3879    }
3880
3881    /**
3882     * Returns an array with selected shops and type (group or boutique shop).
3883     *
3884     * @param string $table
3885     *
3886     * @return array
3887     */
3888    protected function getSelectedAssoShop($table)
3889    {
3890        if (!Shop::isFeatureActive() || !Shop::isTableAssociated($table)) {
3891            return [];
3892        }
3893
3894        $shops = Shop::getShops(true, null, true);
3895        if (count($shops) == 1 && isset($shops[0])) {
3896            return [$shops[0], 'shop'];
3897        }
3898
3899        $assos = [];
3900        if (Tools::isSubmit('checkBoxShopAsso_' . $table)) {
3901            foreach (Tools::getValue('checkBoxShopAsso_' . $table) as $id_shop => $value) {
3902                $assos[] = (int) $id_shop;
3903            }
3904        } elseif (Shop::getTotalShops(false) == 1) {
3905            // if we do not have the checkBox multishop, we can have an admin with only one shop and being in multishop
3906            $assos[] = (int) Shop::getContextShopID();
3907        }
3908
3909        return $assos;
3910    }
3911
3912    /**
3913     * Update the associations of shops.
3914     *
3915     * @param int $id_object
3916     *
3917     * @return bool|void
3918     *
3919     * @throws PrestaShopDatabaseException
3920     */
3921    protected function updateAssoShop($id_object)
3922    {
3923        if (!Shop::isFeatureActive()) {
3924            return;
3925        }
3926
3927        if (!Shop::isTableAssociated($this->table)) {
3928            return;
3929        }
3930
3931        $assos_data = $this->getSelectedAssoShop($this->table);
3932
3933        // Get list of shop id we want to exclude from asso deletion
3934        $exclude_ids = $assos_data;
3935        foreach (Db::getInstance()->executeS('SELECT id_shop FROM ' . _DB_PREFIX_ . 'shop') as $row) {
3936            if (!$this->context->employee->hasAuthOnShop($row['id_shop'])) {
3937                $exclude_ids[] = $row['id_shop'];
3938            }
3939        }
3940        Db::getInstance()->delete($this->table . '_shop', '`' . bqSQL($this->identifier) . '` = ' . (int) $id_object . ($exclude_ids ? ' AND id_shop NOT IN (' . implode(', ', array_map('intval', $exclude_ids)) . ')' : ''));
3941
3942        $insert = [];
3943        foreach ($assos_data as $id_shop) {
3944            $insert[] = [
3945                $this->identifier => (int) $id_object,
3946                'id_shop' => (int) $id_shop,
3947            ];
3948        }
3949
3950        return Db::getInstance()->insert($this->table . '_shop', $insert, false, true, Db::INSERT_IGNORE);
3951    }
3952
3953    /**
3954     * @param mixed $value
3955     * @param array $field
3956     *
3957     * @return bool
3958     */
3959    protected function validateField($value, $field)
3960    {
3961        if (isset($field['validation'])) {
3962            $valid_method_exists = method_exists('Validate', $field['validation']);
3963            if ((!isset($field['empty']) || !$field['empty'] || (isset($field['empty']) && $field['empty'] && $value)) && $valid_method_exists) {
3964                $field_validation = $field['validation'];
3965                if (!Validate::$field_validation($value)) {
3966                    $this->errors[] = $this->trans('%s: Incorrect value', [$field['title']], 'Admin.Notifications.Error');
3967
3968                    return false;
3969                }
3970            }
3971        }
3972
3973        return true;
3974    }
3975
3976    /**
3977     * Can be overridden.
3978     */
3979    public function beforeUpdateOptions()
3980    {
3981    }
3982
3983    /**
3984     * Overload this method for custom checking.
3985     *
3986     * @param int $id Object id used for deleting images
3987     *
3988     * @return bool
3989     */
3990    protected function postImage($id)
3991    {
3992        if (isset($this->fieldImageSettings['name'], $this->fieldImageSettings['dir'])) {
3993            return $this->uploadImage($id, $this->fieldImageSettings['name'], $this->fieldImageSettings['dir'] . '/');
3994        } elseif (!empty($this->fieldImageSettings)) {
3995            foreach ($this->fieldImageSettings as $image) {
3996                if (isset($image['name'], $image['dir'])) {
3997                    $this->uploadImage($id, $image['name'], $image['dir'] . '/');
3998                }
3999            }
4000        }
4001
4002        return !count($this->errors) ? true : false;
4003    }
4004
4005    /**
4006     * @param int $id
4007     * @param string $name
4008     * @param string $dir
4009     * @param string|bool $ext
4010     * @param int|null $width
4011     * @param int|null $height
4012     *
4013     * @return bool
4014     */
4015    protected function uploadImage($id, $name, $dir, $ext = false, $width = null, $height = null)
4016    {
4017        if (isset($_FILES[$name]['tmp_name']) && !empty($_FILES[$name]['tmp_name'])) {
4018            // Delete old image
4019            if (Validate::isLoadedObject($object = $this->loadObject())) {
4020                $object->deleteImage();
4021            } else {
4022                return false;
4023            }
4024
4025            // Check image validity
4026            $max_size = isset($this->max_image_size) ? $this->max_image_size : 0;
4027            if ($error = ImageManager::validateUpload($_FILES[$name], Tools::getMaxUploadSize($max_size))) {
4028                $this->errors[] = $error;
4029            }
4030
4031            $tmp_name = tempnam(_PS_TMP_IMG_DIR_, 'PS');
4032            if (!$tmp_name) {
4033                return false;
4034            }
4035
4036            if (!move_uploaded_file($_FILES[$name]['tmp_name'], $tmp_name)) {
4037                return false;
4038            }
4039
4040            // Evaluate the memory required to resize the image: if it's too much, you can't resize it.
4041            if (!ImageManager::checkImageMemoryLimit($tmp_name)) {
4042                $this->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.', [], 'Admin.Notifications.Error');
4043            }
4044
4045            // Copy new image
4046            if (empty($this->errors) && !ImageManager::resize($tmp_name, _PS_IMG_DIR_ . $dir . $id . '.' . $this->imageType, (int) $width, (int) $height, ($ext ? $ext : $this->imageType))) {
4047                $this->errors[] = $this->trans('An error occurred while uploading the image.', [], 'Admin.Notifications.Error');
4048            }
4049
4050            if (count($this->errors)) {
4051                return false;
4052            }
4053            if ($this->afterImageUpload()) {
4054                unlink($tmp_name);
4055
4056                return true;
4057            }
4058
4059            return false;
4060        }
4061
4062        return true;
4063    }
4064
4065    /**
4066     * Delete multiple items.
4067     *
4068     * @return bool true if success
4069     */
4070    protected function processBulkDelete()
4071    {
4072        if (is_array($this->boxes) && !empty($this->boxes)) {
4073            $object = new $this->className();
4074
4075            if (isset($object->noZeroObject)) {
4076                $objects_count = count(call_user_func([$this->className, $object->noZeroObject]));
4077
4078                // Check if all object will be deleted
4079                if ($objects_count <= 1 || count($this->boxes) == $objects_count) {
4080                    $this->errors[] = $this->trans('You need at least one object.', [], 'Admin.Notifications.Error') .
4081                        ' <b>' . $this->table . '</b><br />' .
4082                        $this->trans('You cannot delete all of the items.', [], 'Admin.Notifications.Error');
4083                }
4084            } else {
4085                $result = true;
4086                foreach ($this->boxes as $id) {
4087                    /** @var $to_delete ObjectModel */
4088                    $to_delete = new $this->className((int) $id);
4089                    $delete_ok = true;
4090                    if ($this->deleted) {
4091                        $to_delete->deleted = true;
4092                        if (!$to_delete->update()) {
4093                            $result = false;
4094                            $delete_ok = false;
4095                        }
4096                    } elseif (!$to_delete->delete()) {
4097                        $result = false;
4098                        $delete_ok = false;
4099                    }
4100
4101                    if ($delete_ok) {
4102                        PrestaShopLogger::addLog(
4103                            $this->trans('%s deletion', [$this->className]),
4104                            1,
4105                            null,
4106                            $this->className,
4107                            (int) $to_delete->id,
4108                            true,
4109                            (int) $this->context->employee->id
4110                        );
4111                    } else {
4112                        $this->errors[] = $this->trans('Can\'t delete #%id%', ['%id%' => (int) $id], 'Admin.Notifications.Error');
4113                    }
4114                }
4115                if ($result) {
4116                    $this->redirect_after = self::$currentIndex . '&conf=2&token=' . $this->token;
4117                }
4118                $this->errors[] = $this->trans('An error occurred while deleting this selection.', [], 'Admin.Notifications.Error');
4119            }
4120        } else {
4121            $this->errors[] = $this->trans('You must select at least one element to delete.', [], 'Admin.Notifications.Error');
4122        }
4123
4124        if (isset($result)) {
4125            return $result;
4126        } else {
4127            return false;
4128        }
4129    }
4130
4131    protected function ajaxProcessOpenHelp()
4132    {
4133        $help_class_name = $_GET['controller'];
4134        $popup_content = "<!doctype html>
4135        <html>
4136            <head>
4137                <meta charset='UTF-8'>
4138                <title>PrestaShop Help</title>
4139                <link href='//help.prestashop.com/css/help.css' rel='stylesheet'>
4140                <link href='//fonts.googleapis.com/css?family=Open+Sans:400,700' rel='stylesheet'>
4141                <script src='" . _PS_JS_DIR_ . "jquery/jquery-1.11.0.min.js'></script>
4142                <script src='" . _PS_JS_DIR_ . "admin.js'></script>
4143                <script src='" . _PS_JS_DIR_ . "tools.js'></script>
4144                <script>
4145                    help_class_name='" . addslashes($help_class_name) . "';
4146                    iso_user = '" . addslashes($this->context->language->iso_code) . "'
4147                </script>
4148                <script src='themes/default/js/help.js'></script>
4149                <script>
4150                    $(function(){
4151                        initHelp();
4152                    });
4153                </script>
4154            </head>
4155            <body><div id='help-container' class='help-popup'></div></body>
4156        </html>";
4157        die($popup_content);
4158    }
4159
4160    /**
4161     * Enable multiple items.
4162     *
4163     * @return bool true if success
4164     */
4165    protected function processBulkEnableSelection()
4166    {
4167        return $this->processBulkStatusSelection(1);
4168    }
4169
4170    /**
4171     * Disable multiple items.
4172     *
4173     * @return bool true if success
4174     */
4175    protected function processBulkDisableSelection()
4176    {
4177        return $this->processBulkStatusSelection(0);
4178    }
4179
4180    /**
4181     * Toggle status of multiple items.
4182     *
4183     * @param bool $status
4184     *
4185     * @return bool true if success
4186     *
4187     * @throws PrestaShopException
4188     */
4189    protected function processBulkStatusSelection($status)
4190    {
4191        $result = true;
4192        if (is_array($this->boxes) && !empty($this->boxes)) {
4193            foreach ($this->boxes as $id) {
4194                /** @var ObjectModel $object */
4195                $object = new $this->className((int) $id);
4196                $object->active = (int) $status;
4197                $isUpdated = (bool) $object->update();
4198                $result &= $isUpdated;
4199
4200                if (!$isUpdated) {
4201                    $this->errors[] = $this->trans('Can\'t update #%id% status', ['%id%' => (int) $id], 'Admin.Notifications.Error');
4202                }
4203            }
4204
4205            if ($result) {
4206                $this->redirect_after = self::$currentIndex . '&conf=5&token=' . $this->token;
4207            } else {
4208                $this->errors[] = $this->trans('An error occurred while updating the status.', [], 'Admin.Notifications.Error');
4209            }
4210        } else {
4211            $this->errors[] = $this->trans('You must select at least one item to perform a bulk action.', [], 'Admin.Notifications.Error');
4212        }
4213
4214        return $result;
4215    }
4216
4217    /**
4218     * @return bool
4219     */
4220    protected function processBulkAffectZone()
4221    {
4222        $result = false;
4223        if (is_array($this->boxes) && !empty($this->boxes)) {
4224            /** @var Country|State $object */
4225            $object = new $this->className();
4226            $result = $object->affectZoneToSelection(Tools::getValue($this->table . 'Box'), Tools::getValue('zone_to_affect'));
4227
4228            if ($result) {
4229                $this->redirect_after = self::$currentIndex . '&conf=28&token=' . $this->token;
4230            }
4231            $this->errors[] = $this->trans('An error occurred while assigning a zone to the selection.', [], 'Admin.Notifications.Error');
4232        } else {
4233            $this->errors[] = $this->trans('You must select at least one element to assign a new zone.', [], 'Admin.Notifications.Error');
4234        }
4235
4236        return $result;
4237    }
4238
4239    /**
4240     * Called before Add.
4241     *
4242     * @param ObjectModel $object Object
4243     *
4244     * @return bool
4245     */
4246    protected function beforeAdd($object)
4247    {
4248        return true;
4249    }
4250
4251    /**
4252     * Prepare the view to display the required fields form.
4253     *
4254     * @return string|void
4255     */
4256    public function displayRequiredFields()
4257    {
4258        if (!$this->access('add') || !$this->access('delete') || !$this->required_database) {
4259            return;
4260        }
4261
4262        $helper = new Helper();
4263        $helper->currentIndex = self::$currentIndex;
4264        $helper->token = $this->token;
4265        $helper->override_folder = $this->override_folder;
4266
4267        return $helper->renderRequiredFields($this->className, $this->identifier, $this->required_fields);
4268    }
4269
4270    /**
4271     * Create a template from the override file, else from the base file.
4272     *
4273     * @param string $tpl_name filename
4274     *
4275     * @return Smarty_Internal_Template
4276     */
4277    public function createTemplate($tpl_name)
4278    {
4279        // Use override tpl if it exists
4280        // If view access is denied, we want to use the default template that will be used to display an error
4281        if ($this->viewAccess() && $this->override_folder) {
4282            if (!Configuration::get('PS_DISABLE_OVERRIDES') && file_exists($this->context->smarty->getTemplateDir(1) . DIRECTORY_SEPARATOR . $this->override_folder . $tpl_name)) {
4283                return $this->context->smarty->createTemplate($this->override_folder . $tpl_name, $this->context->smarty);
4284            } elseif (file_exists($this->context->smarty->getTemplateDir(0) . 'controllers' . DIRECTORY_SEPARATOR . $this->override_folder . $tpl_name)) {
4285                return $this->context->smarty->createTemplate('controllers' . DIRECTORY_SEPARATOR . $this->override_folder . $tpl_name, $this->context->smarty);
4286            }
4287        }
4288
4289        return $this->context->smarty->createTemplate($this->context->smarty->getTemplateDir(0) . $tpl_name, $this->context->smarty);
4290    }
4291
4292    /**
4293     * Shortcut to set up a json success payload.
4294     *
4295     * @param string $message Success message
4296     */
4297    public function jsonConfirmation($message)
4298    {
4299        $this->json = true;
4300        $this->confirmations[] = $message;
4301        if ($this->status === '') {
4302            $this->status = 'ok';
4303        }
4304    }
4305
4306    /**
4307     * Shortcut to set up a json error payload.
4308     *
4309     * @param string $message Error message
4310     */
4311    public function jsonError($message)
4312    {
4313        $this->json = true;
4314        $this->errors[] = $message;
4315        if ($this->status === '') {
4316            $this->status = 'error';
4317        }
4318    }
4319
4320    /**
4321     * @deprecated Since 1.7.7 Use Tools::isFileFresh instead
4322     *
4323     * @param string $file
4324     * @param int $timeout
4325     *
4326     * @return bool
4327     */
4328    public function isFresh($file, $timeout = 604800)
4329    {
4330        return Tools::isFileFresh($file, $timeout);
4331    }
4332
4333    /**
4334     * @deprecated Since 1.7.7 Use Tools::refreshFile instead
4335     *
4336     * @param string $file_to_refresh
4337     * @param string $external_file
4338     *
4339     * @return bool
4340     */
4341    public function refresh($file_to_refresh, $external_file)
4342    {
4343        return Tools::refreshFile($file_to_refresh, $external_file);
4344    }
4345
4346    /**
4347     * @param Module $module
4348     * @param string $output_type
4349     * @param string|null $back
4350     * @param string|bool $install_source_tracking
4351     */
4352    public function fillModuleData(&$module, $output_type = 'link', $back = null, $install_source_tracking = false)
4353    {
4354        /** @var Module $obj */
4355        $obj = null;
4356        if ($module->onclick_option) {
4357            $obj = new $module->name();
4358        }
4359        // Fill module data
4360        $module->logo = '../../img/questionmark.png';
4361
4362        if (@filemtime(_PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . basename(_PS_MODULE_DIR_) . DIRECTORY_SEPARATOR . $module->name
4363            . DIRECTORY_SEPARATOR . 'logo.gif')) {
4364            $module->logo = 'logo.gif';
4365        }
4366        if (@filemtime(_PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . basename(_PS_MODULE_DIR_) . DIRECTORY_SEPARATOR . $module->name
4367            . DIRECTORY_SEPARATOR . 'logo.png')) {
4368            $module->logo = 'logo.png';
4369        }
4370
4371        $link_admin_modules = $this->context->link->getAdminLink('AdminModules', true);
4372
4373        $module->options['install_url'] = $link_admin_modules . '&install=' . urlencode($module->name) . '&tab_module=' . $module->tab . '&module_name=' . $module->name
4374            . '&anchor=' . ucfirst($module->name) . ($install_source_tracking ? '&source=' . $install_source_tracking : '');
4375        $module->options['update_url'] = $link_admin_modules . '&update=' . urlencode($module->name) . '&tab_module=' . $module->tab . '&module_name=' . $module->name . '&anchor=' . ucfirst($module->name);
4376        $module->options['uninstall_url'] = $link_admin_modules . '&uninstall=' . urlencode($module->name) . '&tab_module=' . $module->tab . '&module_name=' . $module->name . '&anchor=' . ucfirst($module->name);
4377
4378        // free modules get their source tracking data here
4379        $module->optionsHtml = $this->displayModuleOptions($module, $output_type, $back, $install_source_tracking);
4380        // pay modules get their source tracking data here
4381        if ($install_source_tracking && isset($module->addons_buy_url)) {
4382            $module->addons_buy_url .= ($install_source_tracking ? '&utm_term=' . $install_source_tracking : '');
4383        }
4384
4385        $module->options['uninstall_onclick'] = ((!$module->onclick_option) ?
4386            ((empty($module->confirmUninstall)) ? 'return confirm(\'' . $this->trans('Do you really want to uninstall this module?') . '\');' : 'return confirm(\'' . addslashes($module->confirmUninstall) . '\');') :
4387            $obj->onclickOption('uninstall', $module->options['uninstall_url']));
4388
4389        if ((Tools::getValue('module_name') == $module->name || in_array($module->name, explode('|', Tools::getValue('modules_list')))) && (int) Tools::getValue('conf') > 0) {
4390            $module->message = $this->_conf[(int) Tools::getValue('conf')];
4391        }
4392
4393        if ((Tools::getValue('module_name') == $module->name || in_array($module->name, explode('|', Tools::getValue('modules_list')))) && (int) Tools::getValue('conf') > 0) {
4394            unset($obj);
4395        }
4396    }
4397
4398    /**
4399     * Display modules list.
4400     *
4401     * @param Module $module
4402     * @param string $output_type (link or select)
4403     * @param string|null $back
4404     * @param string|bool $install_source_tracking
4405     *
4406     * @return string|array
4407     */
4408    public function displayModuleOptions($module, $output_type = 'link', $back = null, $install_source_tracking = false)
4409    {
4410        if (!isset($module->enable_device)) {
4411            $module->enable_device = Context::DEVICE_COMPUTER | Context::DEVICE_TABLET | Context::DEVICE_MOBILE;
4412        }
4413
4414        $this->translationsTab['confirm_uninstall_popup'] = (isset($module->confirmUninstall) ? $module->confirmUninstall : $this->trans('Do you really want to uninstall this module?'));
4415        if (!isset($this->translationsTab['Disable this module'])) {
4416            $this->translationsTab['Disable this module'] = $this->trans('Disable this module');
4417            $this->translationsTab['Enable this module for all shops'] = $this->trans('Enable this module for all shops');
4418            $this->translationsTab['Disable'] = $this->trans('Disable');
4419            $this->translationsTab['Enable'] = $this->trans('Enable');
4420            $this->translationsTab['Disable on mobiles'] = $this->trans('Disable on mobiles');
4421            $this->translationsTab['Disable on tablets'] = $this->trans('Disable on tablets');
4422            $this->translationsTab['Disable on computers'] = $this->trans('Disable on computers');
4423            $this->translationsTab['Display on mobiles'] = $this->trans('Display on mobiles');
4424            $this->translationsTab['Display on tablets'] = $this->trans('Display on tablets');
4425            $this->translationsTab['Display on computers'] = $this->trans('Display on computers');
4426            $this->translationsTab['Reset'] = $this->trans('Reset');
4427            $this->translationsTab['Configure'] = $this->trans('Configure');
4428            $this->translationsTab['Delete'] = $this->trans('Delete');
4429            $this->translationsTab['Install'] = $this->trans('Install');
4430            $this->translationsTab['Uninstall'] = $this->trans('Uninstall');
4431            $this->translationsTab['Would you like to delete the content related to this module ?'] = $this->trans('Would you like to delete the content related to this module ?');
4432            $this->translationsTab['This action will permanently remove the module from the server. Are you sure you want to do this?'] = $this->trans('This action will permanently remove the module from the server. Are you sure you want to do this?');
4433            $this->translationsTab['Remove from Favorites'] = $this->trans('Remove from Favorites');
4434            $this->translationsTab['Mark as Favorite'] = $this->trans('Mark as Favorite');
4435        }
4436
4437        $link_admin_modules = $this->context->link->getAdminLink('AdminModules', true);
4438        $modules_options = [];
4439
4440        $configure_module = [
4441            'href' => $link_admin_modules . '&configure=' . urlencode($module->name) . '&tab_module=' . $module->tab . '&module_name=' . urlencode($module->name),
4442            'onclick' => $module->onclick_option && isset($module->onclick_option_content['configure']) ? $module->onclick_option_content['configure'] : '',
4443            'title' => '',
4444            'text' => $this->translationsTab['Configure'],
4445            'cond' => $module->id && isset($module->is_configurable) && $module->is_configurable,
4446            'icon' => 'wrench',
4447        ];
4448
4449        $desactive_module = [
4450            'href' => $link_admin_modules . '&module_name=' . urlencode($module->name) . '&' . ($module->active ? 'enable=0' : 'enable=1') . '&tab_module=' . $module->tab,
4451            'onclick' => $module->active && $module->onclick_option && isset($module->onclick_option_content['desactive']) ? $module->onclick_option_content['desactive'] : '',
4452            'title' => Shop::isFeatureActive() ? htmlspecialchars($module->active ? $this->translationsTab['Disable this module'] : $this->translationsTab['Enable this module for all shops']) : '',
4453            'text' => $module->active ? $this->translationsTab['Disable'] : $this->translationsTab['Enable'],
4454            'cond' => $module->id,
4455            'icon' => 'off',
4456        ];
4457        $link_reset_module = $link_admin_modules . '&module_name=' . urlencode($module->name) . '&reset&tab_module=' . $module->tab;
4458
4459        $is_reset_ready = false;
4460        if (Validate::isModuleName($module->name)) {
4461            if (method_exists(Module::getInstanceByName($module->name), 'reset')) {
4462                $is_reset_ready = true;
4463            }
4464        }
4465
4466        $reset_module = [
4467            'href' => $link_reset_module,
4468            'onclick' => $module->onclick_option && isset($module->onclick_option_content['reset']) ? $module->onclick_option_content['reset'] : '',
4469            'title' => '',
4470            'text' => $this->translationsTab['Reset'],
4471            'cond' => $module->id && $module->active,
4472            'icon' => 'undo',
4473            'class' => ($is_reset_ready ? 'reset_ready' : ''),
4474        ];
4475
4476        $delete_module = [
4477            'href' => $link_admin_modules . '&delete=' . urlencode($module->name) . '&tab_module=' . $module->tab . '&module_name=' . urlencode($module->name),
4478            'onclick' => $module->onclick_option && isset($module->onclick_option_content['delete']) ? $module->onclick_option_content['delete'] : 'return confirm(\'' . $this->translationsTab['This action will permanently remove the module from the server. Are you sure you want to do this?'] . '\');',
4479            'title' => '',
4480            'text' => $this->translationsTab['Delete'],
4481            'cond' => true,
4482            'icon' => 'trash',
4483            'class' => 'text-danger',
4484        ];
4485
4486        $display_mobile = [
4487            'href' => $link_admin_modules . '&module_name=' . urlencode($module->name) . '&' . ($module->enable_device & Context::DEVICE_MOBILE ? 'disable_device' : 'enable_device') . '=' . Context::DEVICE_MOBILE . '&tab_module=' . $module->tab,
4488            'onclick' => '',
4489            'title' => htmlspecialchars($module->enable_device & Context::DEVICE_MOBILE ? $this->translationsTab['Disable on mobiles'] : $this->translationsTab['Display on mobiles']),
4490            'text' => $module->enable_device & Context::DEVICE_MOBILE ? $this->translationsTab['Disable on mobiles'] : $this->translationsTab['Display on mobiles'],
4491            'cond' => $module->id,
4492            'icon' => 'mobile',
4493        ];
4494
4495        $display_tablet = [
4496            'href' => $link_admin_modules . '&module_name=' . urlencode($module->name) . '&' . ($module->enable_device & Context::DEVICE_TABLET ? 'disable_device' : 'enable_device') . '=' . Context::DEVICE_TABLET . '&tab_module=' . $module->tab,
4497            'onclick' => '',
4498            'title' => htmlspecialchars($module->enable_device & Context::DEVICE_TABLET ? $this->translationsTab['Disable on tablets'] : $this->translationsTab['Display on tablets']),
4499            'text' => $module->enable_device & Context::DEVICE_TABLET ? $this->translationsTab['Disable on tablets'] : $this->translationsTab['Display on tablets'],
4500            'cond' => $module->id,
4501            'icon' => 'tablet',
4502        ];
4503
4504        $display_computer = [
4505            'href' => $link_admin_modules . '&module_name=' . urlencode($module->name) . '&' . ($module->enable_device & Context::DEVICE_COMPUTER ? 'disable_device' : 'enable_device') . '=' . Context::DEVICE_COMPUTER . '&tab_module=' . $module->tab,
4506            'onclick' => '',
4507            'title' => htmlspecialchars($module->enable_device & Context::DEVICE_COMPUTER ? $this->translationsTab['Disable on computers'] : $this->translationsTab['Display on computers']),
4508            'text' => $module->enable_device & Context::DEVICE_COMPUTER ? $this->translationsTab['Disable on computers'] : $this->translationsTab['Display on computers'],
4509            'cond' => $module->id,
4510            'icon' => 'desktop',
4511        ];
4512
4513        $install = [
4514            'href' => $link_admin_modules . '&install=' . urlencode($module->name) . '&tab_module=' . $module->tab . '&module_name=' . $module->name . '&anchor=' . ucfirst($module->name)
4515                . (null !== $back ? '&back=' . urlencode($back) : '') . ($install_source_tracking ? '&source=' . $install_source_tracking : ''),
4516            'onclick' => '',
4517            'title' => $this->translationsTab['Install'],
4518            'text' => $this->translationsTab['Install'],
4519            'cond' => $module->id,
4520            'icon' => 'plus-sign-alt',
4521        ];
4522
4523        $uninstall = [
4524            'href' => $link_admin_modules . '&uninstall=' . urlencode($module->name) . '&tab_module=' . $module->tab . '&module_name=' . $module->name . '&anchor=' . ucfirst($module->name) . (null !== $back ? '&back=' . urlencode($back) : ''),
4525            'onclick' => (isset($module->onclick_option_content['uninstall']) ? $module->onclick_option_content['uninstall'] : 'return confirm(\'' . $this->translationsTab['confirm_uninstall_popup'] . '\');'),
4526            'title' => $this->translationsTab['Uninstall'],
4527            'text' => $this->translationsTab['Uninstall'],
4528            'cond' => $module->id,
4529            'icon' => 'minus-sign-alt',
4530        ];
4531
4532        $remove_from_favorite = [
4533            'href' => '#',
4534            'class' => 'action_unfavorite toggle_favorite',
4535            'onclick' => '',
4536            'title' => $this->translationsTab['Remove from Favorites'],
4537            'text' => $this->translationsTab['Remove from Favorites'],
4538            'cond' => $module->id,
4539            'icon' => 'star',
4540            'data-value' => '0',
4541            'data-module' => $module->name,
4542        ];
4543
4544        $mark_as_favorite = [
4545            'href' => '#',
4546            'class' => 'action_favorite toggle_favorite',
4547            'onclick' => '',
4548            'title' => $this->translationsTab['Mark as Favorite'],
4549            'text' => $this->translationsTab['Mark as Favorite'],
4550            'cond' => $module->id,
4551            'icon' => 'star',
4552            'data-value' => '1',
4553            'data-module' => $module->name,
4554        ];
4555
4556        $update = [
4557            'href' => $module->options['update_url'],
4558            'onclick' => '',
4559            'title' => 'Update it!',
4560            'text' => 'Update it!',
4561            'icon' => 'refresh',
4562            'cond' => $module->id,
4563        ];
4564
4565        $divider = [
4566            'href' => '#',
4567            'onclick' => '',
4568            'title' => 'divider',
4569            'text' => 'divider',
4570            'cond' => $module->id,
4571        ];
4572
4573        if (isset($module->version_addons) && $module->version_addons) {
4574            $modules_options[] = $update;
4575        }
4576
4577        if ($module->active) {
4578            $modules_options[] = $configure_module;
4579            $modules_options[] = $desactive_module;
4580            $modules_options[] = $display_mobile;
4581            $modules_options[] = $display_tablet;
4582            $modules_options[] = $display_computer;
4583        } else {
4584            $modules_options[] = $desactive_module;
4585            $modules_options[] = $configure_module;
4586        }
4587
4588        $modules_options[] = $reset_module;
4589
4590        if ($output_type == 'select') {
4591            if (!$module->id) {
4592                $modules_options[] = $install;
4593            } else {
4594                $modules_options[] = $uninstall;
4595            }
4596        } elseif ($output_type == 'array') {
4597            if ($module->id) {
4598                $modules_options[] = $uninstall;
4599            }
4600        }
4601
4602        if (isset($module->preferences, $module->preferences['favorite']) && $module->preferences['favorite'] == 1) {
4603            $remove_from_favorite['style'] = '';
4604            $mark_as_favorite['style'] = 'display:none;';
4605            $modules_options[] = $remove_from_favorite;
4606            $modules_options[] = $mark_as_favorite;
4607        } else {
4608            $mark_as_favorite['style'] = '';
4609            $remove_from_favorite['style'] = 'display:none;';
4610            $modules_options[] = $remove_from_favorite;
4611            $modules_options[] = $mark_as_favorite;
4612        }
4613
4614        if ($module->id == 0) {
4615            $install['cond'] = 1;
4616            $install['flag_install'] = 1;
4617            $modules_options[] = $install;
4618        }
4619        $modules_options[] = $divider;
4620        $modules_options[] = $delete_module;
4621
4622        $return = '';
4623        foreach ($modules_options as $option_name => $option) {
4624            if ($option['cond']) {
4625                if ($output_type == 'link') {
4626                    $return .= '<li><a class="' . $option_name . ' action_module';
4627                    $return .= '" href="' . $option['href'] . (null !== $back ? '&back=' . urlencode($back) : '') . '"';
4628                    $return .= ' onclick="' . $option['onclick'] . '"  title="' . $option['title'] . '"><i class="icon-' . (isset($option['icon']) && $option['icon'] ? $option['icon'] : 'cog') . '"></i>&nbsp;' . $option['text'] . '</a></li>';
4629                } elseif ($output_type == 'array') {
4630                    if (!is_array($return)) {
4631                        $return = [];
4632                    }
4633
4634                    $html = '<a class="';
4635
4636                    $is_install = isset($option['flag_install']) ? true : false;
4637
4638                    if (isset($option['class'])) {
4639                        $html .= $option['class'];
4640                    }
4641                    if ($is_install) {
4642                        $html .= ' btn btn-success';
4643                    }
4644                    if (!$is_install && count($return) == 0) {
4645                        $html .= ' btn btn-default';
4646                    }
4647
4648                    $html .= '"';
4649
4650                    if (isset($option['data-value'])) {
4651                        $html .= ' data-value="' . $option['data-value'] . '"';
4652                    }
4653
4654                    if (isset($option['data-module'])) {
4655                        $html .= ' data-module="' . $option['data-module'] . '"';
4656                    }
4657
4658                    if (isset($option['style'])) {
4659                        $html .= ' style="' . $option['style'] . '"';
4660                    }
4661
4662                    $html .= ' href="' . htmlentities($option['href']) . (null !== $back ? '&back=' . urlencode($back) : '') . '" onclick="' . $option['onclick'] . '"  title="' . $option['title'] . '"><i class="icon-' . (isset($option['icon']) && $option['icon'] ? $option['icon'] : 'cog') . '"></i> ' . $option['text'] . '</a>';
4663                    $return[] = $html;
4664                } elseif ($output_type == 'select') {
4665                    $return .= '<option id="' . $option_name . '" data-href="' . htmlentities($option['href']) . (null !== $back ? '&back=' . urlencode($back) : '') . '" data-onclick="' . $option['onclick'] . '">' . $option['text'] . '</option>';
4666                }
4667            }
4668        }
4669
4670        if ($output_type == 'select') {
4671            $return = '<select id="select_' . $module->name . '">' . $return . '</select>';
4672        }
4673
4674        return $return;
4675    }
4676
4677    public function ajaxProcessGetModuleQuickView()
4678    {
4679        $modules = Module::getModulesOnDisk();
4680
4681        foreach ($modules as $module) {
4682            if ($module->name == Tools::getValue('module')) {
4683                break;
4684            }
4685        }
4686
4687        $url = $module->url;
4688
4689        if (isset($module->type) && ($module->type == 'addonsPartner' || $module->type == 'addonsNative')) {
4690            $url = $this->context->link->getAdminLink('AdminModules') . '&install=' . urlencode($module->name) . '&tab_module=' . $module->tab . '&module_name=' . $module->name . '&anchor=' . ucfirst($module->name);
4691        }
4692
4693        $this->context->smarty->assign([
4694            'displayName' => $module->displayName,
4695            'image' => $module->image,
4696            'nb_rates' => (int) $module->nb_rates[0],
4697            'avg_rate' => (int) $module->avg_rate[0],
4698            'badges' => $module->badges,
4699            'compatibility' => $module->compatibility,
4700            'description_full' => $module->description_full,
4701            'additional_description' => $module->additional_description,
4702            'is_addons_partner' => (isset($module->type) && ($module->type == 'addonsPartner' || $module->type == 'addonsNative')),
4703            'url' => $url,
4704            'price' => $module->price,
4705        ]);
4706        // Fetch the translations in the right place - they are not defined by our current controller!
4707        Context::getContext()->override_controller_name_for_translations = 'AdminModules';
4708        $this->smartyOutputContent('controllers/modules/quickview.tpl');
4709        die(1);
4710    }
4711
4712    /**
4713     * Add an entry to the meta title.
4714     *
4715     * @param string $entry new entry
4716     */
4717    public function addMetaTitle($entry)
4718    {
4719        // Only add entry if the meta title was not forced.
4720        if (is_array($this->meta_title)) {
4721            $this->meta_title[] = $entry;
4722        }
4723    }
4724
4725    /**
4726     * Set action.
4727     *
4728     * @param string $action
4729     */
4730    public function setAction($action)
4731    {
4732        $this->action = $action;
4733    }
4734
4735    /**
4736     * Set IdObject.
4737     *
4738     * @param int $id_object
4739     */
4740    public function setIdObject($id_object)
4741    {
4742        $this->id_object = (int) $id_object;
4743    }
4744
4745    /**
4746     * @return string
4747     */
4748    public function getTabSlug()
4749    {
4750        if (empty($this->tabSlug)) {
4751            $this->tabSlug = Access::findSlugByIdTab($this->id);
4752        }
4753
4754        return $this->tabSlug;
4755    }
4756
4757    /**
4758     * {@inheritdoc}
4759     */
4760    protected function buildContainer()
4761    {
4762        return SymfonyContainer::getInstance();
4763    }
4764
4765    /**
4766     * Return the type of authorization on module page.
4767     *
4768     * @return int
4769     */
4770    public function authorizationLevel()
4771    {
4772        if (
4773            Access::isGranted(
4774                'ROLE_MOD_TAB_' . strtoupper($this->controller_name) . '_DELETE',
4775                $this->context->employee->id_profile
4776            )
4777        ) {
4778            return AdminController::LEVEL_DELETE;
4779        } elseif (
4780            Access::isGranted(
4781                'ROLE_MOD_TAB_' . strtoupper($this->controller_name) . '_CREATE',
4782                $this->context->employee->id_profile
4783            )
4784        ) {
4785            return AdminController::LEVEL_ADD;
4786        } elseif (
4787            Access::isGranted(
4788                'ROLE_MOD_TAB_' . strtoupper($this->controller_name) . '_UPDATE',
4789                $this->context->employee->id_profile
4790            )
4791        ) {
4792            return AdminController::LEVEL_EDIT;
4793        } elseif (
4794            Access::isGranted(
4795                'ROLE_MOD_TAB_' . strtoupper($this->controller_name) . '_READ',
4796                $this->context->employee->id_profile
4797            )
4798        ) {
4799            return AdminController::LEVEL_VIEW;
4800        } else {
4801            return 0;
4802        }
4803    }
4804
4805    /**
4806     * Get the url of the first active sub-tab.
4807     *
4808     * @param array[] $subtabs
4809     *
4810     * @return string Url, or empty if no active sub-tab
4811     */
4812    private function getTabLinkFromSubTabs(array $subtabs)
4813    {
4814        foreach ($subtabs as $tab) {
4815            if ($tab['active'] && $tab['enabled']) {
4816                return $tab['href'];
4817            }
4818        }
4819
4820        return '';
4821    }
4822
4823    /**
4824     * Prepare price specifications to display cldr prices in javascript context.
4825     *
4826     * @param Context $context
4827     *
4828     * @return array
4829     */
4830    private function preparePriceSpecifications(Context $context)
4831    {
4832        /* @var Currency */
4833        $currency = $context->currency;
4834        /* @var PriceSpecification */
4835        $priceSpecification = $context->getCurrentLocale()->getPriceSpecification($currency->iso_code);
4836        if (empty($priceSpecification)) {
4837            return [];
4838        }
4839
4840        return array_merge(
4841            ['symbol' => $priceSpecification->getSymbolsByNumberingSystem(Locale::NUMBERING_SYSTEM_LATIN)->toArray()],
4842            $priceSpecification->toArray()
4843        );
4844    }
4845
4846    /**
4847     * Prepare number specifications to display cldr numbers in javascript context.
4848     *
4849     * @param Context $context
4850     *
4851     * @return array
4852     */
4853    private function prepareNumberSpecifications(Context $context)
4854    {
4855        /* @var NumberSpecification */
4856        $numberSpecification = $context->getCurrentLocale()->getNumberSpecification();
4857        if (empty($numberSpecification)) {
4858            return [];
4859        }
4860
4861        return array_merge(
4862            ['symbol' => $numberSpecification->getSymbolsByNumberingSystem(Locale::NUMBERING_SYSTEM_LATIN)->toArray()],
4863            $numberSpecification->toArray()
4864        );
4865    }
4866
4867    /**
4868     * Set if anonymous is allowed to run this controller
4869     *
4870     * @param bool $value
4871     *
4872     * @return bool
4873     */
4874    protected function setAllowAnonymous($value)
4875    {
4876        $this->allowAnonymous = (bool) $value;
4877    }
4878
4879    /**
4880     * Return if an anonymous is allowed to run this controller
4881     *
4882     * @return bool
4883     */
4884    protected function isAnonymousAllowed()
4885    {
4886        return $this->allowAnonymous;
4887    }
4888}
4889