Current File : /var/www/prestashop/modules/ps_mbo/src/Traits/HaveTabs.php
<?php
/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License version 3.0
 * that is bundled with this package in the file LICENSE.md.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/AFL-3.0
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0
 */
declare(strict_types=1);

namespace PrestaShop\Module\Mbo\Traits;

use Db;
use Exception;
use LanguageCore as Language;
use PrestaShop\Module\Mbo\Helpers\ErrorHelper;
use Symfony\Component\String\UnicodeString;
use TabCore as Tab;
use ValidateCore as Validate;

trait HaveTabs
{
    /**
     * @var array[]
     */
    public static $ADMIN_CONTROLLERS = [
        'AdminPsMboModuleParent' => [
            'name' => 'Marketplace',
            'visible' => true,
            'position' => 0,
            'class_name' => 'AdminPsMboModuleParent',
            'parent_class_name' => 'AdminParentModulesSf',
        ],
        'AdminPsMboModule' => [
            'name' => 'Marketplace',
            'visible' => true,
            'position' => 0,
            'class_name' => 'AdminPsMboModule',
            'parent_class_name' => 'AdminPsMboModuleParent',
        ],
        'AdminPsMboSelection' => [
            'name' => 'Modules in the spotlight',
            'visible' => false,
            'position' => 1,
            'class_name' => 'AdminPsMboSelection',
            'parent_class_name' => 'AdminPsMboModuleParent',
            'wording' => 'Modules in the spotlight',
            'wording_domain' => 'Modules.Mbo.Modulesselection',
        ],
        'AdminPsMboRecommended' => [
            'name' => 'Modules recommandés',
            'visible' => true,
            'class_name' => 'AdminPsMboRecommended',
        ],
        'AdminPsMboTheme' => [
            'name' => 'Themes Catalog',
            'visible' => true,
            'position' => 2,
            'class_name' => 'AdminPsMboTheme',
            'parent_class_name' => 'AdminParentThemes',
            'wording' => 'Themes Catalog',
            'wording_domain' => 'Modules.Mbo.Themescatalog',
        ],
        'ApiPsMbo' => [
            'name' => 'MBO Api',
            'visible' => false,
            'class_name' => 'ApiPsMbo',
            'parent_class_name' => 'AdminParentModulesSf',
        ],
        'ApiSecurityPsMbo' => [
            'name' => 'MBO Api Security',
            'visible' => false,
            'class_name' => 'ApiSecurityPsMbo',
            'parent_class_name' => 'AdminParentModulesSf',
        ],
    ];

    /**
     * This method is called when module is enabled/disabled.
     *
     * Apply given method on all Tabs
     * Values can be 'install' or 'uninstall'
     * If action is install, the tabs are activated if relevant.
     *
     * @param string $action
     *
     * @return bool
     *
     * @throws Exception
     */
    public function handleTabAction(string $action): bool
    {
        $methodName = (new UnicodeString($action))->camel() . 'Tab';
        if (!method_exists($this, $methodName)) {
            return false;
        }

        foreach (static::$ADMIN_CONTROLLERS as $tabData) {
            if (false === $this->{$methodName}($tabData)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Install Tab.
     * Used when module is enabled and in upgrade script.
     *
     * @param array $tabData
     *
     * @return bool
     */
    public function installTab(array $tabData): bool
    {
        $tabNameByLangId = array_fill_keys(
            Language::getIDs(false),
            $tabData['name']
        );

        $idParent = empty($tabData['parent_class_name']) ? -1 : Tab::getIdFromClassName($tabData['parent_class_name']);

        $tab = new Tab();

        // This will reorder the tabs starting with 1
        $tab->cleanPositions($idParent);

        $tab->module = $this->name;
        $tab->class_name = $tabData['class_name'];
        // For the position, we put the tab at the end of the parent children
        // and after that, place it in the right position
        $tab->position = Tab::getNewLastPosition($idParent);
        $tab->id_parent = $idParent;
        $tab->name = $tabNameByLangId;
        $tab->active = $tabData['visible'] ?: false;

        if (false === self::checkModuleStatus()) {
            // If the MBO module is not active, we disable all the tabs. They will be enabled when MBO is enabling
            $tab->active = false;
        }

        if (!empty($tabData['wording']) && !empty($tabData['wording_domain'])) {
            $tab->wording = $tabData['wording'];
            $tab->wording_domain = $tabData['wording_domain'];
        }

        if (false === $tab->add()) {
            return false;
        }

        if (Validate::isLoadedObject($tab)) {
            $position = $tabData['position'] ?? 0;
            $this->putTabInPosition($tab, $position);
        }

        return true;
    }

    /**
     * Uninstall Tab.
     * Can be used in upgrade script.
     *
     * @param array $tabData
     *
     * @return bool
     *
     * @throws \PrestaShopException
     */
    public function uninstallTab(array $tabData): bool
    {
        $tabId = Tab::getIdFromClassName($tabData['class_name']);
        $tab = new Tab($tabId);

        if (false === Validate::isLoadedObject($tab)) {
            return false;
        }

        if (false === $tab->delete()) {
            return false;
        }

        return true;
    }

    /**
     * This method is called on module upgrade.
     * Tabs will be updated if the module is active.
     * But they will be all unactivated.
     *
     * Update tabs in DB.
     * Search current tabs registered in DB and compare them with the tabs declared in the module.
     * If a tab is missing, it will be added. If a tab is not declared in the module, it will be removed.
     *
     * @return void
     * @throws \PrestaShopException
     */
    public function updateTabs(): void
    {
        $tabData = Db::getInstance()->executeS('
            SELECT class_name
            FROM `' . _DB_PREFIX_ . 'tab`
            WHERE `module` = "' . pSQL($this->name) . '"'
        );

        //Flatten $tabData array
        $tabData = array_unique(array_map('current', $tabData));
        $currentModuleTabs = array_keys(static::$ADMIN_CONTROLLERS);
        
        // First disable all the tabs to reset it all
        foreach ($tabData as $tabInDb) {
            try {
                $tab = new Tab((int) $tabInDb);
            } catch (\PrestaShopDatabaseException|\PrestaShopException $e) {
                continue;
            }

            if (false === Validate::isLoadedObject($tab)) {
                continue;
            }

            $tab->active = false;
            $tab->save();
        }

        $oldTabs = [];
        $newTabs = [];

        // Iterate on DB tabs to get only the old ones
        foreach ($tabData as $tabInDb) {
            if (!in_array($tabInDb, $currentModuleTabs)) {
                $oldTabs[] = $tabInDb;
            }
        }

        // Iterate on module tabs to get only the new ones
        foreach ($currentModuleTabs as $tab) {
            if (!in_array($tab, $tabData)) {
                $newTabs[] = $tab;
            }
        }

        // Delete the tabs that are not relevant anymore
        foreach ($oldTabs as $oldTab) {
            $this->uninstallTab(['class_name' => $oldTab]);
        }
        // Install the new tabs
        foreach ($newTabs as $newTab) {
            $this->installTab(static::$ADMIN_CONTROLLERS[$newTab]);
        }

        foreach ($currentModuleTabs as $currentModuleTab) {
            if (!in_array($currentModuleTab, $oldTabs) && !in_array($currentModuleTab, $newTabs)) {
                $tabData = static::$ADMIN_CONTROLLERS[$currentModuleTab];

                $idParent = empty($tabData['parent_class_name'])
                    ? -1
                    : Tab::getIdFromClassName($tabData['parent_class_name'])
                ;

                $tabId = Tab::getIdFromClassName($tabData['class_name']);
                try {
                    $tab = new Tab($tabId);

                    // This will reorder the tabs starting with 1
                    $tab->cleanPositions($idParent);
                } catch (\PrestaShopDatabaseException|\PrestaShopException $e) {
                    ErrorHelper::reportError($e);
                    throw new Exception('Failed to clean parent tab positions', 0, $e);
                }
            }
        }

        foreach ($currentModuleTabs as $currentModuleTab) {
            if (!in_array($currentModuleTab, $oldTabs) && !in_array($currentModuleTab, $newTabs)) {
                $this->updateTab(static::$ADMIN_CONTROLLERS[$currentModuleTab]);
            }
        }
    }

    /**
     * Updates an already existing tab.
     *
     * @param array $tabData
     */
    private function updateTab(array $tabData): bool
    {
        $tabId = Tab::getIdFromClassName($tabData['class_name']);
        try {
            $tab = new Tab($tabId);
        } catch (\PrestaShopDatabaseException|\PrestaShopException $e) {
            return false;
        }

        if (false === Validate::isLoadedObject($tab)) {
            return false;
        }

        $tabNameByLangId = array_fill_keys(
            Language::getIDs(false),
            $tabData['name']
        );

        $idParent = empty($tabData['parent_class_name']) ? -1 : Tab::getIdFromClassName($tabData['parent_class_name']);

        $tab->id_parent = $idParent;
        $tab->name = $tabNameByLangId;
        $tab->active = $tabData['visible'] ?: false;

        if (false === self::checkModuleStatus()) {
            // If the MBO module is not active, we disable all the tabs. They will be enabled when MBO is enabling
            $tab->active = false;
        }

        if (!empty($tabData['wording']) && !empty($tabData['wording_domain'])) {
            $tab->wording = $tabData['wording'];
            $tab->wording_domain = $tabData['wording_domain'];
        }

        if (false === $tab->save()) {
            return false;
        }

        try {
            $tab = new Tab($tabId);
        } catch (\PrestaShopDatabaseException|\PrestaShopException $e) {
            return false;
        }

        if (
            Validate::isLoadedObject($tab)
            && isset($tabData['position'])
            && (int) $tab->position !== (int) $tabData['position']
        ) {
            $this->putTabInPosition($tab, $tabData['position']);
        }

        return true;
    }

    private function putTabInPosition(Tab $tab, int $position): void
    {
        // Check tab position in DB
        $dbTabPosition = Db::getInstance()->getValue('
			SELECT `position`
			FROM `' . _DB_PREFIX_ . 'tab`
			WHERE `id_tab` = ' . (int) $tab->id
        );

        if ((int) $dbTabPosition === (int) $position) {
            // Nothing to do, tab is already in the right position
            return;
        }

        Db::getInstance()->execute(
            '
            UPDATE `' . _DB_PREFIX_ . 'tab`
            SET `position` = `position`+1
            WHERE `id_parent` = ' . (int) $tab->id_parent . '
            AND `position` >= ' . $position . '
            AND `id_tab` <> ' . (int) $tab->id
        );

        Db::getInstance()->execute(
            '
                UPDATE `' . _DB_PREFIX_ . 'tab`
                SET `position` = ' . $position . '
                WHERE `id_tab` = ' . (int) $tab->id
        );
    }
}