From 972b8d965bdd23de11c3b39c5de0797e907935d5 Mon Sep 17 00:00:00 2001 From: Isabelle Date: Wed, 30 Sep 2026 14:52:38 +0200 Subject: [PATCH] initial module --- .gitignore | 1 + composer.json | 28 ++ composer.lock | 20 ++ logo.png | Bin 0 -> 12032 bytes src/Config/Config.php | 93 +++++++ src/Module/AdminHooks.php | 292 ++++++++++++++++++++ src/Module/DisplayHooks.php | 83 ++++++ src/Module/Install.php | 108 ++++++++ views/css/front.css | 29 ++ views/templates/admin/configuration.tpl | 181 ++++++++++++ views/templates/hook/_partials/trustbox.tpl | 11 + views/templates/hook/afs_snippet.tpl | 1 + views/templates/hook/trustbox_desktop.tpl | 3 + views/templates/hook/trustbox_mobile.tpl | 5 + ws_trustpilotafsbcc.php | 56 ++++ 15 files changed, 911 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 logo.png create mode 100644 src/Config/Config.php create mode 100644 src/Module/AdminHooks.php create mode 100644 src/Module/DisplayHooks.php create mode 100644 src/Module/Install.php create mode 100644 views/css/front.css create mode 100644 views/templates/admin/configuration.tpl create mode 100644 views/templates/hook/_partials/trustbox.tpl create mode 100644 views/templates/hook/afs_snippet.tpl create mode 100644 views/templates/hook/trustbox_desktop.tpl create mode 100644 views/templates/hook/trustbox_mobile.tpl create mode 100644 ws_trustpilotafsbcc.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1efd347 --- /dev/null +++ b/composer.json @@ -0,0 +1,28 @@ +{ + "name": "websmid/trustpilotafsbcc", + "description": "BCCs the order confirmation e-mail to Trustpilot's AFS invitation address", + "type": "prestashop-module", + "authors": [ + { + "name": "Isabelle Oving-Anno - De Websmid BV", + "email": "isabelle@dewebsmid.nl" + } + ], + "require": { + "php": ">=8.1.0" + }, + "autoload": { + "psr-4": { + "Websmid\\TrustpilotAfsBcc\\": "src/" + }, + "classmap": [ + "ws_trustpilotafsbcc.php" + ], + "exclude-from-classmap": [] + }, + "config": { + "preferred-install": "dist", + "prepend-autoloader": false + }, + "license": "MIT" +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..54ccbd6 --- /dev/null +++ b/composer.lock @@ -0,0 +1,20 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "e29c932400094498561eeb7d4c1bf468", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1.0" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..022f87b19e2e309e7357153969cff5ab67b6a977 GIT binary patch literal 12032 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 literal 0 HcmV?d00001 diff --git a/src/Config/Config.php b/src/Config/Config.php new file mode 100644 index 0000000..93d40a5 --- /dev/null +++ b/src/Config/Config.php @@ -0,0 +1,93 @@ + 'dark', + 'Light' => 'light', + ]; + + const TRUSTBOX_SCRIPT_URL = 'https://widget.trustpilot.com/bootstrap/v5/tp.widget.bootstrap.min.js'; + + const TRUSTBOX_TEMPLATE_DIR = 'module:ws_trustpilotafsbcc/views/templates/hook/'; + + // Display hook => template rendered in it + const TRUSTBOX_HOOK_TEMPLATES = [ + 'displayTopUsp' => 'trustbox_desktop.tpl', + 'displayBanner' => 'trustbox_mobile.tpl', + ]; + + // Front controller (php_self) the TrustBox is shown on + const TRUSTBOX_PAGE = 'index'; + + // Language defaults, keyed by PrestaShop iso_code + + const DEFAULT_LOCALES = [ + 'nl' => 'nl-BE', + 'fr' => 'fr-BE', + 'en' => 'en-GB', + 'de' => 'de-DE', + ]; + + const PROFILE_HOSTS = [ + 'nl' => 'nl-be.trustpilot.com', + 'fr' => 'fr-be.trustpilot.com', + 'en' => 'www.trustpilot.com', + 'de' => 'de.trustpilot.com', + ]; + + const DEFAULT_PROFILE_HOST = 'www.trustpilot.com'; + + // Validation patterns + + const PATTERN_TRUSTPILOT_ID = '/^[a-f0-9]{24}$/i'; + + const PATTERN_LOCALE = '/^[a-z]{2}-[A-Z]{2}$/'; +} diff --git a/src/Module/AdminHooks.php b/src/Module/AdminHooks.php new file mode 100644 index 0000000..c49769e --- /dev/null +++ b/src/Module/AdminHooks.php @@ -0,0 +1,292 @@ +saveConfiguration(); + } + + $this->context->smarty->assign(array_merge( + $this->loadConfigurationValues(), + [ + 'languages' => Language::getLanguages(false), + 'trustbox_themes' => Config::TRUSTBOX_THEMES, + 'form_action' => $this->context->link->getAdminLink('AdminModules') . '&configure=' . $this->name, + 'confirmation' => $confirmation, + ] + )); + + return $this->fetch('module:ws_trustpilotafsbcc/views/templates/admin/configuration.tpl'); + } + + private function loadConfigurationValues(): array + { + return [ + 'bcc_email' => Configuration::get(Config::CONFIG_BCC_EMAIL), + 'trigger_templates' => Configuration::get(Config::CONFIG_TRIGGER_TEMPLATES), + 'afs_locales' => Configuration::getConfigInMultipleLangs(Config::CONFIG_AFS_LOCALE), + 'afs_template_ids' => Configuration::getConfigInMultipleLangs(Config::CONFIG_AFS_TEMPLATE_ID), + 'trustbox_enabled' => (bool) Configuration::get(Config::CONFIG_TRUSTBOX_ENABLED), + 'trustbox_business_unit_id' => Configuration::get(Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID), + 'trustbox_template_id' => Configuration::get(Config::CONFIG_TRUSTBOX_TEMPLATE_ID), + 'trustbox_theme' => Configuration::get(Config::CONFIG_TRUSTBOX_THEME), + 'trustbox_locales' => Configuration::getConfigInMultipleLangs(Config::CONFIG_TRUSTBOX_LOCALE), + 'trustbox_profile_urls' => Configuration::getConfigInMultipleLangs(Config::CONFIG_TRUSTBOX_PROFILE_URL), + ]; + } + + private function saveConfiguration(): string + { + $values = $this->buildSubmittedValues(); + $errors = $this->validateSubmittedValues($values); + + if (!empty($errors)) { + return $this->displayError($errors); + } + + foreach ($values as $key => $value) { + Configuration::updateValue($key, $value); + } + + $this->clearTrustboxCache(); + + return $this->displayConfirmation($this->trans('Settings saved.', [], 'Admin.Notifications.Success')); + } + + private function buildSubmittedValues(): array + { + $theme = (string) Tools::getValue('trustbox_theme'); + + return [ + Config::CONFIG_BCC_EMAIL => trim((string) Tools::getValue('bcc_email')), + Config::CONFIG_TRIGGER_TEMPLATES => trim((string) Tools::getValue('trigger_templates')), + Config::CONFIG_AFS_LOCALE => $this->buildSubmittedLangValues('afs_locale'), + Config::CONFIG_AFS_TEMPLATE_ID => $this->buildSubmittedLangValues('afs_template_id'), + Config::CONFIG_TRUSTBOX_ENABLED => (int) (bool) Tools::getValue('trustbox_enabled'), + Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID => trim((string) Tools::getValue('trustbox_business_unit_id')), + Config::CONFIG_TRUSTBOX_TEMPLATE_ID => trim((string) Tools::getValue('trustbox_template_id')), + Config::CONFIG_TRUSTBOX_THEME => in_array($theme, Config::TRUSTBOX_THEMES, true) ? $theme : Config::DEFAULT_TRUSTBOX_THEME, + Config::CONFIG_TRUSTBOX_LOCALE => $this->buildSubmittedLangValues('trustbox_locale'), + Config::CONFIG_TRUSTBOX_PROFILE_URL => $this->buildSubmittedLangValues('trustbox_profile_url'), + ]; + } + + private function buildSubmittedLangValues(string $field): array + { + $submitted = Tools::getValue($field); + $values = []; + + foreach (Language::getLanguages(false) as $language) { + $idLang = (int) $language['id_lang']; + $values[$idLang] = is_array($submitted) ? trim((string) ($submitted[$idLang] ?? '')) : ''; + } + + return $values; + } + + private function validateSubmittedValues(array $values): array + { + $errors = []; + $bccEmail = $values[Config::CONFIG_BCC_EMAIL]; + + if ($bccEmail !== '' && !Validate::isEmail($bccEmail)) { + $errors[] = $this->trans('Please enter a valid e-mail address.', [], 'Modules.Wstrustpilotafsbcc.Admin'); + } + + if (!$this->isValidTrustpilotId($values[Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID]) + || !$this->isValidTrustpilotId($values[Config::CONFIG_TRUSTBOX_TEMPLATE_ID]) + || !$this->areValidTrustpilotIds($values[Config::CONFIG_AFS_TEMPLATE_ID]) + ) { + $errors[] = $this->trans('Trustpilot IDs must be 24 characters (0-9, a-f).', [], 'Modules.Wstrustpilotafsbcc.Admin'); + } + + if (!$this->areValidLocales($values[Config::CONFIG_AFS_LOCALE]) + || !$this->areValidLocales($values[Config::CONFIG_TRUSTBOX_LOCALE]) + ) { + $errors[] = $this->trans('Locales must look like nl-BE.', [], 'Modules.Wstrustpilotafsbcc.Admin'); + } + + foreach ($values[Config::CONFIG_TRUSTBOX_PROFILE_URL] as $profileUrl) { + if ($profileUrl !== '' && !Validate::isAbsoluteUrl($profileUrl)) { + $errors[] = $this->trans('Profile URLs must be full URLs starting with https://.', [], 'Modules.Wstrustpilotafsbcc.Admin'); + break; + } + } + + if ($values[Config::CONFIG_TRUSTBOX_ENABLED] && $values[Config::CONFIG_TRUSTBOX_BUSINESS_UNIT_ID] === '') { + $errors[] = $this->trans('The TrustBox needs a business unit ID before it can be enabled.', [], 'Modules.Wstrustpilotafsbcc.Admin'); + } + + return $errors; + } + + private function isValidTrustpilotId(string $value): bool + { + return $value === '' || preg_match(Config::PATTERN_TRUSTPILOT_ID, $value) === 1; + } + + private function areValidTrustpilotIds(array $values): bool + { + foreach ($values as $value) { + if (!$this->isValidTrustpilotId($value)) { + return false; + } + } + + return true; + } + + private function areValidLocales(array $values): bool + { + foreach ($values as $value) { + if ($value !== '' && preg_match(Config::PATTERN_LOCALE, $value) !== 1) { + return false; + } + } + + return true; + } + + private function clearTrustboxCache(): void + { + foreach (Config::TRUSTBOX_HOOK_TEMPLATES as $template) { + $this->_clearCache(Config::TRUSTBOX_TEMPLATE_DIR . $template); + } + } + + // In the PS core Mail::send() dispatches actionEmailSendBefore before it validates $bcc, passing + // bcc' => &$bcc by reference. Writing $params['bcc'] here rebinds that reference and + // propagates back to Mail::send()'s own $bcc variable + + public function hookActionEmailSendBefore(array $params) + { + if (!$this->isAfsActiveFor((string) ($params['template'] ?? ''))) { + return; + } + + $params['bcc'] = $this->buildBccValue($params['bcc'] ?? null, (string) Configuration::get(Config::CONFIG_BCC_EMAIL)); + } + + // Mail::send() passes 'extra_template_vars' => &$extraTemplateVars and merges it into the + // template vars right before strtr() fills in the HTML, so the JSON lands in the placeholder + // injected by hookActionEmailAddAfterContent below + + public function hookActionGetExtraMailTemplateVars(array $params) + { + if (!$this->isAfsActiveFor((string) ($params['template'] ?? ''))) { + return; + } + + $params['extra_template_vars'][Config::AFS_SNIPPET_VAR] = $this->buildAfsSnippetJson( + (array) ($params['template_vars'] ?? []), + (int) ($params['id_lang'] ?? 0) + ); + } + + // Mail::send() passes 'template_html' => &$templateHtml after loading the template file and + // before the template vars are applied. Only the HTML part gets the snippet + + public function hookActionEmailAddAfterContent(array $params) + { + if (!$this->isAfsActiveFor((string) ($params['template'] ?? ''))) { + return; + } + + $params['template_html'] = $this->buildHtmlWithSnippet( + (string) ($params['template_html'] ?? ''), + $this->renderAfsSnippet() + ); + } + + private function isAfsActiveFor(string $template): bool + { + $bccEmail = (string) Configuration::get(Config::CONFIG_BCC_EMAIL); + + if ($bccEmail === '' || !Validate::isEmail($bccEmail)) { + return false; + } + + return in_array($template, $this->loadTriggerTemplates(), true); + } + + private function loadTriggerTemplates(): array + { + $raw = (string) Configuration::get(Config::CONFIG_TRIGGER_TEMPLATES); + + return array_filter(array_map('trim', explode(',', $raw))); + } + + private function buildBccValue($existingBcc, string $bccEmail) + { + if (is_array($existingBcc)) { + $existingBcc[] = $bccEmail; + + return $existingBcc; + } + + if (!empty($existingBcc)) { + return [$existingBcc, $bccEmail]; + } + + return $bccEmail; + } + + private function buildAfsSnippetJson(array $templateVars, int $idLang): string + { + $firstname = (string) ($templateVars['{firstname}'] ?? ''); + $lastname = (string) ($templateVars['{lastname}'] ?? ''); + + $data = array_filter([ + 'recipientEmail' => (string) ($templateVars['{email}'] ?? ''), + 'recipientName' => trim($firstname . ' ' . $lastname), + 'referenceId' => (string) ($templateVars['{order_name}'] ?? ''), + 'locale' => (string) Configuration::get(Config::CONFIG_AFS_LOCALE, $idLang), + 'templateId' => (string) Configuration::get(Config::CONFIG_AFS_TEMPLATE_ID, $idLang), + ], static function (string $value) { + return $value !== ''; + }); + + // HEX flags keep <, >, &, ' and " out of the output, so it is safe inside a diff --git a/views/templates/hook/trustbox_desktop.tpl b/views/templates/hook/trustbox_desktop.tpl new file mode 100644 index 0000000..78d0048 --- /dev/null +++ b/views/templates/hook/trustbox_desktop.tpl @@ -0,0 +1,3 @@ +
+ {include file='module:ws_trustpilotafsbcc/views/templates/hook/_partials/trustbox.tpl'} +
diff --git a/views/templates/hook/trustbox_mobile.tpl b/views/templates/hook/trustbox_mobile.tpl new file mode 100644 index 0000000..5473806 --- /dev/null +++ b/views/templates/hook/trustbox_mobile.tpl @@ -0,0 +1,5 @@ +
+
+ {include file='module:ws_trustpilotafsbcc/views/templates/hook/_partials/trustbox.tpl'} +
+
diff --git a/ws_trustpilotafsbcc.php b/ws_trustpilotafsbcc.php new file mode 100644 index 0000000..1f4e12d --- /dev/null +++ b/ws_trustpilotafsbcc.php @@ -0,0 +1,56 @@ + + * @copyright 2007-2026 De Websmid BV + * @license Valid for 1 website (or project) for each purchase of license + * International Registered Trademark & Property of De Websmid BV + */ + +use Websmid\TrustpilotAfsBcc\Module\Install; +use Websmid\TrustpilotAfsBcc\Module\AdminHooks; +use Websmid\TrustpilotAfsBcc\Module\DisplayHooks; +use PrestaShop\PrestaShop\Core\Module\WidgetInterface; + +if (!defined('_PS_VERSION_')) { + exit; +} + +if (file_exists(__DIR__ . '/vendor/autoload.php')) { + require_once __DIR__ . '/vendor/autoload.php'; +} + +class Ws_TrustpilotAfsBcc extends Module implements WidgetInterface +{ + use Install; + use AdminHooks; + use DisplayHooks; + + public function __construct() + { + $this->name = 'ws_trustpilotafsbcc'; + $this->version = '1.1.0'; + $this->author = 'Isabelle Oving-Anno | De Websmid BV'; + $this->need_instance = 0; + $this->bootstrap = true; + + parent::__construct(); + + $this->displayName = $this->trans('Trustpilot AFS + TrustBox', [], 'Modules.Wstrustpilotafsbcc.Admin'); + $this->description = $this->trans('Sends Trustpilot review invitations in the customer\'s language by BCCing the order confirmation to Trustpilot\'s Automatic Feedback Service, and shows a TrustBox widget in the shop header.', [], 'Modules.Wstrustpilotafsbcc.Admin'); + $this->ps_versions_compliancy = ['min' => '9.0.0', 'max' => _PS_VERSION_]; + } +}