[ 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 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 }
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 |