* @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) */ declare(strict_types=1); namespace PrestaShopBundle\Bridge\AdminController; use Context; use Employee; use Media; use PrestaShop\PrestaShop\Adapter\LegacyContext; use PrestaShop\PrestaShop\Core\Exception\InvalidArgumentException; use PrestaShop\PrestaShop\Core\Feature\FeatureInterface; use PrestaShop\PrestaShop\Core\Hook\HookDispatcherInterface; use Tools; /** * The bridge class is used as a proxy between the internal controller configuration and the legacy code. * * It is accessible via the Context and/or the hooks, but to avoid legacy code accessing the controller configuration * directly this object provides access to the configuration fields via a whitelist thus protecting th other elements * of the ControllerConfiguration which shouldn't be accessed directly. * * It also allows integrating legacy functions that were usable on AdminController thus we don't need to implement them * in the framework controller. * * @property int $id * @property string $className * @property string $controller_name * @property string $php_self * @property string $current_index * @property string|null $position_identifier * @property string $table * @property string $token * @property Employee $user * @property array $meta_title * @property array $breadcrumbs * @property bool $lite_display * @property string $display * @property bool $show_page_header_toolbar * @property string $page_header_toolbar_title * @property array $toolbar_btn * @property array $toolbar_title * @property bool $display_header * @property bool $display_header_javascript * @property bool $display_footer * @property bool $bootstrap * @property array $css_files * @property array $js_files * @property string $tpl_folder * @property array $errors * @property array $warnings * @property array $confirmations * @property array $informations * @property bool $json * @property string $template * @property array $tpl_vars * @property array $modals * @property int $multishop_context * @property bool $multishop_context_group */ class LegacyControllerBridge implements LegacyControllerBridgeInterface { /** * @var ControllerConfiguration */ private $controllerConfiguration; /** * @var FeatureInterface */ private $multistoreFeature; /** * @var LegacyContext */ private $legacyContext; /** * @var HookDispatcherInterface */ private $hookDispatcher; /** * @var string[] maps legacy controller properties with the bridge */ private $propertiesMap = [ 'id' => 'controllerConfiguration.tabId', 'className' => 'controllerConfiguration.objectModelClassName', 'controller_name' => 'controllerConfiguration.legacyControllerName', 'php_self' => 'controllerConfiguration.legacyControllerName', 'current_index' => 'controllerConfiguration.legacyCurrentIndex', 'position_identifier' => 'controllerConfiguration.positionIdentifierKey', 'table' => 'controllerConfiguration.tableName', 'token' => 'controllerConfiguration.token', 'meta_title' => 'controllerConfiguration.metaTitle', 'breadcrumbs' => 'controllerConfiguration.breadcrumbs', 'lite_display' => 'controllerConfiguration.liteDisplay', 'display' => 'controllerConfiguration.displayType', 'show_page_header_toolbar' => 'controllerConfiguration.showPageHeaderToolbar', 'page_header_toolbar_title' => 'controllerConfiguration.pageHeaderToolbarTitle', 'page_header_toolbar_btn' => 'controllerConfiguration.pageHeaderToolbarActions', 'toolbar_btn' => 'controllerConfiguration.pageHeaderToolbarActions', 'toolbar_title' => 'controllerConfiguration.toolbarTitle', 'display_header' => 'controllerConfiguration.displayHeader', 'display_header_javascript' => 'controllerConfiguration.displayHeaderJavascript', 'display_footer' => 'controllerConfiguration.displayFooter', 'bootstrap' => 'controllerConfiguration.bootstrap', 'css_files' => 'controllerConfiguration.cssFiles', 'js_files' => 'controllerConfiguration.jsFiles', 'tpl_folder' => 'controllerConfiguration.templateFolder', 'errors' => 'controllerConfiguration.errors', 'warnings' => 'controllerConfiguration.warnings', 'confirmations' => 'controllerConfiguration.confirmations', 'informations' => 'controllerConfiguration.informations', 'json' => 'controllerConfiguration.json', 'template' => 'controllerConfiguration.template', 'tpl_vars' => 'controllerConfiguration.templateVars', 'modals' => 'controllerConfiguration.modals', 'multishop_context' => 'controllerConfiguration.multiShopContext', 'multishop_context_group' => 'controllerConfiguration.multiShopContextGroup', ]; /** * @param ControllerConfiguration $controllerConfiguration * @param FeatureInterface $multistoreFeature * @param LegacyContext $legacyContext * @param HookDispatcherInterface $hookDispatcher */ public function __construct( ControllerConfiguration $controllerConfiguration, FeatureInterface $multistoreFeature, LegacyContext $legacyContext, HookDispatcherInterface $hookDispatcher ) { $this->controllerConfiguration = $controllerConfiguration; $this->multistoreFeature = $multistoreFeature; $this->legacyContext = $legacyContext; $this->hookDispatcher = $hookDispatcher; } /** * {@intheritedoc} */ public function setMedia($isNewTheme = false): void { $adminWebpath = ''; if (defined('_PS_ADMIN_DIR_')) { $adminWebpath = preg_replace( '/^' . preg_quote(DIRECTORY_SEPARATOR, '/') . '/', '', str_ireplace(_PS_CORE_DIR_, '', _PS_ADMIN_DIR_) ); } if ($this->getContext()->language->is_rtl) { $this->addJS(_PS_JS_DIR_ . 'rtl.js'); $this->addCSS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/css/' . $this->getContext()->language->iso_code . '.css'); } if ($isNewTheme) { $this->addCSS(__PS_BASE_URI__ . $adminWebpath . '/themes/new-theme/public/theme.css', 'all', 1); $this->addJS(__PS_BASE_URI__ . $adminWebpath . '/themes/new-theme/public/main.bundle.js'); // the multistore dropdown should be called only once, and only if multistore is used if ($this->multistoreFeature->isUsed()) { $this->addJs(__PS_BASE_URI__ . $adminWebpath . '/themes/new-theme/public/multistore_dropdown.bundle.js'); } $this->addJqueryPlugin(['chosen', 'fancybox']); } else { //Bootstrap $this->addCSS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/css/theme.css', 'all', 0); $this->addCSS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/css/vendor/titatoggle-min.css', 'all', 0); $this->addCSS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/public/theme.css', 'all', 0); // add Jquery 3 and its migration script $this->addJs(_PS_JS_DIR_ . 'jquery/jquery-3.5.1.min.js'); $this->addJs(_PS_JS_DIR_ . 'jquery/bo-migrate-mute.min.js'); $this->addJs(_PS_JS_DIR_ . 'jquery/jquery-migrate-3.1.0.min.js'); // implement $.browser object and live method, that has been removed since jquery 1.9 $this->addJs(_PS_JS_DIR_ . 'jquery/jquery.browser-0.1.0.min.js'); $this->addJs(_PS_JS_DIR_ . 'jquery/jquery.live-polyfill-1.1.2.min.js'); $this->addJqueryPlugin(['scrollTo', 'alerts', 'chosen', 'autosize', 'fancybox']); $this->addJqueryPlugin('growl', null, false); $this->addJqueryUI(['ui.slider', 'ui.datepicker']); $this->addJS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/js/vendor/bootstrap.min.js'); $this->addJS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/js/vendor/modernizr.min.js'); $this->addJS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/js/modernizr-loads.js'); $this->addJS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/js/vendor/moment-with-langs.min.js'); $this->addJS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/public/bundle.js'); $this->addJS(_PS_JS_DIR_ . 'jquery/plugins/timepicker/jquery-ui-timepicker-addon.js'); if (!$this->controllerConfiguration->liteDisplay) { $this->addJS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/js/help.js'); } if (!Tools::getValue('submitFormAjax')) { $this->addJS(_PS_JS_DIR_ . 'admin/notifications.js'); } // Specific Admin Theme $this->addCSS(__PS_BASE_URI__ . $adminWebpath . '/themes/' . self::DEFAULT_THEME . '/css/overrides.css', 'all', PHP_INT_MAX); } $this->addJS([ _PS_JS_DIR_ . 'admin.js?v=' . _PS_VERSION_, // TODO: SEE IF REMOVABLE __PS_BASE_URI__ . $adminWebpath . '/themes/new-theme/public/cldr.bundle.js', _PS_JS_DIR_ . 'tools.js?v=' . _PS_VERSION_, __PS_BASE_URI__ . $adminWebpath . '/public/bundle.js', ]); Media::addJsDef([ 'changeFormLanguageUrl' => $this->getContext()->link->getAdminLink( 'AdminEmployees', true, [], ['action' => 'formLanguage'] ), ]); Media::addJsDef(['host_mode' => (defined('_PS_HOST_MODE_') && _PS_HOST_MODE_)]); Media::addJsDef(['baseDir' => __PS_BASE_URI__]); Media::addJsDef(['baseAdminDir' => __PS_BASE_URI__ . basename(_PS_ADMIN_DIR_) . '/']); Media::addJsDef(['currency' => [ 'iso_code' => $this->getContext()->currency->iso_code, 'sign' => $this->getContext()->currency->sign, 'name' => $this->getContext()->currency->name, 'format' => $this->getContext()->currency->format, ]]); Media::addJsDef([ 'prestashop' => [ 'debug' => _PS_MODE_DEV_, ], ]); $this->hookDispatcher->dispatchWithParameters('actionAdminControllerSetMedia'); } /** * {@intheritedoc} */ public function addCSS($cssUri, $cssMediaType = 'all', $offset = null, $checkPath = true): void { if (!is_array($cssUri)) { $cssUri = [$cssUri]; } foreach ($cssUri as $cssFile => $media) { if (is_string($cssFile) && strlen($cssFile) > 1) { if ($checkPath) { $css_path = Media::getCSSPath($cssFile, $media); } else { $css_path = [$cssFile => $media]; } } else { if ($checkPath) { $css_path = Media::getCSSPath($media, $cssMediaType); } else { $css_path = [$media => $cssMediaType]; } } $key = is_array($css_path) ? key($css_path) : $css_path; if ($css_path && (!isset($this->controllerConfiguration->cssFiles[$key]) || ($this->controllerConfiguration->cssFiles[$key] != reset($css_path)))) { $size = count($this->controllerConfiguration->cssFiles); if ($offset === null || $offset > $size || $offset < 0 || !is_numeric($offset)) { $offset = $size; } $this->controllerConfiguration->cssFiles = array_merge(array_slice($this->controllerConfiguration->cssFiles, 0, $offset), $css_path, array_slice($this->controllerConfiguration->cssFiles, $offset)); } } } /** * {@intheritedoc} */ public function addJS($jsUri, $checkPath = true): void { if (!is_array($jsUri)) { $jsUri = [$jsUri]; } foreach ($jsUri as $jsFile) { $jsFile = explode('?', $jsFile); $version = ''; if (isset($jsFile[1]) && $jsFile[1]) { $version = $jsFile[1]; } $jsPath = $jsFile = $jsFile[0]; if ($checkPath) { $jsPath = Media::getJSPath($jsFile); } if ($jsPath && !in_array($jsPath, $this->controllerConfiguration->jsFiles)) { $this->controllerConfiguration->jsFiles[] = $jsPath . ($version ? '?' . $version : ''); } } } /** * {@intheritedoc} */ public function addJqueryPlugin($name, $folder = null, $css = true): void { if (!is_array($name)) { $name = [$name]; } foreach ($name as $plugin) { $pluginPath = Media::getJqueryPluginPath($plugin, $folder); if (!empty($pluginPath['js'])) { $this->addJS($pluginPath['js'], false); } if ($css && !empty($pluginPath['css'])) { if (is_array($pluginPath['css'])) { $this->addCSS((string) key($pluginPath['css']), 'all', null, false); } else { $this->addCSS($pluginPath['css'], 'all', null, false); } } } } /** * {@intheritedoc} */ public function addJqueryUI($component, $theme = 'base', $checkDependencies = true): void { if (!is_array($component)) { $component = [$component]; } foreach ($component as $ui) { $uiPath = Media::getJqueryUIPath($ui, $theme, $checkDependencies); $this->addCSS($uiPath['css'], 'all'); $this->addJS($uiPath['js'], false); } } /** * This whole bridge is used as legacy $context->controller, but all properties are held in configuration, * so we use a "magic" getter with a help of properties map and property accessor * to allow legacy code retrieving properties from configuration as if it would be in legacy controller * * @param string $name * * @return mixed */ public function &__get(string $name) { return $this->getPropertyReference($name); } /** * This whole bridge is used as legacy $context->controller, but all properties are held in configuration, * so we use a "magic" setter with a help of properties map and property accessor * to allow legacy code setting properties in configuration as if it would be in legacy controller. * * @param string $name * @param mixed $value * * @return void */ public function __set(string $name, $value): void { $modifiedField = &$this->getPropertyReference($name); $modifiedField = $value; } /** * We need to return a reference in order for legacy codes to do manipulation like: * $controller->toolbar_button['new_product'] = ['text' => 'New Product']; * * That's why we can't use a PropertyAccessor component because it does not return references, only values. * * @param string $name * * @return mixed * * @throws InvalidArgumentException */ private function &getPropertyReference(string $name) { if (!isset($this->propertiesMap[$name])) { throw new InvalidArgumentException(sprintf('No mapping found for %s', $name)); } $propertyPath = explode('.', $this->propertiesMap[$name]); $currentReference = &$this; foreach ($propertyPath as $property) { if (!property_exists($currentReference, $property)) { throw new InvalidArgumentException(sprintf('Could not find property %s', implode('.', $propertyPath))); } $currentReference = &$currentReference->{$property}; } return $currentReference; } /** * @return Context */ private function getContext(): Context { return $this->legacyContext->getContext(); } }