[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/user/token/ -> token.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  User.token
   6   *
   7   * @copyright   (C) 2020 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\Crypt\Crypt;
  14  use Joomla\CMS\Factory;
  15  use Joomla\CMS\Form\Form;
  16  use Joomla\CMS\Language\Text;
  17  use Joomla\CMS\Plugin\CMSPlugin;
  18  use Joomla\CMS\Plugin\PluginHelper;
  19  use Joomla\Database\ParameterType;
  20  use Joomla\Utilities\ArrayHelper;
  21  
  22  // phpcs:disable PSR1.Files.SideEffects
  23  \defined('_JEXEC') or die;
  24  // phpcs:enable PSR1.Files.SideEffects
  25  
  26  /**
  27   * An example custom terms and conditions plugin.
  28   *
  29   * @since  3.9.0
  30   */
  31  class PlgUserToken extends CMSPlugin
  32  {
  33      /**
  34       * Load the language file on instantiation.
  35       *
  36       * @var    boolean
  37       * @since  4.0.0
  38       */
  39      protected $autoloadLanguage = true;
  40  
  41      /**
  42       * Application object.
  43       *
  44       * @var    \Joomla\CMS\Application\CMSApplication
  45       * @since  4.0.0
  46       */
  47      protected $app;
  48  
  49      /**
  50       * Database object.
  51       *
  52       * @var    \Joomla\Database\DatabaseInterface
  53       * @since  4.0.0
  54       */
  55      protected $db;
  56  
  57      /**
  58       * Joomla XML form contexts where we should inject our token management user interface.
  59       *
  60       * @var     array
  61       * @since   4.0.0
  62       */
  63      private $allowedContexts = [
  64          'com_users.profile',
  65          'com_users.user',
  66      ];
  67  
  68      /**
  69       * The prefix of the user profile keys, without the dot.
  70       *
  71       * @var     string
  72       * @since   4.0.0
  73       */
  74      private $profileKeyPrefix = 'joomlatoken';
  75  
  76      /**
  77       * Token length, in bytes.
  78       *
  79       * @var     integer
  80       * @since   4.0.0
  81       */
  82      private $tokenLength = 32;
  83  
  84      /**
  85       * Inject the Joomla token management panel's data into the User Profile.
  86       *
  87       * This method is called whenever Joomla is preparing the data for an XML form for display.
  88       *
  89       * @param   string  $context  Form context, passed by Joomla
  90       * @param   mixed   $data     Form data
  91       *
  92       * @return  boolean
  93       * @since   4.0.0
  94       */
  95      public function onContentPrepareData(string $context, &$data): bool
  96      {
  97          // Only do something if the api-authentication plugin with the same name is published
  98          if (!PluginHelper::isEnabled('api-authentication', $this->_name)) {
  99              return true;
 100          }
 101  
 102          // Check we are manipulating a valid form.
 103          if (!in_array($context, $this->allowedContexts)) {
 104              return true;
 105          }
 106  
 107          // $data must be an object
 108          if (!is_object($data)) {
 109              return true;
 110          }
 111  
 112          // We expect the numeric user ID in $data->id
 113          if (!isset($data->id)) {
 114              return true;
 115          }
 116  
 117          // Get the user ID
 118          $userId = intval($data->id);
 119  
 120          // Make sure we have a positive integer user ID
 121          if ($userId <= 0) {
 122              return true;
 123          }
 124  
 125          if (!$this->isInAllowedUserGroup($userId)) {
 126              return true;
 127          }
 128  
 129          $data->{$this->profileKeyPrefix} = [];
 130  
 131          // Load the profile data from the database.
 132          try {
 133              $db    = $this->db;
 134              $query = $db->getQuery(true)
 135                  ->select([
 136                          $db->qn('profile_key'),
 137                          $db->qn('profile_value'),
 138                      ])
 139                  ->from($db->qn('#__user_profiles'))
 140                  ->where($db->qn('user_id') . ' = :userId')
 141                  ->where($db->qn('profile_key') . ' LIKE :profileKey')
 142                  ->order($db->qn('ordering'));
 143  
 144              $profileKey = $this->profileKeyPrefix . '.%';
 145              $query->bind(':userId', $userId, ParameterType::INTEGER);
 146              $query->bind(':profileKey', $profileKey, ParameterType::STRING);
 147  
 148              $results = $db->setQuery($query)->loadRowList();
 149  
 150              foreach ($results as $v) {
 151                  $k = str_replace($this->profileKeyPrefix . '.', '', $v[0]);
 152  
 153                  $data->{$this->profileKeyPrefix}[$k] = $v[1];
 154              }
 155          } catch (Exception $e) {
 156              // We suppress any database error. It means we get no token saved by default.
 157          }
 158  
 159          /**
 160           * Modify the data for display in the user profile view page in the frontend.
 161           *
 162           * It's important to note that we deliberately not register HTMLHelper methods to do the
 163           * same (unlike e.g. the actionlogs system plugin) because the names of our fields are too
 164           * generic and we run the risk of creating naming clashes. Instead, we manipulate the data
 165           * directly.
 166           */
 167          if (($context === 'com_users.profile') && ($this->app->input->get('layout') !== 'edit')) {
 168              $pluginData = $data->{$this->profileKeyPrefix} ?? [];
 169              $enabled    = $pluginData['enabled'] ?? false;
 170              $token      = $pluginData['token'] ?? '';
 171  
 172              $pluginData['enabled'] = Text::_('JDISABLED');
 173              $pluginData['token']   = '';
 174  
 175              if ($enabled) {
 176                  $algo                  = $this->getAlgorithmFromFormFile();
 177                  $pluginData['enabled'] = Text::_('JENABLED');
 178                  $pluginData['token']   = $this->getTokenForDisplay($userId, $token, $algo);
 179              }
 180  
 181              $data->{$this->profileKeyPrefix} = $pluginData;
 182          }
 183  
 184          return true;
 185      }
 186  
 187      /**
 188       * Runs whenever Joomla is preparing a form object.
 189       *
 190       * @param   Form   $form  The form to be altered.
 191       * @param   mixed  $data  The associated data for the form.
 192       *
 193       * @return  boolean
 194       *
 195       * @throws  Exception  When $form is not a valid form object
 196       * @since   4.0.0
 197       */
 198      public function onContentPrepareForm(Form $form, $data): bool
 199      {
 200          // Only do something if the api-authentication plugin with the same name is published
 201          if (!PluginHelper::isEnabled('api-authentication', $this->_name)) {
 202              return true;
 203          }
 204  
 205          // Check we are manipulating a valid form.
 206          if (!in_array($form->getName(), $this->allowedContexts)) {
 207              return true;
 208          }
 209  
 210          // If we are on the save command, no data is passed to $data variable, we need to get it directly from request
 211          $jformData = $this->app->input->get('jform', [], 'array');
 212  
 213          if ($jformData && !$data) {
 214              $data = $jformData;
 215          }
 216  
 217          if (is_array($data)) {
 218              $data = (object) $data;
 219          }
 220  
 221          // Check if the user belongs to an allowed user group
 222          $userId = (is_object($data) && isset($data->id)) ? $data->id : 0;
 223  
 224          if (!empty($userId) && !$this->isInAllowedUserGroup($userId)) {
 225              return true;
 226          }
 227  
 228          // Add the registration fields to the form.
 229          Form::addFormPath(__DIR__ . '/forms');
 230          $form->loadFile('token', false);
 231  
 232          // No token: no reset
 233          $userTokenSeed = $this->getTokenSeedForUser($userId);
 234          $currentUser   = Factory::getUser();
 235  
 236          if (empty($userTokenSeed)) {
 237              $form->removeField('notokenforotherpeople', 'joomlatoken');
 238              $form->removeField('reset', 'joomlatoken');
 239              $form->removeField('token', 'joomlatoken');
 240              $form->removeField('enabled', 'joomlatoken');
 241          } else {
 242              $form->removeField('saveme', 'joomlatoken');
 243          }
 244  
 245          if ($userId != $currentUser->id) {
 246              $form->removeField('token', 'joomlatoken');
 247          } else {
 248              $form->removeField('notokenforotherpeople', 'joomlatoken');
 249          }
 250  
 251          if (($userId != $currentUser->id) && empty($userTokenSeed)) {
 252              $form->removeField('saveme', 'joomlatoken');
 253          } else {
 254              $form->removeField('savemeforotherpeople', 'joomlatoken');
 255          }
 256  
 257          // Remove the Reset field when displaying the user profile form
 258          if (($form->getName() === 'com_users.profile') && ($this->app->input->get('layout') !== 'edit')) {
 259              $form->removeField('reset', 'joomlatoken');
 260          }
 261  
 262          return true;
 263      }
 264  
 265      /**
 266       * Save the Joomla token in the user profile field
 267       *
 268       * @param   mixed   $data    The incoming form data
 269       * @param   bool    $isNew   Is this a new user?
 270       * @param   bool    $result  Has Joomla successfully saved the user?
 271       * @param   string  $error   Error string
 272       *
 273       * @return  void
 274       * @since   4.0.0
 275       */
 276      public function onUserAfterSave($data, bool $isNew, bool $result, ?string $error): void
 277      {
 278          if (!is_array($data)) {
 279              return;
 280          }
 281  
 282          $userId = ArrayHelper::getValue($data, 'id', 0, 'int');
 283  
 284          if ($userId <= 0) {
 285              return;
 286          }
 287  
 288          if (!$result) {
 289              return;
 290          }
 291  
 292          $noToken = false;
 293  
 294          // No Joomla token data. Set the $noToken flag which results in a new token being generated.
 295          if (!isset($data[$this->profileKeyPrefix])) {
 296              /**
 297               * Is the user being saved programmatically, without passing the user profile
 298               * information? In this case I do not want to accidentally try to generate a new token!
 299               *
 300               * We determine that by examining whether the Joomla token field exists. If it does but
 301               * it wasn't passed when saving the user I know it's a programmatic user save and I have
 302               * to ignore it.
 303               */
 304              if ($this->hasTokenProfileFields($userId)) {
 305                  return;
 306              }
 307  
 308              $noToken                       = true;
 309              $data[$this->profileKeyPrefix] = [];
 310          }
 311  
 312          if (isset($data[$this->profileKeyPrefix]['reset'])) {
 313              $reset = $data[$this->profileKeyPrefix]['reset'] == 1;
 314              unset($data[$this->profileKeyPrefix]['reset']);
 315  
 316              if ($reset) {
 317                  $noToken = true;
 318              }
 319          }
 320  
 321          // We may have a token already saved. Let's check, shall we?
 322          if (!$noToken) {
 323              $noToken       = true;
 324              $existingToken = $this->getTokenSeedForUser($userId);
 325  
 326              if (!empty($existingToken)) {
 327                  $noToken                                = false;
 328                  $data[$this->profileKeyPrefix]['token'] = $existingToken;
 329              }
 330          }
 331  
 332          // If there is no token or this is a new user generate a new token.
 333          if ($noToken || $isNew) {
 334              if (
 335                  isset($data[$this->profileKeyPrefix]['token'])
 336                  && empty($data[$this->profileKeyPrefix]['token'])
 337              ) {
 338                  unset($data[$this->profileKeyPrefix]['token']);
 339              }
 340  
 341              $default                       = $this->getDefaultProfileFieldValues();
 342              $data[$this->profileKeyPrefix] = array_merge($default, $data[$this->profileKeyPrefix]);
 343          }
 344  
 345          // Remove existing Joomla Token user profile values
 346          $db    = $this->db;
 347          $query = $db->getQuery(true)
 348              ->delete($db->qn('#__user_profiles'))
 349              ->where($db->qn('user_id') . ' = :userId')
 350              ->where($db->qn('profile_key') . ' LIKE :profileKey');
 351  
 352          $profileKey = $this->profileKeyPrefix . '.%';
 353          $query->bind(':userId', $userId, ParameterType::INTEGER);
 354          $query->bind(':profileKey', $profileKey, ParameterType::STRING);
 355  
 356          $db->setQuery($query)->execute();
 357  
 358          // If the user is not in the allowed user group don't save any new token information.
 359          if (!$this->isInAllowedUserGroup($data['id'])) {
 360              return;
 361          }
 362  
 363          // Save the new Joomla Token user profile values
 364          $order = 1;
 365          $query = $db->getQuery(true)
 366              ->insert($db->qn('#__user_profiles'))
 367              ->columns([
 368                      $db->qn('user_id'),
 369                      $db->qn('profile_key'),
 370                      $db->qn('profile_value'),
 371                      $db->qn('ordering'),
 372                  ]);
 373  
 374          foreach ($data[$this->profileKeyPrefix] as $k => $v) {
 375              $query->values($userId . ', '
 376                  . $db->quote($this->profileKeyPrefix . '.' . $k)
 377                  . ', ' . $db->quote($v)
 378                  . ', ' . ($order++));
 379          }
 380  
 381          $db->setQuery($query)->execute();
 382      }
 383  
 384      /**
 385       * Remove the Joomla token when the user account is deleted from the database.
 386       *
 387       * This event is called after the user data is deleted from the database.
 388       *
 389       * @param   array    $user     Holds the user data
 390       * @param   boolean  $success  True if user was successfully stored in the database
 391       * @param   string   $msg      Message
 392       *
 393       * @return  void
 394       *
 395       * @throws  Exception
 396       * @since   4.0.0
 397       */
 398      public function onUserAfterDelete(array $user, bool $success, string $msg): void
 399      {
 400          if (!$success) {
 401              return;
 402          }
 403  
 404          $userId = ArrayHelper::getValue($user, 'id', 0, 'int');
 405  
 406          if ($userId <= 0) {
 407              return;
 408          }
 409  
 410          try {
 411              $db    = $this->db;
 412              $query = $db->getQuery(true)
 413                  ->delete($db->qn('#__user_profiles'))
 414                  ->where($db->qn('user_id') . ' = :userId')
 415                  ->where($db->qn('profile_key') . ' LIKE :profileKey');
 416  
 417              $profileKey = $this->profileKeyPrefix . '.%';
 418              $query->bind(':userId', $userId, ParameterType::INTEGER);
 419              $query->bind(':profileKey', $profileKey, ParameterType::STRING);
 420  
 421              $db->setQuery($query)->execute();
 422          } catch (Exception $e) {
 423              // Do nothing.
 424          }
 425      }
 426  
 427      /**
 428       * Returns an array with the default profile field values.
 429       *
 430       * This is used when saving the form data of a user (new or existing) without a token already
 431       * set.
 432       *
 433       * @return  array
 434       * @since   4.0.0
 435       */
 436      private function getDefaultProfileFieldValues(): array
 437      {
 438          return [
 439              'token'   => base64_encode(Crypt::genRandomBytes($this->tokenLength)),
 440              'enabled' => true,
 441          ];
 442      }
 443  
 444      /**
 445       * Retrieve the token seed string for the given user ID.
 446       *
 447       * @param   int  $userId  The numeric user ID to return the token seed string for.
 448       *
 449       * @return  string|null  Null if there is no token configured or the user doesn't exist.
 450       * @since   4.0.0
 451       */
 452      private function getTokenSeedForUser(int $userId): ?string
 453      {
 454          try {
 455              $db    = $this->db;
 456              $query = $db->getQuery(true)
 457                  ->select($db->qn('profile_value'))
 458                  ->from($db->qn('#__user_profiles'))
 459                  ->where($db->qn('profile_key') . ' = :profileKey')
 460                  ->where($db->qn('user_id') . ' = :userId');
 461  
 462              $profileKey = $this->profileKeyPrefix . '.token';
 463              $query->bind(':profileKey', $profileKey, ParameterType::STRING);
 464              $query->bind(':userId', $userId, ParameterType::INTEGER);
 465  
 466              return $db->setQuery($query)->loadResult();
 467          } catch (Exception $e) {
 468              return null;
 469          }
 470      }
 471  
 472      /**
 473       * Get the configured user groups which are allowed to have access to tokens.
 474       *
 475       * @return  int[]
 476       * @since   4.0.0
 477       */
 478      private function getAllowedUserGroups(): array
 479      {
 480          $userGroups = $this->params->get('allowedUserGroups', [8]);
 481  
 482          if (empty($userGroups)) {
 483              return [];
 484          }
 485  
 486          if (!is_array($userGroups)) {
 487              $userGroups = [$userGroups];
 488          }
 489  
 490          return $userGroups;
 491      }
 492  
 493      /**
 494       * Is the user with the given ID in the allowed User Groups with access to tokens?
 495       *
 496       * @param   int  $userId  The user ID to check
 497       *
 498       * @return  boolean  False when doesn't belong to allowed user groups, user not found, or guest
 499       * @since   4.0.0
 500       */
 501      private function isInAllowedUserGroup($userId)
 502      {
 503          $allowedUserGroups = $this->getAllowedUserGroups();
 504  
 505          $user = Factory::getUser($userId);
 506  
 507          if ($user->id != $userId) {
 508              return false;
 509          }
 510  
 511          if ($user->guest) {
 512              return false;
 513          }
 514  
 515          // No specifically allowed user groups: allow ALL user groups.
 516          if (empty($allowedUserGroups)) {
 517              return true;
 518          }
 519  
 520          $groups       = $user->getAuthorisedGroups();
 521          $intersection = array_intersect($groups, $allowedUserGroups);
 522  
 523          return !empty($intersection);
 524      }
 525  
 526      /**
 527       * Returns the token formatted suitably for the user to copy.
 528       *
 529       * @param   integer  $userId     The user id for token
 530       * @param   string   $tokenSeed  The token seed data stored in the database
 531       * @param   string   $algorithm  The hashing algorithm to use for the token (default: sha256)
 532       *
 533       * @return  string
 534       * @since   4.0.0
 535       */
 536      private function getTokenForDisplay(
 537          int $userId,
 538          string $tokenSeed,
 539          string $algorithm = 'sha256'
 540      ): string {
 541          if (empty($tokenSeed)) {
 542              return '';
 543          }
 544  
 545          try {
 546              $siteSecret = $this->app->get('secret');
 547          } catch (\Exception $e) {
 548              $siteSecret = '';
 549          }
 550  
 551          // NO site secret? You monster!
 552          if (empty($siteSecret)) {
 553              return '';
 554          }
 555  
 556          $rawToken  = base64_decode($tokenSeed);
 557          $tokenHash = hash_hmac($algorithm, $rawToken, $siteSecret);
 558          $message   = base64_encode("$algorithm:$userId:$tokenHash");
 559  
 560          if ($userId !== $this->app->getIdentity()->id) {
 561              $message = '';
 562          }
 563  
 564          return $message;
 565      }
 566  
 567      /**
 568       * Get the token algorithm as defined in the form file
 569       *
 570       * We use a simple RegEx match instead of loading the form for better performance.
 571       *
 572       * @return  string  The configured algorithm, 'sha256' as a fallback if none is found.
 573       */
 574      private function getAlgorithmFromFormFile(): string
 575      {
 576          $algo = 'sha256';
 577  
 578          $file     = __DIR__ . '/forms/token.xml';
 579          $contents = @file_get_contents($file);
 580  
 581          if ($contents === false) {
 582              return $algo;
 583          }
 584  
 585          if (preg_match('/\s*algo=\s*"\s*([a-z0-9]+)\s*"/i', $contents, $matches) !== 1) {
 586              return $algo;
 587          }
 588  
 589          return $matches[1];
 590      }
 591  
 592      /**
 593       * Does the user have the Joomla Token profile fields?
 594       *
 595       * @param   int|null  $userId  The user we're interested in
 596       *
 597       * @return  bool  True if the user has Joomla Token profile fields
 598       */
 599      private function hasTokenProfileFields(?int $userId): bool
 600      {
 601          if (is_null($userId) || ($userId <= 0)) {
 602              return false;
 603          }
 604  
 605          $db = $this->db;
 606          $q  = $db->getQuery(true)
 607              ->select('COUNT(*)')
 608              ->from($db->qn('#__user_profiles'))
 609              ->where($db->qn('user_id') . ' = ' . $userId)
 610              ->where($db->qn('profile_key') . ' = ' . $db->q($this->profileKeyPrefix . '.token'));
 611  
 612          try {
 613              $numRows = $db->setQuery($q)->loadResult() ?? 0;
 614          } catch (Exception $e) {
 615              return false;
 616          }
 617  
 618          return $numRows > 0;
 619      }
 620  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer