Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ GitHub contributors:
- Shiryu75
- daresh
- eternoendless
- guillaumeArgiles
- netraagal
- v4lux
18 changes: 18 additions & 0 deletions config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ services:
- "@prestashop.adapter.group.provider.default_groups_provider"
- "@hashing"

PrestaShop\Module\Psgdpr\Service\DataRetention\DataRetentionService:
class: 'PrestaShop\Module\Psgdpr\Service\DataRetention\DataRetentionService'
# Public so the module can resolve it from the actionCronJob hook.
public: true
arguments:
- "@Psgdpr"
- '@=service("prestashop.adapter.legacy.context").getContext()'
- '@PrestaShop\Module\Psgdpr\Repository\CustomerRepository'
- '@PrestaShop\Module\Psgdpr\Service\CustomerService'
- '@PrestaShop\Module\Psgdpr\Service\LoggerService'

PrestaShop\Module\Psgdpr\Command\DataRetentionCommand:
class: 'PrestaShop\Module\Psgdpr\Command\DataRetentionCommand'
arguments:
- '@PrestaShop\Module\Psgdpr\Service\DataRetention\DataRetentionService'
tags:
- { name: 'console.command' }

PrestaShop\Module\Psgdpr\Service\Export\Strategy\ExportToCsv:
class: 'PrestaShop\Module\Psgdpr\Service\Export\Strategy\ExportToCsv'
tags: ["psgdpr.export.customerData"]
Expand Down
52 changes: 52 additions & 0 deletions mails/en/retention_warning.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{shop_name}</title>
</head>
<body style="margin:0; padding:0; background-color:#f6f7f9; font-family:Arial,Helvetica,sans-serif; color:#363a41;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f6f7f9;">
<tr>
<td align="center" style="padding:24px 12px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px; width:100%; background-color:#ffffff; border-radius:6px; overflow:hidden; border:1px solid #e0e3e8;">
<tr>
<td align="center" style="background-color:#1d2b3a; padding:24px;">
<img src="{shop_logo}" alt="{shop_name}" style="max-height:48px; border:0;">
</td>
</tr>
<tr>
<td style="padding:32px 32px 8px 32px;">
<h2 style="margin:0 0 16px 0; font-size:20px; color:#1d2b3a;">Your account is about to be anonymized</h2>
<p style="margin:0 0 16px 0; font-size:15px; line-height:22px;">Hello {firstname} {lastname},</p>
<p style="margin:0 0 16px 0; font-size:15px; line-height:22px;">
You have not signed in to your account at <strong>{shop_name}</strong> for a long time. In line with data-protection rules (GDPR), we automatically anonymize the personal data of inactive customers.
</p>
<p style="margin:0 0 24px 0; font-size:15px; line-height:22px;">
If you wish to keep your account active, simply sign in again by clicking the button below. Otherwise, your personal data will be anonymized soon.
</p>
</td>
</tr>
<tr>
<td align="center" style="padding:0 32px 32px 32px;">
<a href="{reconnect_url}" style="display:inline-block; background-color:#2576c6; color:#ffffff; text-decoration:none; font-size:15px; font-weight:bold; padding:12px 28px; border-radius:4px;">Keep my account active</a>
</td>
</tr>
<tr>
<td style="padding:0 32px 32px 32px;">
<p style="margin:0; font-size:12px; line-height:18px; color:#7a8089;">
Your orders and invoices are kept for the legal retention period required by law; only data identifying you personally is anonymized.
</p>
</td>
</tr>
<tr>
<td align="center" style="background-color:#f0f2f5; padding:18px; font-size:12px; color:#7a8089;">
<a href="{shop_url}" style="color:#2576c6; text-decoration:none;">{shop_name}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
14 changes: 14 additions & 0 deletions mails/en/retention_warning.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Your account is about to be anonymized

Hello {firstname} {lastname},

You have not signed in to your account at {shop_name} for a long time. In line with data-protection rules (GDPR), we automatically anonymize the personal data of inactive customers.

If you wish to keep your account active, simply sign in again here:
{reconnect_url}

Otherwise, your personal data will be anonymized soon.

Your orders and invoices are kept for the legal retention period required by law; only data identifying you personally is anonymized.

{shop_name} - {shop_url}
52 changes: 52 additions & 0 deletions mails/fr/retention_warning.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{shop_name}</title>
</head>
<body style="margin:0; padding:0; background-color:#f6f7f9; font-family:Arial,Helvetica,sans-serif; color:#363a41;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background-color:#f6f7f9;">
<tr>
<td align="center" style="padding:24px 12px;">
<table role="presentation" width="600" cellpadding="0" cellspacing="0" style="max-width:600px; width:100%; background-color:#ffffff; border-radius:6px; overflow:hidden; border:1px solid #e0e3e8;">
<tr>
<td align="center" style="background-color:#1d2b3a; padding:24px;">
<img src="{shop_logo}" alt="{shop_name}" style="max-height:48px; border:0;">
</td>
</tr>
<tr>
<td style="padding:32px 32px 8px 32px;">
<h2 style="margin:0 0 16px 0; font-size:20px; color:#1d2b3a;">Votre compte va bientôt être anonymisé</h2>
<p style="margin:0 0 16px 0; font-size:15px; line-height:22px;">Bonjour {firstname} {lastname},</p>
<p style="margin:0 0 16px 0; font-size:15px; line-height:22px;">
Vous ne vous êtes pas connecté à votre compte sur <strong>{shop_name}</strong> depuis longtemps. Conformément à la réglementation sur la protection des données (RGPD), nous anonymisons automatiquement les données personnelles des clients inactifs.
</p>
<p style="margin:0 0 24px 0; font-size:15px; line-height:22px;">
Si vous souhaitez conserver votre compte actif, il vous suffit de vous reconnecter en cliquant sur le bouton ci-dessous. Sans action de votre part, vos données personnelles seront prochainement anonymisées.
</p>
</td>
</tr>
<tr>
<td align="center" style="padding:0 32px 32px 32px;">
<a href="{reconnect_url}" style="display:inline-block; background-color:#2576c6; color:#ffffff; text-decoration:none; font-size:15px; font-weight:bold; padding:12px 28px; border-radius:4px;">Conserver mon compte actif</a>
</td>
</tr>
<tr>
<td style="padding:0 32px 32px 32px;">
<p style="margin:0; font-size:12px; line-height:18px; color:#7a8089;">
Vos commandes et factures sont conservées pendant la durée légale imposée par la loi ; seules les données permettant de vous identifier personnellement sont anonymisées.
</p>
</td>
</tr>
<tr>
<td align="center" style="background-color:#f0f2f5; padding:18px; font-size:12px; color:#7a8089;">
<a href="{shop_url}" style="color:#2576c6; text-decoration:none;">{shop_name}</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
14 changes: 14 additions & 0 deletions mails/fr/retention_warning.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Votre compte va bientôt être anonymisé

Bonjour {firstname} {lastname},

Vous ne vous êtes pas connecté à votre compte sur {shop_name} depuis longtemps. Conformément à la réglementation sur la protection des données (RGPD), nous anonymisons automatiquement les données personnelles des clients inactifs.

Si vous souhaitez conserver votre compte actif, il vous suffit de vous reconnecter ici :
{reconnect_url}

Sans action de votre part, vos données personnelles seront prochainement anonymisées.

Vos commandes et factures sont conservées pendant la durée légale imposée par la loi ; seules les données permettant de vous identifier personnellement sont anonymisées.

{shop_name} - {shop_url}
63 changes: 63 additions & 0 deletions psgdpr.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use PrestaShop\Module\Psgdpr\Entity\PsgdprConsentLang;
use PrestaShop\Module\Psgdpr\Repository\ConsentRepository;
use PrestaShop\Module\Psgdpr\Repository\LoggerRepository;
use PrestaShop\Module\Psgdpr\Service\DataRetention\DataRetentionService;
use PrestaShop\Module\Psgdpr\Service\LoggerService;
use PrestaShop\PrestaShop\Adapter\LegacyLogger;
use PrestaShopBundle\Entity\Lang;
Expand Down Expand Up @@ -59,6 +60,7 @@ class Psgdpr extends Module
'actionAdminControllerSetMedia',
'additionalCustomerFormFields',
'actionCustomerAccountAdd',
'actionCronJob',
];

private $presetMessageAccountCreation = [
Expand Down Expand Up @@ -153,6 +155,12 @@ public function install(): bool
$this->registerHook($this->hooksUsedByModule);
$this->executeQuerySql(self::SQL_QUERY_TYPE_UNINSTALL);
$this->executeQuerySql(self::SQL_QUERY_TYPE_INSTALL);

// Data-retention defaults. Disabled by default: nothing is ever
// anonymized automatically until the merchant/DPO enables it.
Configuration::updateValue(DataRetentionService::CONFIG_ENABLED, 0);
Configuration::updateValue(DataRetentionService::CONFIG_INACTIVITY_DAYS, 1095);
Configuration::updateValue(DataRetentionService::CONFIG_WARN_DAYS, 30);
} catch (PrestaShopException $e) {
/** @var LegacyLogger $legacyLogger */
$legacyLogger = $this->get('prestashop.adapter.legacy.logger');
Expand All @@ -178,6 +186,10 @@ public function uninstall(): bool
Configuration::deleteByName($value);
}

Configuration::deleteByName(DataRetentionService::CONFIG_ENABLED);
Configuration::deleteByName(DataRetentionService::CONFIG_INACTIVITY_DAYS);
Configuration::deleteByName(DataRetentionService::CONFIG_WARN_DAYS);

parent::uninstall();
$this->executeQuerySql(self::SQL_QUERY_TYPE_UNINSTALL);
} catch (PrestaShopException $e) {
Expand All @@ -194,6 +206,19 @@ public function uninstall(): bool
return empty($this->_errors);
}

/**
* Triggered by the ps_cronjobs module (or a system cron calling it).
* Enforces the configured data-retention policy. No-op while disabled.
*
* @return void
*/
public function hookActionCronJob(): void
{
/** @var DataRetentionService $retentionService */
$retentionService = $this->get('PrestaShop\Module\Psgdpr\Service\DataRetention\DataRetentionService');
$retentionService->processRetention();
}

/**
* @return array
*/
Expand Down Expand Up @@ -333,6 +358,9 @@ public function getContent()
'currentPage' => $currentPage,
'ps_base_dir' => Tools::getHttpHost(true),
'ps_version' => _PS_VERSION_,
'retention_enabled' => (bool) Configuration::get(DataRetentionService::CONFIG_ENABLED),
'retention_inactivity_days' => (int) Configuration::get(DataRetentionService::CONFIG_INACTIVITY_DAYS),
'retention_warn_days' => (int) Configuration::get(DataRetentionService::CONFIG_WARN_DAYS),
]);

$this->output .= $this->context->smarty->fetch($this->local_path . 'views/templates/admin/menu.tpl');
Expand Down Expand Up @@ -404,6 +432,41 @@ public function loadAssets(): void
public function postProcess()
{
$this->submitDataConsent();
$this->submitDataRetention();
}

/**
* Save the data-retention settings from the "Data retention" tab.
*
* @return void
*/
private function submitDataRetention()
{
if (!Tools::isSubmit('submitDataRetention')) {
return;
}

$enabled = (int) (bool) Tools::getValue(DataRetentionService::CONFIG_ENABLED);
$inactivityDays = (int) Tools::getValue(DataRetentionService::CONFIG_INACTIVITY_DAYS);
$warnDays = (int) Tools::getValue(DataRetentionService::CONFIG_WARN_DAYS);

if ($inactivityDays < 1) {
$this->output .= $this->displayError($this->getTranslator()->trans('The inactivity period must be at least 1 day.', [], 'Modules.Psgdpr.Admin'));

return;
}

if ($warnDays < 0 || $warnDays >= $inactivityDays) {
$this->output .= $this->displayError($this->getTranslator()->trans('The warning lead time must be 0 or more, and shorter than the inactivity period.', [], 'Modules.Psgdpr.Admin'));

return;
}

Configuration::updateValue(DataRetentionService::CONFIG_ENABLED, $enabled);
Configuration::updateValue(DataRetentionService::CONFIG_INACTIVITY_DAYS, $inactivityDays);
Configuration::updateValue(DataRetentionService::CONFIG_WARN_DAYS, $warnDays);

$this->output .= $this->displayConfirmation($this->getTranslator()->trans('Saved with success !', [], 'Modules.Psgdpr.Shop'));
}

/**
Expand Down
101 changes: 101 additions & 0 deletions src/Command/DataRetentionCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?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
*/

namespace PrestaShop\Module\Psgdpr\Command;

use PrestaShop\Module\Psgdpr\Service\DataRetention\DataRetentionService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

/**
* Runs the GDPR data-retention job from the CLI:
* php bin/console psgdpr:data-retention:run
*
* The CLI kernel exposes the full container (incl. the command bus used for
* anonymization), so this is the recommended way to schedule the job from a
* system crontab, with no dependency on the ps_cronjobs module.
*/
class DataRetentionCommand extends Command
{
/**
* @var string
*/
protected static $defaultName = 'psgdpr:data-retention:run';

/**
* @var DataRetentionService
*/
private $dataRetentionService;

/**
* @param DataRetentionService $dataRetentionService
*/
public function __construct(DataRetentionService $dataRetentionService)
{
parent::__construct();
$this->dataRetentionService = $dataRetentionService;
}

protected function configure()
{
$this
->setName('psgdpr:data-retention:run')
->setDescription('Warn and anonymize customers inactive beyond the configured retention period.')
->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'Preview only: report how many customers would be warned/anonymized, without sending any email or anonymizing any data.'
);
}

/**
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$dryRun = (bool) $input->getOption('dry-run');

$result = $this->dataRetentionService->processRetention($dryRun);

if ($dryRun) {
$output->writeln(sprintf(
'<comment>[DRY RUN]</comment> Would warn %d customer(s) and anonymize %d customer(s). Nothing was sent or changed.',
$result['warned'],
$result['anonymized']
));

return 0;
}

$output->writeln(sprintf(
'<info>Data retention:</info> %d customer(s) warned, %d customer(s) anonymized.',
$result['warned'],
$result['anonymized']
));

return 0;
}
}
2 changes: 2 additions & 0 deletions src/Entity/PsgdprLog.php
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ private function assertRequestTypeIsValid(int $requestType): void
LoggerService::REQUEST_TYPE_EXPORT_PDF,
LoggerService::REQUEST_TYPE_CONSENT_COLLECTING,
LoggerService::REQUEST_TYPE_DELETE,
LoggerService::REQUEST_TYPE_RETENTION_WARNING,
LoggerService::REQUEST_TYPE_SCHEDULED_ANONYMIZATION,
];

if (!in_array($requestType, $validTypes)) {
Expand Down
Loading