* @license GNU General Public License version 2 or later; see LICENSE.txt */ namespace Joomla\Plugin\Task\PrivacyConsent\Extension; use Joomla\CMS\Application\ApplicationHelper; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Mail\Exception\MailDisabledException; use Joomla\CMS\Mail\MailTemplate; use Joomla\CMS\Plugin\CMSPlugin; use Joomla\CMS\Router\Route; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\UserFactoryAwareTrait; use Joomla\CMS\User\UserHelper; use Joomla\Component\Messages\Administrator\Model\MessageModel; use Joomla\Component\Scheduler\Administrator\Event\ExecuteTaskEvent; use Joomla\Component\Scheduler\Administrator\Task\Status; use Joomla\Component\Scheduler\Administrator\Traits\TaskPluginTrait; use Joomla\Database\DatabaseAwareTrait; use Joomla\Database\ParameterType; use Joomla\Event\SubscriberInterface; use PHPMailer\PHPMailer\Exception as phpmailerException; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects /** * A task plugin. Offers 2 task routines Invalidate Expired Consents and Remind Expired Consents * {@see ExecuteTaskEvent}. * * @since 5.0.0 */ final class PrivacyConsent extends CMSPlugin implements SubscriberInterface { use DatabaseAwareTrait; use TaskPluginTrait; use UserFactoryAwareTrait; /** * @var string[] * @since 5.0.0 */ private const TASKS_MAP = [ 'privacy.consent' => [ 'langConstPrefix' => 'PLG_TASK_PRIVACYCONSENT_INVALIDATE', 'method' => 'privacyConsents', 'form' => 'privacyconsentForm', ], ]; /** * @var boolean * * @since 5.0.0 */ protected $autoloadLanguage = true; /** * @inheritDoc * * @return string[] * * @since 5.0.0 */ public static function getSubscribedEvents(): array { return [ 'onTaskOptionsList' => 'advertiseRoutines', 'onExecuteTask' => 'standardRoutineHandler', 'onContentPrepareForm' => 'enhanceTaskItemForm', ]; } /** * Method to send the remind for privacy consents renew. * * @param ExecuteTaskEvent $event The `onExecuteTask` event. * * @return integer The routine exit code. * * @since 5.0.0 * @throws \Exception */ private function privacyConsents(ExecuteTaskEvent $event): int { // Load the parameters. $expire = (int) $event->getArgument('params')->consentexpiration ?? 365; $remind = (int) $event->getArgument('params')->remind ?? 30; if ( $this->invalidateExpiredConsents($expire) === Status::OK && $this->remindExpiringConsents($expire, $remind) === Status::OK ) { return Status::OK; } return Status::KNOCKOUT; } /** * Method to send the remind for privacy consents renew. * * @param integer $expire * @param integer $remind * * @return integer The routine exit code. * * @since 5.0.0 * @throws \Exception */ private function remindExpiringConsents($expire, $remind): int { $now = Factory::getDate()->toSql(); $period = '-' . ($expire - $remind); $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select($db->quoteName(['r.id', 'r.user_id', 'u.email'])) ->from($db->quoteName('#__privacy_consents', 'r')) ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('r.user_id')) ->where($db->quoteName('subject') . ' = ' . $db->quote('PLG_SYSTEM_PRIVACYCONSENT_SUBJECT')) ->where($db->quoteName('remind') . ' = 0') ->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('created')); try { $users = $db->setQuery($query)->loadObjectList(); } catch (\RuntimeException $exception) { return Status::KNOCKOUT; } // Do not process further if no expired consents found if (empty($users)) { return Status::OK; } $app = $this->getApplication(); $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; foreach ($users as $user) { $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); $hashedToken = UserHelper::hashPassword($token); // The mail try { $templateData = [ 'sitename' => $app->get('sitename'), 'url' => Uri::root(), 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=remind&remind_token=' . $token, false, $linkMode, true), 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=remind', false, $linkMode, true), 'token' => $token, ]; $mailer = new MailTemplate('plg_task_privacyconsent.request.reminder', $app->getLanguage()->getTag()); $mailer->addTemplateData($templateData); $mailer->addRecipient($user->email); $mailResult = $mailer->send(); if ($mailResult === false) { return Status::KNOCKOUT; } $userId = (int) $user->id; // Update the privacy_consents item to not send the reminder again $query->clear() ->update($db->quoteName('#__privacy_consents')) ->set($db->quoteName('remind') . ' = 1') ->set($db->quoteName('token') . ' = :token') ->where($db->quoteName('id') . ' = :userid') ->bind(':token', $hashedToken) ->bind(':userid', $userId, ParameterType::INTEGER); $db->setQuery($query); try { $db->execute(); } catch (\RuntimeException $e) { return Status::KNOCKOUT; } } catch (MailDisabledException | phpmailerException $exception) { return Status::KNOCKOUT; } } $this->logTask('Remind end'); return Status::OK; } /** * Method to delete the expired privacy consents. * * @param integer $expire * * @return integer The routine exit code. * * @since 5.0.0 * @throws \Exception */ private function invalidateExpiredConsents($expire): int { $now = Factory::getDate()->toSql(); $period = '-' . $expire; $db = $this->getDatabase(); $query = $db->getQuery(true); $query->select($db->quoteName(['id', 'user_id'])) ->from($db->quoteName('#__privacy_consents')) ->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('created')) ->where($db->quoteName('subject') . ' = ' . $db->quote('PLG_SYSTEM_PRIVACYCONSENT_SUBJECT')) ->where($db->quoteName('state') . ' = 1'); $db->setQuery($query); try { $users = $db->loadObjectList(); } catch (\RuntimeException $e) { return Status::KNOCKOUT; } // Do not process further if no expired consents found if (empty($users)) { return Status::OK; } // Push a notification to the site's super users /** @var MessageModel $messageModel */ $messageModel = $this->getApplication()->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); foreach ($users as $user) { $userId = (int) $user->id; $query = $db->getQuery(true) ->update($db->quoteName('#__privacy_consents')) ->set($db->quoteName('state') . ' = 0') ->where($db->quoteName('id') . ' = :userid') ->bind(':userid', $userId, ParameterType::INTEGER); $db->setQuery($query); try { $db->execute(); } catch (\RuntimeException $e) { return Status::KNOCKOUT; } $messageModel->notifySuperUsers( Text::_('PLG_TASK_PRIVACYCONSENT_NOTIFICATION_USER_PRIVACY_EXPIRED_SUBJECT'), Text::sprintf('PLG_TASK_PRIVACYCONSENT_NOTIFICATION_USER_PRIVACY_EXPIRED_MESSAGE', $this->getUserFactory()->loadUserById($user->user_id)->username) ); } return Status::OK; } }