diff --git a/composer.json b/composer.json index 03f355b..dd6349d 100644 --- a/composer.json +++ b/composer.json @@ -21,5 +21,10 @@ "optimize-autoloader": true, "prepend-autoloader": false }, + "autoload": { + "psr-4": { + "PrestaShop\\Module\\DashProducts\\": "src/" + } + }, "type": "prestashop-module" } diff --git a/config.xml b/config.xml index 8f0a0fe..81799ef 100644 --- a/config.xml +++ b/config.xml @@ -2,11 +2,11 @@ dashproducts - - + + - - 0 + + 1 1 diff --git a/config/index.php b/config/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/config/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/config/routes.yml b/config/routes.yml new file mode 100644 index 0000000..52d4693 --- /dev/null +++ b/config/routes.yml @@ -0,0 +1,7 @@ +dashproducts_configuration: + path: /dashproducts/configuration + methods: [GET, POST] + defaults: + _controller: PrestaShop\Module\DashProducts\Controller\ConfigurationController::indexAction + _legacy_controller: AdminDashproductsConfiguration + _legacy_link: AdminDashproductsConfiguration diff --git a/config/services.yml b/config/services.yml new file mode 100644 index 0000000..af29789 --- /dev/null +++ b/config/services.yml @@ -0,0 +1,10 @@ +services: + PrestaShop\Module\DashProducts\Controller\ConfigurationController: + public: true + autowire: true + autoconfigure: true + + PrestaShop\Module\DashProducts\Type\ConfigurationType: + autowire: true + autoconfigure: true + tags: ['form.type'] diff --git a/dashproducts.php b/dashproducts.php index cc72ef6..cf63362 100755 --- a/dashproducts.php +++ b/dashproducts.php @@ -23,6 +23,9 @@ * @copyright Since 2007 PrestaShop SA and Contributors * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) */ +use PrestaShop\PrestaShop\Adapter\SymfonyContainer; +use Twig\Environment; + if (!defined('_PS_VERSION_')) { exit; } @@ -33,7 +36,7 @@ public function __construct() { $this->name = 'dashproducts'; $this->tab = 'administration'; - $this->version = '2.2.1'; + $this->version = '3.0.0'; $this->author = 'PrestaShop'; parent::__construct(); @@ -50,11 +53,54 @@ public function install() Configuration::updateValue('DASHPRODUCT_NBR_SHOW_TOP_SEARCH', 10); return parent::install() + && $this->createConfigurationTab() && $this->registerHook('dashboardZoneTwo') && $this->registerHook('dashboardData') + // Modern counterpart of dashboardZoneTwo, registered alongside it (#41971). + && $this->registerHook('displayAdminDashboardZoneTwo') ; } + public function uninstall() + { + $idTab = (int) Tab::getIdFromClassName('AdminDashproductsConfiguration'); + if ($idTab) { + (new Tab($idTab))->delete(); + } + + return parent::uninstall(); + } + + /** + * Hidden tab (id_parent -1): only used to back the settings route's ACL. Shared between + * install() and the 3.0.0 upgrade script so shops upgrading from an earlier version get it too. + */ + public function createConfigurationTab(): bool + { + if (Tab::getIdFromClassName('AdminDashproductsConfiguration')) { + return true; + } + + $tab = new Tab(); + $tab->active = true; + $tab->class_name = 'AdminDashproductsConfiguration'; + $tab->name = []; + foreach (Language::getLanguages(true) as $lang) { + $tab->name[$lang['id_lang']] = 'Dashproducts configuration'; + } + $tab->id_parent = -1; + $tab->module = $this->name; + + return $tab->add(); + } + + public function getContent() + { + Tools::redirectAdmin( + SymfonyContainer::getInstance()->get('router')->generate('dashproducts_configuration') + ); + } + public function hookDashboardZoneTwo($params) { $this->context->smarty->assign( @@ -90,6 +136,46 @@ public function hookDashboardData($params) ]; } + /** + * Modern counterpart of hookDashboardZoneTwo(). Tables are plain server-rendered HTML. + */ + public function hookDisplayAdminDashboardZoneTwo(array $params) + { + $data = $this->hookDashboardData($params); + + return $this->render('zone_two.html.twig', [ + 'configUrl' => $this->getConfigUrl(), + 'tables' => [ + ['title' => $this->trans('Recent Orders', [], 'Modules.Dashproducts.Admin'), 'table' => $data['data_table']['table_recent_orders']], + ['title' => $this->trans('Best Sellers', [], 'Modules.Dashproducts.Admin'), 'table' => $data['data_table']['table_best_sellers']], + ['title' => $this->trans('Most Viewed', [], 'Modules.Dashproducts.Admin'), 'table' => $data['data_table']['table_most_viewed']], + ['title' => $this->trans('Top Searches', [], 'Modules.Dashproducts.Admin'), 'table' => $data['data_table']['table_top_10_most_search']], + ], + ]); + } + + private function render(string $template, array $params = []): string + { + $twig = $this->get('twig'); + if (!$twig instanceof Environment) { + return ''; + } + + return $twig->render('@Modules/dashproducts/views/templates/admin/' . $template, $params); + } + + /** + * Null (no "Configure" link shown) when the current employee can't configure this module. + */ + private function getConfigUrl(): ?string + { + if (!Tab::checkTabRights((int) Tab::getIdFromClassName('AdminDashproductsConfiguration'))) { + return null; + } + + return SymfonyContainer::getInstance()->get('router')->generate('dashproducts_configuration'); + } + public function getTableRecentOrders() { $header = [ @@ -132,7 +218,7 @@ public function getTableRecentOrders() 'value' => $this->context->getCurrentLocale()->formatPrice((float) $order['total_paid_tax_excl'], $currency['iso_code']), 'class' => 'text-center', 'wrapper_start' => $order['valid'] ? '' : '', - 'wrapper_end' => '', + 'wrapper_end' => $order['valid'] ? '' : '', ]; $tr[] = [ 'id' => 'date_add', @@ -148,7 +234,7 @@ public function getTableRecentOrders() 'id' => 'details', 'value' => '', 'class' => 'text-right', - 'wrapper_start' => '', + 'wrapper_start' => 'search', 'wrapper_end' => '', ]; @@ -361,7 +447,7 @@ public function getTableMostViewed($date_from, $date_to) $purchased = $this->getTotalProductPurchased($date_from, $date_to, (int) $product_obj->id); $tr[] = [ 'id' => 'purchased', - 'value' => $this->getTotalProductPurchased($date_from, $date_to, (int) $product_obj->id), + 'value' => $purchased, 'class' => 'text-center', ]; $tr[] = [ @@ -406,7 +492,9 @@ public function getTableTop10MostSearch($date_from, $date_to) $tr = []; $tr[] = [ 'id' => 'product', - 'value' => $term['keywords'], + // Customer-supplied search terms: escaped, unlike other columns in this + // table which are trusted server-computed numbers. + 'value' => Tools::htmlentitiesUTF8($term['keywords']), 'class' => 'text-left', ]; $tr[] = [ diff --git a/src/Controller/ConfigurationController.php b/src/Controller/ConfigurationController.php new file mode 100644 index 0000000..c89bbff --- /dev/null +++ b/src/Controller/ConfigurationController.php @@ -0,0 +1,77 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\DashProducts\Controller; + +use Configuration; +use PrestaShop\Module\DashProducts\Type\ConfigurationType; +// FrameworkBundleAdminController is deprecated since 9.0 in favor of PrestaShopAdminController, +// which doesn't exist on PS 8.2 — kept for the module's declared 8.2+ compatibility. +use PrestaShopBundle\Controller\Admin\FrameworkBundleAdminController; +use PrestaShopBundle\Security\Annotation\AdminSecurity; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +class ConfigurationController extends FrameworkBundleAdminController +{ + private const FIELDS = [ + 'DASHPRODUCT_NBR_SHOW_LAST_ORDER', + 'DASHPRODUCT_NBR_SHOW_BEST_SELLER', + 'DASHPRODUCT_NBR_SHOW_MOST_VIEWED', + 'DASHPRODUCT_NBR_SHOW_TOP_SEARCH', + ]; + + /** + * @AdminSecurity("is_granted('read', request.get('_legacy_controller'))") + */ + public function indexAction(Request $request): Response + { + $data = []; + foreach (self::FIELDS as $field) { + $data[$field] = (int) Configuration::get($field); + } + + $form = $this->createForm(ConfigurationType::class, $data); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->denyAccessUnlessGranted('update', $request->attributes->get('_legacy_controller')); + + // No @DemoRestricted attribute here: its listener only recognizes the PHP 8 + // attribute form (PrestaShopBundle\Security\Attribute\DemoRestricted), which + // doesn't exist on PS 8.2 — this module's declared minimum version. + if ($this->isDemoModeEnabled()) { + $this->addFlash('error', $this->trans('This functionality has been disabled.', 'Admin.Notifications.Error')); + + return $this->redirectToRoute('dashproducts_configuration'); + } + + foreach ($form->getData() as $field => $value) { + Configuration::updateValue($field, (int) $value); + } + $this->addFlash('success', $this->trans('Successful update.', 'Admin.Notifications.Success')); + + return $this->redirectToRoute('dashproducts_configuration'); + } + + return $this->render('@Modules/dashproducts/views/templates/admin/configuration.html.twig', [ + 'configurationForm' => $form->createView(), + 'enableSidebar' => true, + 'help_link' => $this->generateSidebarLink('AdminDashproductsConfiguration'), + ]); + } +} diff --git a/src/Controller/index.php b/src/Controller/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/src/Controller/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/src/Type/ConfigurationType.php b/src/Type/ConfigurationType.php new file mode 100644 index 0000000..1ecba4f --- /dev/null +++ b/src/Type/ConfigurationType.php @@ -0,0 +1,77 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ + +declare(strict_types=1); + +namespace PrestaShop\Module\DashProducts\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Plain AbstractType (no need for TranslatorAwareType's locales), with TranslatorInterface + * constructor-injected directly so label strings go through an explicit trans() call — + * required for the translation extractor to pick them up, since it only extracts ChoiceType + * "choices", not "label" option strings. + */ +class ConfigurationType extends AbstractType +{ + private const ROW_COUNT_CHOICES = [5, 10, 20, 50]; + + private const DOMAIN = 'Modules.Dashproducts.Admin'; + + /** @var TranslatorInterface */ + private $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = array_combine(self::ROW_COUNT_CHOICES, self::ROW_COUNT_CHOICES); + + $builder + ->add('DASHPRODUCT_NBR_SHOW_LAST_ORDER', ChoiceType::class, [ + 'label' => $this->translator->trans('Number of "Recent Orders" to display', [], self::DOMAIN), + 'choices' => $choices, + ]) + ->add('DASHPRODUCT_NBR_SHOW_BEST_SELLER', ChoiceType::class, [ + 'label' => $this->translator->trans('Number of "Best Sellers" to display', [], self::DOMAIN), + 'choices' => $choices, + ]) + ->add('DASHPRODUCT_NBR_SHOW_MOST_VIEWED', ChoiceType::class, [ + 'label' => $this->translator->trans('Number of "Most Viewed" to display', [], self::DOMAIN), + 'choices' => $choices, + ]) + ->add('DASHPRODUCT_NBR_SHOW_TOP_SEARCH', ChoiceType::class, [ + 'label' => $this->translator->trans('Number of "Top Searches" to display', [], self::DOMAIN), + 'choices' => $choices, + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'required' => true, + 'translation_domain' => 'Modules.Dashproducts.Admin', + ]); + } +} diff --git a/src/Type/index.php b/src/Type/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/src/Type/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/src/index.php b/src/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/src/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/tests/php/phpstan/phpstan.neon b/tests/php/phpstan/phpstan.neon index 018ab98..0cb7f98 100644 --- a/tests/php/phpstan/phpstan.neon +++ b/tests/php/phpstan/phpstan.neon @@ -1,2 +1,10 @@ # Base PHPStan configuration for the module - # This file is included by version-specific configs \ No newline at end of file + # This file is included by version-specific configs + +parameters: + # Reset to auto-detection: the shared CI action injects a phpVersion computed from the + # matrix's php-version input, which some older PrestaShop releases' pinned PHPStan can't + # parse once that input reaches a version newer than its own release date (PHPStan's + # phpVersion schema is capped at whatever PHP existed when that PHPStan version shipped). + # Included last, so this wins over the injected value (see PrestaShop/.github#phpstan). + phpVersion: null diff --git a/upgrade/index.php b/upgrade/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/upgrade/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/upgrade/upgrade-3.0.0.php b/upgrade/upgrade-3.0.0.php new file mode 100644 index 0000000..d9ef19b --- /dev/null +++ b/upgrade/upgrade-3.0.0.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +if (!defined('_PS_VERSION_')) { + exit; +} + +function upgrade_module_3_0_0($object) +{ + return $object->createConfigurationTab() + && $object->registerHook('displayAdminDashboardZoneTwo'); +} diff --git a/views/templates/admin/configuration.html.twig b/views/templates/admin/configuration.html.twig new file mode 100644 index 0000000..e5ae3f5 --- /dev/null +++ b/views/templates/admin/configuration.html.twig @@ -0,0 +1,31 @@ +{#** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * This source file is subject to the Academic Free License 3.0 (AFL-3.0). + * It is also available through the world-wide-web at this URL: https://opensource.org/licenses/AFL-3.0 + *#} +{% extends '@PrestaShop/Admin/layout.html.twig' %} + +{% block content %} + {{ form_start(configurationForm) }} +
+
+
+

+ settings + {{ 'Products dashboard settings'|trans({}, 'Modules.Dashproducts.Admin') }} +

+
+ {{ form_widget(configurationForm) }} +
+ +
+
+
+ {{ form_end(configurationForm) }} +{% endblock %} diff --git a/views/templates/admin/index.php b/views/templates/admin/index.php new file mode 100644 index 0000000..45df26c --- /dev/null +++ b/views/templates/admin/index.php @@ -0,0 +1,34 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0) + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/views/templates/admin/zone_two.html.twig b/views/templates/admin/zone_two.html.twig new file mode 100644 index 0000000..e8400a0 --- /dev/null +++ b/views/templates/admin/zone_two.html.twig @@ -0,0 +1,38 @@ +{# + # For the full copyright and license information, please view the + # docs/licenses/LICENSE.txt file that was distributed with this source code. + #} +{% for entry in tables %} +
+ {% component 'Card' with {title: entry.title, configUrl: loop.first ? configUrl : null} %} + {% block content %} + {% if entry.table.body is iterable and entry.table.body|length %} +
+ + + + {% for column in entry.table.header %} + + {% endfor %} + + + + {% for row in entry.table.body %} + + {% for cell in row %} + + {% endfor %} + + {% endfor %} + +
{{ column.title }}
{{ cell.wrapper_start|default('')|raw }}{{ cell.value|raw }}{{ cell.wrapper_end|default('')|raw }}
+
+ {% elseif entry.table.body is iterable %} +

{{ 'No data'|trans({}, 'Modules.Dashproducts.Admin') }}

+ {% else %} + {{ entry.table.body|raw }} + {% endif %} + {% endblock %} + {% endcomponent %} +
+{% endfor %}