[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @package Joomla.Plugin 5 * @subpackage System.privacyconsent 6 * 7 * @copyright (C) 2018 Open Source Matters, Inc. <https://www.joomla.org> 8 * @license GNU General Public License version 2 or later; see LICENSE.txt 9 10 * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace 11 */ 12 13 use Joomla\CMS\Application\ApplicationHelper; 14 use Joomla\CMS\Cache\Cache; 15 use Joomla\CMS\Factory; 16 use Joomla\CMS\Form\Form; 17 use Joomla\CMS\Form\FormHelper; 18 use Joomla\CMS\Language\Associations; 19 use Joomla\CMS\Language\Text; 20 use Joomla\CMS\Mail\Exception\MailDisabledException; 21 use Joomla\CMS\Mail\MailTemplate; 22 use Joomla\CMS\Plugin\CMSPlugin; 23 use Joomla\CMS\Router\Route; 24 use Joomla\CMS\Uri\Uri; 25 use Joomla\CMS\User\UserHelper; 26 use Joomla\Component\Actionlogs\Administrator\Model\ActionlogModel; 27 use Joomla\Component\Messages\Administrator\Model\MessageModel; 28 use Joomla\Database\Exception\ExecutionFailureException; 29 use Joomla\Database\ParameterType; 30 use Joomla\Utilities\ArrayHelper; 31 use PHPMailer\PHPMailer\Exception as phpmailerException; 32 33 // phpcs:disable PSR1.Files.SideEffects 34 \defined('_JEXEC') or die; 35 // phpcs:enable PSR1.Files.SideEffects 36 37 /** 38 * An example custom privacyconsent plugin. 39 * 40 * @since 3.9.0 41 */ 42 class PlgSystemPrivacyconsent extends CMSPlugin 43 { 44 /** 45 * Load the language file on instantiation. 46 * 47 * @var boolean 48 * @since 3.9.0 49 */ 50 protected $autoloadLanguage = true; 51 52 /** 53 * Application object. 54 * 55 * @var \Joomla\CMS\Application\CMSApplication 56 * @since 3.9.0 57 */ 58 protected $app; 59 60 /** 61 * Database object. 62 * 63 * @var \Joomla\Database\DatabaseDriver 64 * @since 3.9.0 65 */ 66 protected $db; 67 68 /** 69 * Adds additional fields to the user editing form 70 * 71 * @param Form $form The form to be altered. 72 * @param mixed $data The associated data for the form. 73 * 74 * @return boolean 75 * 76 * @since 3.9.0 77 */ 78 public function onContentPrepareForm(Form $form, $data) 79 { 80 // Check we are manipulating a valid form - we only display this on user registration form and user profile form. 81 $name = $form->getName(); 82 83 if (!in_array($name, ['com_users.profile', 'com_users.registration'])) { 84 return true; 85 } 86 87 // We only display this if user has not consented before 88 if (is_object($data)) { 89 $userId = $data->id ?? 0; 90 91 if ($userId > 0 && $this->isUserConsented($userId)) { 92 return true; 93 } 94 } 95 96 // Add the privacy policy fields to the form. 97 FormHelper::addFieldPrefix('Joomla\\Plugin\\System\\PrivacyConsent\\Field'); 98 FormHelper::addFormPath(__DIR__ . '/forms'); 99 $form->loadFile('privacyconsent'); 100 101 $privacyType = $this->params->get('privacy_type', 'article'); 102 $privacyId = ($privacyType == 'menu_item') ? $this->getPrivacyItemId() : $this->getPrivacyArticleId(); 103 $privacynote = $this->params->get('privacy_note'); 104 105 // Push the privacy article ID into the privacy field. 106 $form->setFieldAttribute('privacy', $privacyType, $privacyId, 'privacyconsent'); 107 $form->setFieldAttribute('privacy', 'note', $privacynote, 'privacyconsent'); 108 } 109 110 /** 111 * Method is called before user data is stored in the database 112 * 113 * @param array $user Holds the old user data. 114 * @param boolean $isNew True if a new user is stored. 115 * @param array $data Holds the new user data. 116 * 117 * @return boolean 118 * 119 * @since 3.9.0 120 * @throws InvalidArgumentException on missing required data. 121 */ 122 public function onUserBeforeSave($user, $isNew, $data) 123 { 124 // // Only check for front-end user creation/update profile 125 if ($this->app->isClient('administrator')) { 126 return true; 127 } 128 129 $userId = ArrayHelper::getValue($user, 'id', 0, 'int'); 130 131 // User already consented before, no need to check it further 132 if ($userId > 0 && $this->isUserConsented($userId)) { 133 return true; 134 } 135 136 // Check that the privacy is checked if required ie only in registration from frontend. 137 $option = $this->app->input->get('option'); 138 $task = $this->app->input->post->get('task'); 139 $form = $this->app->input->post->get('jform', [], 'array'); 140 141 if ( 142 $option == 'com_users' && in_array($task, array('registration.register', 'profile.save')) 143 && empty($form['privacyconsent']['privacy']) 144 ) { 145 throw new InvalidArgumentException(Text::_('PLG_SYSTEM_PRIVACYCONSENT_FIELD_ERROR')); 146 } 147 148 return true; 149 } 150 151 /** 152 * Saves user privacy confirmation 153 * 154 * @param array $data entered user data 155 * @param boolean $isNew true if this is a new user 156 * @param boolean $result true if saving the user worked 157 * @param string $error error message 158 * 159 * @return void 160 * 161 * @since 3.9.0 162 */ 163 public function onUserAfterSave($data, $isNew, $result, $error): void 164 { 165 // Only create an entry on front-end user creation/update profile 166 if ($this->app->isClient('administrator')) { 167 return; 168 } 169 170 // Get the user's ID 171 $userId = ArrayHelper::getValue($data, 'id', 0, 'int'); 172 173 // If user already consented before, no need to check it further 174 if ($userId > 0 && $this->isUserConsented($userId)) { 175 return; 176 } 177 178 $option = $this->app->input->get('option'); 179 $task = $this->app->input->post->get('task'); 180 $form = $this->app->input->post->get('jform', [], 'array'); 181 182 if ( 183 $option == 'com_users' 184 && in_array($task, ['registration.register', 'profile.save']) 185 && !empty($form['privacyconsent']['privacy']) 186 ) { 187 $userId = ArrayHelper::getValue($data, 'id', 0, 'int'); 188 189 // Get the user's IP address 190 $ip = $this->app->input->server->get('REMOTE_ADDR', '', 'string'); 191 192 // Get the user agent string 193 $userAgent = $this->app->input->server->get('HTTP_USER_AGENT', '', 'string'); 194 195 // Create the user note 196 $userNote = (object) [ 197 'user_id' => $userId, 198 'subject' => 'PLG_SYSTEM_PRIVACYCONSENT_SUBJECT', 199 'body' => Text::sprintf('PLG_SYSTEM_PRIVACYCONSENT_BODY', $ip, $userAgent), 200 'created' => Factory::getDate()->toSql(), 201 ]; 202 203 try { 204 $this->db->insertObject('#__privacy_consents', $userNote); 205 } catch (Exception $e) { 206 // Do nothing if the save fails 207 } 208 209 $userId = ArrayHelper::getValue($data, 'id', 0, 'int'); 210 211 $message = [ 212 'action' => 'consent', 213 'id' => $userId, 214 'title' => $data['name'], 215 'itemlink' => 'index.php?option=com_users&task=user.edit&id=' . $userId, 216 'userid' => $userId, 217 'username' => $data['username'], 218 'accountlink' => 'index.php?option=com_users&task=user.edit&id=' . $userId, 219 ]; 220 221 /** @var ActionlogModel $model */ 222 $model = $this->app->bootComponent('com_actionlogs')->getMVCFactory()->createModel('Actionlog', 'Administrator'); 223 $model->addLog([$message], 'PLG_SYSTEM_PRIVACYCONSENT_CONSENT', 'plg_system_privacyconsent', $userId); 224 } 225 } 226 227 /** 228 * Remove all user privacy consent information for the given user ID 229 * 230 * Method is called after user data is deleted from the database 231 * 232 * @param array $user Holds the user data 233 * @param boolean $success True if user was successfully stored in the database 234 * @param string $msg Message 235 * 236 * @return void 237 * 238 * @since 3.9.0 239 */ 240 public function onUserAfterDelete($user, $success, $msg): void 241 { 242 if (!$success) { 243 return; 244 } 245 246 $userId = ArrayHelper::getValue($user, 'id', 0, 'int'); 247 248 if ($userId) { 249 // Remove user's consent 250 try { 251 $query = $this->db->getQuery(true) 252 ->delete($this->db->quoteName('#__privacy_consents')) 253 ->where($this->db->quoteName('user_id') . ' = :userid') 254 ->bind(':userid', $userId, ParameterType::INTEGER); 255 $this->db->setQuery($query); 256 $this->db->execute(); 257 } catch (Exception $e) { 258 $this->_subject->setError($e->getMessage()); 259 } 260 } 261 } 262 263 /** 264 * If logged in users haven't agreed to privacy consent, redirect them to profile edit page, ask them to agree to 265 * privacy consent before allowing access to any other pages 266 * 267 * @return void 268 * 269 * @since 3.9.0 270 */ 271 public function onAfterRoute() 272 { 273 // Run this in frontend only 274 if ($this->app->isClient('administrator')) { 275 return; 276 } 277 278 $userId = Factory::getUser()->id; 279 280 // Check to see whether user already consented, if not, redirect to user profile page 281 if ($userId > 0) { 282 // If user consented before, no need to check it further 283 if ($this->isUserConsented($userId)) { 284 return; 285 } 286 287 $option = $this->app->input->getCmd('option'); 288 $task = $this->app->input->get('task'); 289 $view = $this->app->input->getString('view', ''); 290 $layout = $this->app->input->getString('layout', ''); 291 $id = $this->app->input->getInt('id'); 292 293 $privacyArticleId = $this->getPrivacyArticleId(); 294 295 /* 296 * If user is already on edit profile screen or view privacy article 297 * or press update/apply button, or logout, do nothing to avoid infinite redirect 298 */ 299 $allowedUserTasks = [ 300 'profile.save', 'profile.apply', 'user.logout', 'user.menulogout', 301 'method', 'methods', 'captive', 'callback' 302 ]; 303 $isAllowedUserTask = in_array($task, $allowedUserTasks) 304 || substr($task, 0, 8) === 'captive.' 305 || substr($task, 0, 8) === 'methods.' 306 || substr($task, 0, 7) === 'method.' 307 || substr($task, 0, 9) === 'callback.'; 308 309 if ( 310 ($option == 'com_users' && $isAllowedUserTask) 311 || ($option == 'com_content' && $view == 'article' && $id == $privacyArticleId) 312 || ($option == 'com_users' && $view == 'profile' && $layout == 'edit') 313 ) { 314 return; 315 } 316 317 // Redirect to com_users profile edit 318 $this->app->enqueueMessage($this->getRedirectMessage(), 'notice'); 319 $link = 'index.php?option=com_users&view=profile&layout=edit'; 320 $this->app->redirect(Route::_($link, false)); 321 } 322 } 323 324 /** 325 * Event to specify whether a privacy policy has been published. 326 * 327 * @param array &$policy The privacy policy status data, passed by reference, with keys "published", "editLink" and "articlePublished". 328 * 329 * @return void 330 * 331 * @since 3.9.0 332 */ 333 public function onPrivacyCheckPrivacyPolicyPublished(&$policy) 334 { 335 // If another plugin has already indicated a policy is published, we won't change anything here 336 if ($policy['published']) { 337 return; 338 } 339 340 $articleId = (int) $this->params->get('privacy_article'); 341 342 if (!$articleId) { 343 return; 344 } 345 346 // Check if the article exists in database and is published 347 $query = $this->db->getQuery(true) 348 ->select($this->db->quoteName(['id', 'state'])) 349 ->from($this->db->quoteName('#__content')) 350 ->where($this->db->quoteName('id') . ' = :id') 351 ->bind(':id', $articleId, ParameterType::INTEGER); 352 $this->db->setQuery($query); 353 354 $article = $this->db->loadObject(); 355 356 // Check if the article exists 357 if (!$article) { 358 return; 359 } 360 361 // Check if the article is published 362 if ($article->state == 1) { 363 $policy['articlePublished'] = true; 364 } 365 366 $policy['published'] = true; 367 $policy['editLink'] = Route::_('index.php?option=com_content&task=article.edit&id=' . $articleId); 368 } 369 370 /** 371 * Returns the configured redirect message and falls back to the default version. 372 * 373 * @return string redirect message 374 * 375 * @since 3.9.0 376 */ 377 private function getRedirectMessage() 378 { 379 $messageOnRedirect = trim($this->params->get('messageOnRedirect', '')); 380 381 if (empty($messageOnRedirect)) { 382 return Text::_('PLG_SYSTEM_PRIVACYCONSENT_REDIRECT_MESSAGE_DEFAULT'); 383 } 384 385 return $messageOnRedirect; 386 } 387 388 /** 389 * Method to check if the given user has consented yet 390 * 391 * @param integer $userId ID of uer to check 392 * 393 * @return boolean 394 * 395 * @since 3.9.0 396 */ 397 private function isUserConsented($userId) 398 { 399 $userId = (int) $userId; 400 $db = $this->db; 401 $query = $db->getQuery(true); 402 403 $query->select('COUNT(*)') 404 ->from($db->quoteName('#__privacy_consents')) 405 ->where($db->quoteName('user_id') . ' = :userid') 406 ->where($db->quoteName('subject') . ' = ' . $db->quote('PLG_SYSTEM_PRIVACYCONSENT_SUBJECT')) 407 ->where($db->quoteName('state') . ' = 1') 408 ->bind(':userid', $userId, ParameterType::INTEGER); 409 $db->setQuery($query); 410 411 return (int) $db->loadResult() > 0; 412 } 413 414 /** 415 * Get privacy article ID. If the site is a multilingual website and there is associated article for the 416 * current language, ID of the associated article will be returned 417 * 418 * @return integer 419 * 420 * @since 3.9.0 421 */ 422 private function getPrivacyArticleId() 423 { 424 $privacyArticleId = $this->params->get('privacy_article'); 425 426 if ($privacyArticleId > 0 && Associations::isEnabled()) { 427 $privacyAssociated = Associations::getAssociations('com_content', '#__content', 'com_content.item', $privacyArticleId); 428 $currentLang = Factory::getLanguage()->getTag(); 429 430 if (isset($privacyAssociated[$currentLang])) { 431 $privacyArticleId = $privacyAssociated[$currentLang]->id; 432 } 433 } 434 435 return $privacyArticleId; 436 } 437 438 /** 439 * Get privacy menu item ID. If the site is a multilingual website and there is associated menu item for the 440 * current language, ID of the associated menu item will be returned. 441 * 442 * @return integer 443 * 444 * @since 4.0.0 445 */ 446 private function getPrivacyItemId() 447 { 448 $itemId = $this->params->get('privacy_menu_item'); 449 450 if ($itemId > 0 && Associations::isEnabled()) { 451 $privacyAssociated = Associations::getAssociations('com_menus', '#__menu', 'com_menus.item', $itemId, 'id', '', ''); 452 $currentLang = Factory::getLanguage()->getTag(); 453 454 if (isset($privacyAssociated[$currentLang])) { 455 $itemId = $privacyAssociated[$currentLang]->id; 456 } 457 } 458 459 return $itemId; 460 } 461 462 /** 463 * The privacy consent expiration check code is triggered after the page has fully rendered. 464 * 465 * @return void 466 * 467 * @since 3.9.0 468 */ 469 public function onAfterRender() 470 { 471 if (!$this->params->get('enabled', 0)) { 472 return; 473 } 474 475 $cacheTimeout = (int) $this->params->get('cachetimeout', 30); 476 $cacheTimeout = 24 * 3600 * $cacheTimeout; 477 478 // Do we need to run? Compare the last run timestamp stored in the plugin's options with the current 479 // timestamp. If the difference is greater than the cache timeout we shall not execute again. 480 $now = time(); 481 $last = (int) $this->params->get('lastrun', 0); 482 483 if ((abs($now - $last) < $cacheTimeout)) { 484 return; 485 } 486 487 // Update last run status 488 $this->params->set('lastrun', $now); 489 490 $paramsJson = $this->params->toString('JSON'); 491 $db = $this->db; 492 $query = $db->getQuery(true) 493 ->update($db->quoteName('#__extensions')) 494 ->set($db->quoteName('params') . ' = :params') 495 ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) 496 ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) 497 ->where($db->quoteName('element') . ' = ' . $db->quote('privacyconsent')) 498 ->bind(':params', $paramsJson); 499 500 try { 501 // Lock the tables to prevent multiple plugin executions causing a race condition 502 $db->lockTable('#__extensions'); 503 } catch (Exception $e) { 504 // If we can't lock the tables it's too risky to continue execution 505 return; 506 } 507 508 try { 509 // Update the plugin parameters 510 $result = $db->setQuery($query)->execute(); 511 $this->clearCacheGroups(['com_plugins'], [0, 1]); 512 } catch (Exception $exc) { 513 // If we failed to execute 514 $db->unlockTables(); 515 $result = false; 516 } 517 518 try { 519 // Unlock the tables after writing 520 $db->unlockTables(); 521 } catch (Exception $e) { 522 // If we can't lock the tables assume we have somehow failed 523 $result = false; 524 } 525 526 // Abort on failure 527 if (!$result) { 528 return; 529 } 530 531 // Delete the expired privacy consents 532 $this->invalidateExpiredConsents(); 533 534 // Remind for privacy consents near to expire 535 $this->remindExpiringConsents(); 536 } 537 538 /** 539 * Method to send the remind for privacy consents renew 540 * 541 * @return integer 542 * 543 * @since 3.9.0 544 */ 545 private function remindExpiringConsents() 546 { 547 // Load the parameters. 548 $expire = (int) $this->params->get('consentexpiration', 365); 549 $remind = (int) $this->params->get('remind', 30); 550 $now = Factory::getDate()->toSql(); 551 $period = '-' . ($expire - $remind); 552 $db = $this->db; 553 $query = $db->getQuery(true); 554 555 $query->select($db->quoteName(['r.id', 'r.user_id', 'u.email'])) 556 ->from($db->quoteName('#__privacy_consents', 'r')) 557 ->join('LEFT', $db->quoteName('#__users', 'u'), $db->quoteName('u.id') . ' = ' . $db->quoteName('r.user_id')) 558 ->where($db->quoteName('subject') . ' = ' . $db->quote('PLG_SYSTEM_PRIVACYCONSENT_SUBJECT')) 559 ->where($db->quoteName('remind') . ' = 0') 560 ->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('created')); 561 562 try { 563 $users = $db->setQuery($query)->loadObjectList(); 564 } catch (ExecutionFailureException $exception) { 565 return false; 566 } 567 568 $app = Factory::getApplication(); 569 $linkMode = $app->get('force_ssl', 0) == 2 ? Route::TLS_FORCE : Route::TLS_IGNORE; 570 571 foreach ($users as $user) { 572 $token = ApplicationHelper::getHash(UserHelper::genRandomPassword()); 573 $hashedToken = UserHelper::hashPassword($token); 574 575 // The mail 576 try { 577 $templateData = [ 578 'sitename' => $app->get('sitename'), 579 'url' => Uri::root(), 580 'tokenurl' => Route::link('site', 'index.php?option=com_privacy&view=remind&remind_token=' . $token, false, $linkMode, true), 581 'formurl' => Route::link('site', 'index.php?option=com_privacy&view=remind', false, $linkMode, true), 582 'token' => $token, 583 ]; 584 585 $mailer = new MailTemplate('plg_system_privacyconsent.request.reminder', $app->getLanguage()->getTag()); 586 $mailer->addTemplateData($templateData); 587 $mailer->addRecipient($user->email); 588 589 $mailResult = $mailer->send(); 590 591 if ($mailResult === false) { 592 return false; 593 } 594 595 $userId = (int) $user->id; 596 597 // Update the privacy_consents item to not send the reminder again 598 $query->clear() 599 ->update($db->quoteName('#__privacy_consents')) 600 ->set($db->quoteName('remind') . ' = 1') 601 ->set($db->quoteName('token') . ' = :token') 602 ->where($db->quoteName('id') . ' = :userid') 603 ->bind(':token', $hashedToken) 604 ->bind(':userid', $userId, ParameterType::INTEGER); 605 $db->setQuery($query); 606 607 try { 608 $db->execute(); 609 } catch (RuntimeException $e) { 610 return false; 611 } 612 } catch (MailDisabledException | phpmailerException $exception) { 613 return false; 614 } 615 } 616 } 617 618 /** 619 * Method to delete the expired privacy consents 620 * 621 * @return boolean 622 * 623 * @since 3.9.0 624 */ 625 private function invalidateExpiredConsents() 626 { 627 // Load the parameters. 628 $expire = (int) $this->params->get('consentexpiration', 365); 629 $now = Factory::getDate()->toSql(); 630 $period = '-' . $expire; 631 $db = $this->db; 632 $query = $db->getQuery(true); 633 634 $query->select($db->quoteName(['id', 'user_id'])) 635 ->from($db->quoteName('#__privacy_consents')) 636 ->where($query->dateAdd($db->quote($now), $period, 'DAY') . ' > ' . $db->quoteName('created')) 637 ->where($db->quoteName('subject') . ' = ' . $db->quote('PLG_SYSTEM_PRIVACYCONSENT_SUBJECT')) 638 ->where($db->quoteName('state') . ' = 1'); 639 640 $db->setQuery($query); 641 642 try { 643 $users = $db->loadObjectList(); 644 } catch (RuntimeException $e) { 645 return false; 646 } 647 648 // Do not process further if no expired consents found 649 if (empty($users)) { 650 return true; 651 } 652 653 // Push a notification to the site's super users 654 /** @var MessageModel $messageModel */ 655 $messageModel = $this->app->bootComponent('com_messages')->getMVCFactory()->createModel('Message', 'Administrator'); 656 657 foreach ($users as $user) { 658 $userId = (int) $user->id; 659 $query = $db->getQuery(true) 660 ->update($db->quoteName('#__privacy_consents')) 661 ->set($db->quoteName('state') . ' = 0') 662 ->where($db->quoteName('id') . ' = :userid') 663 ->bind(':userid', $userId, ParameterType::INTEGER); 664 $db->setQuery($query); 665 666 try { 667 $db->execute(); 668 } catch (RuntimeException $e) { 669 return false; 670 } 671 672 $messageModel->notifySuperUsers( 673 Text::_('PLG_SYSTEM_PRIVACYCONSENT_NOTIFICATION_USER_PRIVACY_EXPIRED_SUBJECT'), 674 Text::sprintf('PLG_SYSTEM_PRIVACYCONSENT_NOTIFICATION_USER_PRIVACY_EXPIRED_MESSAGE', Factory::getUser($user->user_id)->username) 675 ); 676 } 677 678 return true; 679 } 680 /** 681 * Clears cache groups. We use it to clear the plugins cache after we update the last run timestamp. 682 * 683 * @param array $clearGroups The cache groups to clean 684 * @param array $cacheClients The cache clients (site, admin) to clean 685 * 686 * @return void 687 * 688 * @since 3.9.0 689 */ 690 private function clearCacheGroups(array $clearGroups, array $cacheClients = [0, 1]) 691 { 692 foreach ($clearGroups as $group) { 693 foreach ($cacheClients as $client_id) { 694 try { 695 $options = [ 696 'defaultgroup' => $group, 697 'cachebase' => $client_id ? JPATH_ADMINISTRATOR . '/cache' : 698 Factory::getApplication()->get('cache_path', JPATH_SITE . '/cache'), 699 ]; 700 701 $cache = Cache::getInstance('callback', $options); 702 $cache->clean(); 703 } catch (Exception $e) { 704 // Ignore it 705 } 706 } 707 } 708 } 709 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |