1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage User
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 use Joomla\Utilities\ArrayHelper;
13
14 /**
15 * Authorisation helper class, provides static methods to perform various tasks relevant
16 * to the Joomla user and authorisation classes
17 *
18 * This class has influences and some method logic from the Horde Auth package
19 *
20 * @since 11.1
21 */
22 abstract class JUserHelper
23 {
24 /**
25 * Method to add a user to a group.
26 *
27 * @param integer $userId The id of the user.
28 * @param integer $groupId The id of the group.
29 *
30 * @return boolean True on success
31 *
32 * @since 11.1
33 * @throws RuntimeException
34 */
35 public static function addUserToGroup($userId, $groupId)
36 {
37 // Get the user object.
38 $user = new JUser((int) $userId);
39
40 // Add the user to the group if necessary.
41 if (!in_array($groupId, $user->groups))
42 {
43 // Get the title of the group.
44 $db = JFactory::getDbo();
45 $query = $db->getQuery(true)
46 ->select($db->quoteName('title'))
47 ->from($db->quoteName('#__usergroups'))
48 ->where($db->quoteName('id') . ' = ' . (int) $groupId);
49 $db->setQuery($query);
50 $title = $db->loadResult();
51
52 // If the group does not exist, return an exception.
53 if (!$title)
54 {
55 throw new RuntimeException('Access Usergroup Invalid');
56 }
57
58 // Add the group data to the user object.
59 $user->groups[$title] = $groupId;
60
61 // Store the user object.
62 $user->save();
63 }
64
65 // Set the group data for any preloaded user objects.
66 $temp = JUser::getInstance((int) $userId);
67 $temp->groups = $user->groups;
68
69 if (JFactory::getSession()->getId())
70 {
71 // Set the group data for the user object in the session.
72 $temp = JFactory::getUser();
73
74 if ($temp->id == $userId)
75 {
76 $temp->groups = $user->groups;
77 }
78 }
79
80 return true;
81 }
82
83 /**
84 * Method to get a list of groups a user is in.
85 *
86 * @param integer $userId The id of the user.
87 *
88 * @return array List of groups
89 *
90 * @since 11.1
91 */
92 public static function getUserGroups($userId)
93 {
94 // Get the user object.
95 $user = JUser::getInstance((int) $userId);
96
97 return isset($user->groups) ? $user->groups : array();
98 }
99
100 /**
101 * Method to remove a user from a group.
102 *
103 * @param integer $userId The id of the user.
104 * @param integer $groupId The id of the group.
105 *
106 * @return boolean True on success
107 *
108 * @since 11.1
109 */
110 public static function removeUserFromGroup($userId, $groupId)
111 {
112 // Get the user object.
113 $user = JUser::getInstance((int) $userId);
114
115 // Remove the user from the group if necessary.
116 $key = array_search($groupId, $user->groups);
117
118 if ($key !== false)
119 {
120 // Remove the user from the group.
121 unset($user->groups[$key]);
122
123 // Store the user object.
124 $user->save();
125 }
126
127 // Set the group data for any preloaded user objects.
128 $temp = JFactory::getUser((int) $userId);
129 $temp->groups = $user->groups;
130
131 // Set the group data for the user object in the session.
132 $temp = JFactory::getUser();
133
134 if ($temp->id == $userId)
135 {
136 $temp->groups = $user->groups;
137 }
138
139 return true;
140 }
141
142 /**
143 * Method to set the groups for a user.
144 *
145 * @param integer $userId The id of the user.
146 * @param array $groups An array of group ids to put the user in.
147 *
148 * @return boolean True on success
149 *
150 * @since 11.1
151 */
152 public static function setUserGroups($userId, $groups)
153 {
154 // Get the user object.
155 $user = JUser::getInstance((int) $userId);
156
157 // Set the group ids.
158 $groups = ArrayHelper::toInteger($groups);
159 $user->groups = $groups;
160
161 // Get the titles for the user groups.
162 $db = JFactory::getDbo();
163 $query = $db->getQuery(true)
164 ->select($db->quoteName('id') . ', ' . $db->quoteName('title'))
165 ->from($db->quoteName('#__usergroups'))
166 ->where($db->quoteName('id') . ' = ' . implode(' OR ' . $db->quoteName('id') . ' = ', $user->groups));
167 $db->setQuery($query);
168 $results = $db->loadObjectList();
169
170 // Set the titles for the user groups.
171 for ($i = 0, $n = count($results); $i < $n; $i++)
172 {
173 $user->groups[$results[$i]->id] = $results[$i]->id;
174 }
175
176 // Store the user object.
177 $user->save();
178
179 if (session_id())
180 {
181 // Set the group data for any preloaded user objects.
182 $temp = JFactory::getUser((int) $userId);
183 $temp->groups = $user->groups;
184
185 // Set the group data for the user object in the session.
186 $temp = JFactory::getUser();
187
188 if ($temp->id == $userId)
189 {
190 $temp->groups = $user->groups;
191 }
192 }
193
194 return true;
195 }
196
197 /**
198 * Gets the user profile information
199 *
200 * @param integer $userId The id of the user.
201 *
202 * @return object
203 *
204 * @since 11.1
205 */
206 public static function getProfile($userId = 0)
207 {
208 if ($userId == 0)
209 {
210 $user = JFactory::getUser();
211 $userId = $user->id;
212 }
213
214 // Get the dispatcher and load the user's plugins.
215 $dispatcher = JEventDispatcher::getInstance();
216 JPluginHelper::importPlugin('user');
217
218 $data = new JObject;
219 $data->id = $userId;
220
221 // Trigger the data preparation event.
222 $dispatcher->trigger('onContentPrepareData', array('com_users.profile', &$data));
223
224 return $data;
225 }
226
227 /**
228 * Method to activate a user
229 *
230 * @param string $activation Activation string
231 *
232 * @return boolean True on success
233 *
234 * @since 11.1
235 */
236 public static function activateUser($activation)
237 {
238 $db = JFactory::getDbo();
239
240 // Let's get the id of the user we want to activate
241 $query = $db->getQuery(true)
242 ->select($db->quoteName('id'))
243 ->from($db->quoteName('#__users'))
244 ->where($db->quoteName('activation') . ' = ' . $db->quote($activation))
245 ->where($db->quoteName('block') . ' = 1')
246 ->where($db->quoteName('lastvisitDate') . ' = ' . $db->quote($db->getNullDate()));
247 $db->setQuery($query);
248 $id = (int) $db->loadResult();
249
250 // Is it a valid user to activate?
251 if ($id)
252 {
253 $user = JUser::getInstance((int) $id);
254
255 $user->set('block', '0');
256 $user->set('activation', '');
257
258 // Time to take care of business.... store the user.
259 if (!$user->save())
260 {
261 JLog::add($user->getError(), JLog::WARNING, 'jerror');
262
263 return false;
264 }
265 }
266 else
267 {
268 JLog::add(JText::_('JLIB_USER_ERROR_UNABLE_TO_FIND_USER'), JLog::WARNING, 'jerror');
269
270 return false;
271 }
272
273 return true;
274 }
275
276 /**
277 * Returns userid if a user exists
278 *
279 * @param string $username The username to search on.
280 *
281 * @return integer The user id or 0 if not found.
282 *
283 * @since 11.1
284 */
285 public static function getUserId($username)
286 {
287 // Initialise some variables
288 $db = JFactory::getDbo();
289 $query = $db->getQuery(true)
290 ->select($db->quoteName('id'))
291 ->from($db->quoteName('#__users'))
292 ->where($db->quoteName('username') . ' = ' . $db->quote($username));
293 $db->setQuery($query, 0, 1);
294
295 return $db->loadResult();
296 }
297
298 /**
299 * Hashes a password using the current encryption.
300 *
301 * @param string $password The plaintext password to encrypt.
302 *
303 * @return string The encrypted password.
304 *
305 * @since 3.2.1
306 */
307 public static function hashPassword($password)
308 {
309 // JCrypt::hasStrongPasswordSupport() includes a fallback for us in the worst case
310 JCrypt::hasStrongPasswordSupport();
311
312 return password_hash($password, PASSWORD_DEFAULT);
313 }
314
315 /**
316 * Formats a password using the current encryption. If the user ID is given
317 * and the hash does not fit the current hashing algorithm, it automatically
318 * updates the hash.
319 *
320 * @param string $password The plaintext password to check.
321 * @param string $hash The hash to verify against.
322 * @param integer $user_id ID of the user if the password hash should be updated
323 *
324 * @return boolean True if the password and hash match, false otherwise
325 *
326 * @since 3.2.1
327 */
328 public static function verifyPassword($password, $hash, $user_id = 0)
329 {
330 // If we are using phpass
331 if (strpos($hash, '$P$') === 0)
332 {
333 // Use PHPass's portable hashes with a cost of 10.
334 $phpass = new PasswordHash(10, true);
335
336 $match = $phpass->CheckPassword($password, $hash);
337
338 $rehash = true;
339 }
340 elseif ($hash[0] == '$')
341 {
342 // JCrypt::hasStrongPasswordSupport() includes a fallback for us in the worst case
343 JCrypt::hasStrongPasswordSupport();
344 $match = password_verify($password, $hash);
345
346 // Uncomment this line if we actually move to bcrypt.
347 $rehash = password_needs_rehash($hash, PASSWORD_DEFAULT);
348 }
349 elseif (substr($hash, 0, 8) == '{SHA256}')
350 {
351 // Check the password
352 $parts = explode(':', $hash);
353 $salt = @$parts[1];
354 $testcrypt = static::getCryptedPassword($password, $salt, 'sha256', true);
355
356 $match = JCrypt::timingSafeCompare($hash, $testcrypt);
357
358 $rehash = true;
359 }
360 else
361 {
362 // Check the password
363 $parts = explode(':', $hash);
364 $salt = @$parts[1];
365
366 $rehash = true;
367
368 // Compile the hash to compare
369 // If the salt is empty AND there is a ':' in the original hash, we must append ':' at the end
370 $testcrypt = md5($password . $salt) . ($salt ? ':' . $salt : (strpos($hash, ':') !== false ? ':' : ''));
371
372 $match = JCrypt::timingSafeCompare($hash, $testcrypt);
373 }
374
375 // If we have a match and rehash = true, rehash the password with the current algorithm.
376 if ((int) $user_id > 0 && $match && $rehash)
377 {
378 $user = new JUser($user_id);
379 $user->password = static::hashPassword($password);
380 $user->save();
381 }
382
383 return $match;
384 }
385
386 /**
387 * Formats a password using the old encryption methods.
388 *
389 * @param string $plaintext The plaintext password to encrypt.
390 * @param string $salt The salt to use to encrypt the password. []
391 * If not present, a new salt will be
392 * generated.
393 * @param string $encryption The kind of password encryption to use.
394 * Defaults to md5-hex.
395 * @param boolean $show_encrypt Some password systems prepend the kind of
396 * encryption to the crypted password ({SHA},
397 * etc). Defaults to false.
398 *
399 * @return string The encrypted password.
400 *
401 * @since 11.1
402 * @deprecated 4.0
403 */
404 public static function getCryptedPassword($plaintext, $salt = '', $encryption = 'md5-hex', $show_encrypt = false)
405 {
406 // Get the salt to use.
407 $salt = static::getSalt($encryption, $salt, $plaintext);
408
409 // Encrypt the password.
410 switch ($encryption)
411 {
412 case 'plain':
413 return $plaintext;
414
415 case 'sha':
416 $encrypted = base64_encode(mhash(MHASH_SHA1, $plaintext));
417
418 return ($show_encrypt) ? '{SHA}' . $encrypted : $encrypted;
419
420 case 'crypt':
421 case 'crypt-des':
422 case 'crypt-md5':
423 case 'crypt-blowfish':
424 return ($show_encrypt ? '{crypt}' : '') . crypt($plaintext, $salt);
425
426 case 'md5-base64':
427 $encrypted = base64_encode(mhash(MHASH_MD5, $plaintext));
428
429 return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted;
430
431 case 'ssha':
432 $encrypted = base64_encode(mhash(MHASH_SHA1, $plaintext . $salt) . $salt);
433
434 return ($show_encrypt) ? '{SSHA}' . $encrypted : $encrypted;
435
436 case 'smd5':
437 $encrypted = base64_encode(mhash(MHASH_MD5, $plaintext . $salt) . $salt);
438
439 return ($show_encrypt) ? '{SMD5}' . $encrypted : $encrypted;
440
441 case 'aprmd5':
442 $length = strlen($plaintext);
443 $context = $plaintext . '$apr1$' . $salt;
444 $binary = static::_bin(md5($plaintext . $salt . $plaintext));
445
446 for ($i = $length; $i > 0; $i -= 16)
447 {
448 $context .= substr($binary, 0, ($i > 16 ? 16 : $i));
449 }
450
451 for ($i = $length; $i > 0; $i >>= 1)
452 {
453 $context .= ($i & 1) ? chr(0) : $plaintext[0];
454 }
455
456 $binary = static::_bin(md5($context));
457
458 for ($i = 0; $i < 1000; $i++)
459 {
460 $new = ($i & 1) ? $plaintext : substr($binary, 0, 16);
461
462 if ($i % 3)
463 {
464 $new .= $salt;
465 }
466
467 if ($i % 7)
468 {
469 $new .= $plaintext;
470 }
471
472 $new .= ($i & 1) ? substr($binary, 0, 16) : $plaintext;
473 $binary = static::_bin(md5($new));
474 }
475
476 $p = array();
477
478 for ($i = 0; $i < 5; $i++)
479 {
480 $k = $i + 6;
481 $j = $i + 12;
482
483 if ($j == 16)
484 {
485 $j = 5;
486 }
487
488 $p[] = static::_toAPRMD5((ord($binary[$i]) << 16) | (ord($binary[$k]) << 8) | (ord($binary[$j])), 5);
489 }
490
491 return '$apr1$' . $salt . '$' . implode('', $p) . static::_toAPRMD5(ord($binary[11]), 3);
492
493 case 'sha256':
494 $encrypted = ($salt) ? hash('sha256', $plaintext . $salt) . ':' . $salt : hash('sha256', $plaintext);
495
496 return ($show_encrypt) ? '{SHA256}' . $encrypted : '{SHA256}' . $encrypted;
497
498 case 'md5-hex':
499 default:
500 $encrypted = ($salt) ? md5($plaintext . $salt) : md5($plaintext);
501
502 return ($show_encrypt) ? '{MD5}' . $encrypted : $encrypted;
503 }
504 }
505
506 /**
507 * Returns a salt for the appropriate kind of password encryption using the old encryption methods.
508 * Optionally takes a seed and a plaintext password, to extract the seed
509 * of an existing password, or for encryption types that use the plaintext
510 * in the generation of the salt.
511 *
512 * @param string $encryption The kind of password encryption to use.
513 * Defaults to md5-hex.
514 * @param string $seed The seed to get the salt from (probably a
515 * previously generated password). Defaults to
516 * generating a new seed.
517 * @param string $plaintext The plaintext password that we're generating
518 * a salt for. Defaults to none.
519 *
520 * @return string The generated or extracted salt.
521 *
522 * @since 11.1
523 * @deprecated 4.0
524 */
525 public static function getSalt($encryption = 'md5-hex', $seed = '', $plaintext = '')
526 {
527 // Encrypt the password.
528 switch ($encryption)
529 {
530 case 'crypt':
531 case 'crypt-des':
532 if ($seed)
533 {
534 return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 2);
535 }
536 else
537 {
538 return substr(md5(mt_rand()), 0, 2);
539 }
540 break;
541
542 case 'sha256':
543 if ($seed)
544 {
545 return preg_replace('|^{sha256}|i', '', $seed);
546 }
547 else
548 {
549 return static::genRandomPassword(16);
550 }
551 break;
552
553 case 'crypt-md5':
554 if ($seed)
555 {
556 return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 12);
557 }
558 else
559 {
560 return '$1$' . substr(md5(JCrypt::genRandomBytes()), 0, 8) . '$';
561 }
562 break;
563
564 case 'crypt-blowfish':
565 if ($seed)
566 {
567 return substr(preg_replace('|^{crypt}|i', '', $seed), 0, 30);
568 }
569 else
570 {
571 return '$2y$10$' . substr(md5(JCrypt::genRandomBytes()), 0, 22) . '$';
572 }
573 break;
574
575 case 'ssha':
576 if ($seed)
577 {
578 return substr(preg_replace('|^{SSHA}|', '', $seed), -20);
579 }
580 else
581 {
582 return mhash_keygen_s2k(MHASH_SHA1, $plaintext, substr(pack('h*', md5(JCrypt::genRandomBytes())), 0, 8), 4);
583 }
584 break;
585
586 case 'smd5':
587 if ($seed)
588 {
589 return substr(preg_replace('|^{SMD5}|', '', $seed), -16);
590 }
591 else
592 {
593 return mhash_keygen_s2k(MHASH_MD5, $plaintext, substr(pack('h*', md5(JCrypt::genRandomBytes())), 0, 8), 4);
594 }
595 break;
596
597 case 'aprmd5': /* 64 characters that are valid for APRMD5 passwords. */
598 $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
599
600 if ($seed)
601 {
602 return substr(preg_replace('/^\$apr1\$(.{8}).*/', '\\1', $seed), 0, 8);
603 }
604 else
605 {
606 $salt = '';
607
608 for ($i = 0; $i < 8; $i++)
609 {
610 $salt .= $APRMD5{mt_rand(0, 63)};
611 }
612
613 return $salt;
614 }
615 break;
616
617 default:
618 $salt = '';
619
620 if ($seed)
621 {
622 $salt = $seed;
623 }
624
625 return $salt;
626 break;
627 }
628 }
629
630 /**
631 * Generate a random password
632 *
633 * @param integer $length Length of the password to generate
634 *
635 * @return string Random Password
636 *
637 * @since 11.1
638 */
639 public static function genRandomPassword($length = 8)
640 {
641 $salt = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
642 $base = strlen($salt);
643 $makepass = '';
644
645 /*
646 * Start with a cryptographic strength random string, then convert it to
647 * a string with the numeric base of the salt.
648 * Shift the base conversion on each character so the character
649 * distribution is even, and randomize the start shift so it's not
650 * predictable.
651 */
652 $random = JCrypt::genRandomBytes($length + 1);
653 $shift = ord($random[0]);
654
655 for ($i = 1; $i <= $length; ++$i)
656 {
657 $makepass .= $salt[($shift + ord($random[$i])) % $base];
658 $shift += ord($random[$i]);
659 }
660
661 return $makepass;
662 }
663
664 /**
665 * Converts to allowed 64 characters for APRMD5 passwords.
666 *
667 * @param string $value The value to convert.
668 * @param integer $count The number of characters to convert.
669 *
670 * @return string $value converted to the 64 MD5 characters.
671 *
672 * @since 11.1
673 */
674 protected static function _toAPRMD5($value, $count)
675 {
676 /* 64 characters that are valid for APRMD5 passwords. */
677 $APRMD5 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
678
679 $aprmd5 = '';
680 $count = abs($count);
681
682 while (--$count)
683 {
684 $aprmd5 .= $APRMD5[$value & 0x3f];
685 $value >>= 6;
686 }
687
688 return $aprmd5;
689 }
690
691 /**
692 * Converts hexadecimal string to binary data.
693 *
694 * @param string $hex Hex data.
695 *
696 * @return string Binary data.
697 *
698 * @since 11.1
699 */
700 private static function _bin($hex)
701 {
702 $bin = '';
703 $length = strlen($hex);
704
705 for ($i = 0; $i < $length; $i += 2)
706 {
707 $tmp = sscanf(substr($hex, $i, 2), '%x');
708 $bin .= chr(array_shift($tmp));
709 }
710
711 return $bin;
712 }
713
714 /**
715 * Method to remove a cookie record from the database and the browser
716 *
717 * @param string $userId User ID for this user
718 * @param string $cookieName Series id (cookie name decoded)
719 *
720 * @return boolean True on success
721 *
722 * @since 3.2
723 * @deprecated 4.0 This is handled in the authentication plugin itself. The 'invalid' column in the db should be removed as well
724 */
725 public static function invalidateCookie($userId, $cookieName)
726 {
727 $db = JFactory::getDbo();
728 $query = $db->getQuery(true);
729
730 // Invalidate cookie in the database
731 $query
732 ->update($db->quoteName('#__user_keys'))
733 ->set($db->quoteName('invalid') . ' = 1')
734 ->where($db->quotename('user_id') . ' = ' . $db->quote($userId));
735
736 $db->setQuery($query)->execute();
737
738 // Destroy the cookie in the browser.
739 $app = JFactory::getApplication();
740 $app->input->cookie->set($cookieName, '', 1, $app->get('cookie_path', '/'), $app->get('cookie_domain', ''));
741
742 return true;
743 }
744
745 /**
746 * Clear all expired tokens for all users.
747 *
748 * @return mixed Database query result
749 *
750 * @since 3.2
751 * @deprecated 4.0 This is handled in the authentication plugin itself
752 */
753 public static function clearExpiredTokens()
754 {
755 $now = time();
756
757 $db = JFactory::getDbo();
758 $query = $db->getQuery(true)
759 ->delete('#__user_keys')
760 ->where($db->quoteName('time') . ' < ' . $db->quote($now));
761
762 return $db->setQuery($query)->execute();
763 }
764
765 /**
766 * Method to get the remember me cookie data
767 *
768 * @return mixed An array of information from an authentication cookie or false if there is no cookie
769 *
770 * @since 3.2
771 * @deprecated 4.0 This is handled in the authentication plugin itself
772 */
773 public static function getRememberCookieData()
774 {
775 // Create the cookie name
776 $cookieName = static::getShortHashedUserAgent();
777
778 // Fetch the cookie value
779 $app = JFactory::getApplication();
780 $cookieValue = $app->input->cookie->get($cookieName);
781
782 if (!empty($cookieValue))
783 {
784 return explode('.', $cookieValue);
785 }
786 else
787 {
788 return false;
789 }
790 }
791
792 /**
793 * Method to get a hashed user agent string that does not include browser version.
794 * Used when frequent version changes cause problems.
795 *
796 * @return string A hashed user agent string with version replaced by 'abcd'
797 *
798 * @since 3.2
799 */
800 public static function getShortHashedUserAgent()
801 {
802 $ua = JFactory::getApplication()->client;
803 $uaString = $ua->userAgent;
804 $browserVersion = $ua->browserVersion;
805 $uaShort = str_replace($browserVersion, 'abcd', $uaString);
806
807 return md5(JUri::base() . $uaShort);
808 }
809
810 /**
811 * Check if there is a super user in the user ids.
812 *
813 * @param array $userIds An array of user IDs on which to operate
814 *
815 * @return boolean True on success, false on failure
816 *
817 * @since 3.6.5
818 */
819 public static function checkSuperUserInUsers(array $userIds)
820 {
821 foreach ($userIds as $userId)
822 {
823 foreach (static::getUserGroups($userId) as $userGroupId)
824 {
825 if (JAccess::checkGroup($userGroupId, 'core.admin'))
826 {
827 return true;
828 }
829 }
830 }
831
832 return false;
833 }
834 }
835