moduser.class.php 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. <?php
  2. /*
  3. * This file is part of MODX Revolution.
  4. *
  5. * Copyright (c) MODX, LLC. All Rights Reserved.
  6. *
  7. * For complete copyright and license information, see the COPYRIGHT and LICENSE
  8. * files found in the top-level directory of this distribution.
  9. */
  10. /**
  11. * The core MODX user class.
  12. *
  13. * @property int $id The ID of the User
  14. * @property string $username The username for this User
  15. * @property string $password The encrypted password for this User
  16. * @property string $cachepwd A cached, encrypted password used when resetting the User's password or for confirmation
  17. * @property string $class_key The class key of the user. Used for extending the modUser class.
  18. * @property boolean $active Whether or not this user is active, and thereby able to log in
  19. * @property string $remote_key Used for storing a remote reference key for authentication for a User
  20. * @property json $remote_data Used for storing remote data for authentication for a User
  21. * @property string $hash_class The hashing class used to create this User's password
  22. * @property string $salt A salt that might have been used to create this User's password
  23. * @property int $primary_group The user primary Group
  24. * @property array $session_stale
  25. * @property int $sudo If checked, this user will have full access to all the site and will bypass any Access Permissions checks
  26. * @property int $createdon The user creation date
  27. *
  28. * @property modUserProfile $Profile
  29. * @property modUserGroup $PrimaryGroup
  30. * @property array $CreatedResources
  31. * @property array $EditedResources
  32. * @property array $DeletedResources
  33. * @property array $PublishedResources
  34. * @property array $SentMessages
  35. * @property array $ReceivedMessages
  36. * @property array $UserSettings
  37. * @property array $UserGroupMembers
  38. *
  39. * @see modUserGroupMember
  40. * @see modUserGroupRole
  41. * @see modUserMessage
  42. * @see modUserProfile
  43. * @package modx
  44. */
  45. class modUser extends modPrincipal {
  46. /** @var modX|xPDO $xpdo */
  47. public $xpdo;
  48. /**
  49. * A collection of contexts which the current principal is authenticated in.
  50. * @var array
  51. * @access public
  52. */
  53. public $sessionContexts= array ();
  54. /**
  55. * The modUser password field is hashed automatically, and prevent sudo from being set via mass-assignment
  56. *
  57. * {@inheritdoc}
  58. */
  59. public function set($k, $v= null, $vType= '') {
  60. if (!$this->getOption(xPDO::OPT_SETUP)) {
  61. if ($k == 'sudo') return false;
  62. }
  63. if (in_array($k, array('password', 'cachepwd')) && $this->xpdo->getService('hashing', 'hashing.modHashing')) {
  64. if (!$this->get('salt')) {
  65. $this->set('salt', md5(uniqid(rand(),true)));
  66. }
  67. $vOptions = array('salt' => $this->get('salt'));
  68. $v = $this->xpdo->hashing->getHash('', $this->get('hash_class'))->hash($v, $vOptions);
  69. }
  70. return parent::set($k, $v, $vType);
  71. }
  72. /**
  73. * Set the sudo field explicitly
  74. *
  75. * @param boolean $sudo
  76. * @return bool
  77. */
  78. public function setSudo($sudo) {
  79. $this->_fields['sudo'] = (boolean)$sudo;
  80. $this->setDirty('sudo');
  81. return true;
  82. }
  83. /**
  84. * Overrides xPDOObject::save to fire modX-specific events
  85. *
  86. * {@inheritDoc}
  87. */
  88. public function save($cacheFlag = false) {
  89. $isNew = $this->isNew();
  90. if ($isNew && ($this->get('createdon') < 1)) $this->set('createdon', time());
  91. if ($this->xpdo instanceof modX) {
  92. $this->xpdo->invokeEvent('OnUserBeforeSave',array(
  93. 'mode' => $isNew ? modSystemEvent::MODE_NEW : modSystemEvent::MODE_UPD,
  94. 'user' => &$this,
  95. 'cacheFlag' => $cacheFlag,
  96. ));
  97. }
  98. $saved = parent :: save($cacheFlag);
  99. if ($saved && $this->xpdo instanceof modX) {
  100. $this->xpdo->invokeEvent('OnUserSave',array(
  101. 'mode' => $isNew ? modSystemEvent::MODE_NEW : modSystemEvent::MODE_UPD,
  102. 'user' => &$this,
  103. 'cacheFlag' => $cacheFlag,
  104. ));
  105. }
  106. return $saved;
  107. }
  108. /**
  109. * Overrides xPDOObject::remove to fire modX-specific events
  110. *
  111. * {@inheritDoc}
  112. */
  113. public function remove(array $ancestors = array()) {
  114. if ($this->xpdo instanceof modX) {
  115. $this->xpdo->invokeEvent('OnUserBeforeRemove',array(
  116. 'user' => &$this,
  117. 'ancestors' => $ancestors,
  118. ));
  119. }
  120. $removed = parent :: remove($ancestors);
  121. if ($this->xpdo instanceof modX) {
  122. $this->xpdo->invokeEvent('OnUserRemove',array(
  123. 'user' => &$this,
  124. 'ancestors' => $ancestors,
  125. ));
  126. }
  127. return $removed;
  128. }
  129. /**
  130. * Loads the principal attributes that define a modUser security profile.
  131. *
  132. * {@inheritdoc}
  133. */
  134. public function loadAttributes($target, $context = '', $reload = false) {
  135. $context = !empty($context) ? $context : $this->xpdo->context->get('key');
  136. $id = $this->get('id') ? (string) $this->get('id') : '0';
  137. if ($this->get('id') && !$reload) {
  138. $staleContexts = $this->get('session_stale');
  139. $staleContexts = !empty($staleContexts) ? $staleContexts : array();
  140. $stale = array_search($context, $staleContexts);
  141. if ($stale !== false) {
  142. $reload = true;
  143. $staleContexts = array_diff($staleContexts, array($context));
  144. $this->set('session_stale', $staleContexts);
  145. $this->save();
  146. }
  147. }
  148. if ($this->_attributes === null || $reload) {
  149. $this->_attributes = array();
  150. if (isset($_SESSION["modx.user.{$id}.attributes"])) {
  151. if ($reload) {
  152. unset($_SESSION["modx.user.{$id}.attributes"]);
  153. } else {
  154. $this->_attributes = $_SESSION["modx.user.{$id}.attributes"];
  155. }
  156. }
  157. }
  158. if (!isset($this->_attributes[$context])) {
  159. $this->_attributes[$context] = array();
  160. }
  161. $target = (array) $target;
  162. foreach ($target as $t) {
  163. if (!isset($this->_attributes[$context][$t])) {
  164. $this->_attributes[$context][$t] = $this->xpdo->call(
  165. $t,
  166. 'loadAttributes',
  167. array(&$this->xpdo, $context, $this->get('id'))
  168. );
  169. if (!isset($this->_attributes[$context][$t]) || !is_array($this->_attributes[$context][$t])) {
  170. $this->_attributes[$context][$t] = array();
  171. }
  172. }
  173. }
  174. $_SESSION["modx.user.{$id}.attributes"] = $this->_attributes;
  175. }
  176. /**
  177. * Determines if this user is authenticated in a specific context.
  178. *
  179. * Separate session contexts can allow users to login/out of specific sub-sites
  180. * individually (or in collections).
  181. *
  182. * @access public
  183. * @param string $sessionContext The context to determine if the user is
  184. * authenticated in.
  185. * @return boolean true, if the user is authenticated in the specified
  186. * context, false otherwise.
  187. */
  188. public function isAuthenticated($sessionContext= 'web') {
  189. $isAuthenticated= false;
  190. if (!empty ($sessionContext) && is_string($sessionContext)) {
  191. if ($this->hasSessionContext($sessionContext)) {
  192. $isAuthenticated= true;
  193. }
  194. elseif (isset ($_SESSION[$sessionContext . "Validated"])) {
  195. $isAuthenticated= ($_SESSION[$sessionContext . "Validated"] == 1);
  196. }
  197. }
  198. return $isAuthenticated;
  199. }
  200. /**
  201. * Ends a user session completely, including all contexts.
  202. *
  203. * @access public
  204. */
  205. public function endSession() {
  206. $this->removeLocks();
  207. $_SESSION = array();
  208. if (ini_get("session.use_cookies")) {
  209. $params = session_get_cookie_params();
  210. setcookie(
  211. session_name(),
  212. '',
  213. time() - 42000,
  214. $params["path"],
  215. $params["domain"],
  216. $params["secure"],
  217. $params["httponly"]
  218. );
  219. }
  220. session_destroy();
  221. }
  222. /**
  223. * Determines if the provided password matches the hashed password stored for the user.
  224. *
  225. * @param string $password The password to determine if it matches.
  226. * @param array $options Optional settings for the hashing process.
  227. * @return boolean True if the provided password matches the stored password for the user.
  228. */
  229. public function passwordMatches($password, array $options = array()) {
  230. $match = false;
  231. if ($this->xpdo->getService('hashing', 'hashing.modHashing')) {
  232. $options = array_merge(array('salt' => $this->get('salt')), $options);
  233. $hasher = $this->xpdo->hashing->getHash('', $this->get('hash_class'));
  234. $match = $hasher->verify($password, $this->get('password'), $options);
  235. }
  236. return $match;
  237. }
  238. /**
  239. * Activate a reset user password if the proper activation key is provided.
  240. *
  241. * {@internal This does not mark the user active, but rather moves the cachepwd to the
  242. * password field if the activation key matches.}
  243. *
  244. * @param string $key The activation key provided to the user and stored in the registry for matching.
  245. * @return boolean|integer True if the activation was successful, false if unsuccessful,
  246. * and -1 if there is no activation to perform.
  247. */
  248. public function activatePassword($key) {
  249. $activated = -1;
  250. if ($this->get('cachepwd')) {
  251. if ($this->xpdo->getService('registry', 'registry.modRegistry') && $this->xpdo->registry->getRegister('user', 'registry.modDbRegister')) {
  252. if ($this->xpdo->registry->user->connect()) {
  253. $activated = false;
  254. $this->xpdo->registry->user->subscribe('/pwd/reset/' . md5($this->get('username')));
  255. $msgs = $this->xpdo->registry->user->read(array('poll_limit' => 1));
  256. if (!empty($msgs)) {
  257. if ($key === reset($msgs)) {
  258. $this->_setRaw('password', $this->get('cachepwd'));
  259. $this->_setRaw('cachepwd', '');
  260. $activated = $this->save();
  261. }
  262. }
  263. }
  264. }
  265. if ($activated === false) {
  266. $this->_setRaw('cachepwd', '');
  267. $this->save();
  268. }
  269. }
  270. return $activated;
  271. }
  272. /**
  273. * Change the user password.
  274. *
  275. * @access public
  276. * @param string $newPassword Password to set.
  277. * @param string $oldPassword Current password for validation.
  278. * @param boolean $validateOldPassword Current password validation required flag.
  279. * @return boolean Indicates if password was successfully changed.
  280. * @todo Add support for configurable password encoding.
  281. */
  282. public function changePassword($newPassword, $oldPassword, $validateOldPassword = true) {
  283. $changed= false;
  284. $changePassword = $validateOldPassword ? $this->passwordMatches($oldPassword) : true;
  285. if ($changePassword) {
  286. if (!empty ($newPassword)) {
  287. $this->set('password', $newPassword);
  288. $changed= $this->save();
  289. if ($changed) {
  290. $this->xpdo->invokeEvent('OnUserChangePassword', array (
  291. 'user' => &$this,
  292. 'newpassword' => $newPassword,
  293. 'oldpassword' => $oldPassword,
  294. 'userid' => $this->get('id'),/* deprecated */
  295. 'username' => $this->get('username'),/* deprecated */
  296. 'userpassword' => $newPassword,/* deprecated */
  297. ));
  298. }
  299. }
  300. }
  301. return $changed;
  302. }
  303. /**
  304. * Returns an array of user session context keys.
  305. *
  306. * @access public
  307. * @return array An array of session contexts.
  308. */
  309. public function getSessionContexts() {
  310. if (!is_array($this->sessionContexts) || empty ($this->sessionContexts)) {
  311. $this->sessionContexts= array ();
  312. if (isset ($_SESSION['modx.user.contextTokens'])) {
  313. $this->sessionContexts= $_SESSION['modx.user.contextTokens'];
  314. } else {
  315. $legacyContextTokens= array ();
  316. if (isset ($_SESSION["webValidated"]) && $_SESSION["webValidated"] == 1) {
  317. $legacyContextTokens[]= 'web';
  318. }
  319. if (isset ($_SESSION["mgrValidated"]) && $_SESSION["mgrValidated"] == 1) {
  320. $legacyContextTokens[]= 'mgr';
  321. }
  322. foreach ($legacyContextTokens as $token)
  323. $this->addSessionContext($token);
  324. }
  325. $_SESSION['modx.user.contextTokens']= $this->sessionContexts;
  326. }
  327. return $this->sessionContexts;
  328. }
  329. /**
  330. * Adds a new context to the user session context array.
  331. *
  332. * @access public
  333. * @param string $context The context to add to the user session.
  334. */
  335. public function addSessionContext($context) {
  336. if (!$this->xpdo->startSession()) {
  337. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Attempt to start a session failed", '', __METHOD__, __FILE__, __LINE__);
  338. return;
  339. }
  340. if (!empty($context)) {
  341. $this->getSessionContexts();
  342. session_regenerate_id(true);
  343. $this->getOne('Profile');
  344. if ($this->Profile && $this->Profile instanceof modUserProfile) {
  345. $ua= & $this->Profile;
  346. if ($ua && !isset ($this->sessionContexts[$context]) || $this->sessionContexts[$context] != $this->get('id')) {
  347. $ua->set('failedlogincount', 0);
  348. $ua->set('logincount', $ua->logincount + 1);
  349. $ua->set('lastlogin', $ua->thislogin);
  350. $ua->set('thislogin', time());
  351. $ua->set('sessionid', session_id());
  352. $ua->save();
  353. }
  354. }
  355. $this->sessionContexts[$context]= $this->get('id');
  356. $_SESSION['modx.user.contextTokens']= $this->sessionContexts;
  357. if (!isset($_SESSION["modx.{$context}.user.token"]) || empty($_SESSION["modx.{$context}.user.token"])) {
  358. $_SESSION["modx.{$context}.user.token"]= $this->generateToken($context);
  359. }
  360. } else {
  361. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, "Attempt to login to a context with an empty key", '', __METHOD__, __FILE__, __LINE__);
  362. }
  363. }
  364. /**
  365. * Generate a specific authentication token for this user for accessing the MODX manager
  366. * @param string $salt Ignored
  367. * @return string
  368. */
  369. public function generateToken($salt) {
  370. return uniqid($this->xpdo->site_id . '_' . $this->get('id'), true);
  371. }
  372. /**
  373. * Get the user token for the user
  374. * @param string $ctx
  375. * @return string
  376. */
  377. public function getUserToken($ctx = '') {
  378. if (empty($ctx)) $ctx = $this->xpdo->context->get('key');
  379. return isset($_SESSION['modx.'.$ctx.'.user.token']) ? $_SESSION['modx.'.$ctx.'.user.token'] : '';
  380. }
  381. /**
  382. * Removes a user session context.
  383. *
  384. * @access public
  385. * @param string|array $context The context key or an array of context keys.
  386. */
  387. public function removeSessionContext($context) {
  388. if ($this->getSessionContexts()) {
  389. $contextToken= array ();
  390. if (is_array($context)) {
  391. foreach ($context as $ctx) {
  392. $contextToken[$ctx]= $this->get('id');
  393. $this->removeSessionContextVars($ctx);
  394. }
  395. } else {
  396. $contextToken[$context]= $this->get('id');
  397. $this->removeSessionContextVars($context);
  398. }
  399. $this->sessionContexts= array_diff_assoc($this->sessionContexts, $contextToken);
  400. if (empty($this->sessionContexts)) {
  401. $this->endSession();
  402. } else {
  403. $_SESSION['modx.user.contextTokens']= $this->sessionContexts;
  404. }
  405. }
  406. }
  407. /**
  408. * Removes the session vars associated with a specific context.
  409. *
  410. * @access public
  411. * @param string $context The context key.
  412. */
  413. public function removeSessionContextVars($context) {
  414. if (is_string($context) && !empty ($context)) {
  415. unset($_SESSION["modx.{$context}.user.token"]);
  416. unset($_SESSION["modx.{$context}.user.config"]);
  417. unset($_SESSION["modx.{$context}.session.cookie.lifetime"]);
  418. }
  419. }
  420. /**
  421. * Removes a session cookie for a user.
  422. *
  423. * TODO Implement this.
  424. *
  425. * @access public
  426. * @param string $context The context to remove.
  427. */
  428. public function removeSessionCookie($context) {}
  429. /**
  430. * Checks if the user has a specific session context.
  431. *
  432. * @access public
  433. * @param mixed $context Either a name of a context or array of context
  434. * names to check against.
  435. * @return boolean True if the user has the context(s) specified.
  436. */
  437. public function hasSessionContext($context) {
  438. $hasContext= false;
  439. if ($this->getSessionContexts()) {
  440. $contextTokens= array ();
  441. if (is_array($context)) {
  442. foreach ($context as $ctx) {
  443. $contextTokens[$ctx]= $this->get('id');
  444. }
  445. }
  446. elseif (is_string($context)) {
  447. $contextTokens[$context]= $this->get('id');
  448. }
  449. $hasContext= (count(array_intersect_assoc($contextTokens, $this->sessionContexts)) == count($contextTokens));
  450. }
  451. return $hasContext;
  452. }
  453. /**
  454. * Gets a count of {@link modUserMessage} objects ascribed to the user.
  455. *
  456. * @access public
  457. * @param mixed $read
  458. * @return integer The number of messages.
  459. */
  460. public function countMessages($read = '') {
  461. if ($read == 'read') { $read = 1; } elseif ($read == 'unread') { $read = 0; }
  462. $criteria= array ('recipient' => $this->get('id'));
  463. if ($read) {
  464. $criteria['messageread']= $read;
  465. }
  466. return $this->xpdo->getCount('modUserMessage', $criteria);
  467. }
  468. /**
  469. * Gets all user settings in array format.
  470. *
  471. * @access public
  472. * @return array A key -> value array of settings.
  473. */
  474. public function getSettings() {
  475. $settings = $this->getUserGroupSettings();
  476. $uss = $this->getMany('UserSettings');
  477. /** @var modUserSetting $us */
  478. foreach ($uss as $us) {
  479. $settings[$us->get('key')] = $us->get('value');
  480. }
  481. $this->settings = $settings;
  482. return $settings;
  483. }
  484. /**
  485. * Get all group settings for the user in array format.
  486. *
  487. * Preference is set by group rank + member rank, with primary_group having
  488. * highest priority.
  489. *
  490. * @return array An associative array of group settings.
  491. */
  492. public function getUserGroupSettings() {
  493. $settings = array();
  494. $primary = array();
  495. $query = $this->xpdo->newQuery('modUserGroupSetting');
  496. $query->innerJoin('modUserGroup', 'UserGroup', array('UserGroup.id = modUserGroupSetting.group'));
  497. $query->innerJoin('modUserGroupMember', 'Member', array('Member.member' => $this->get('id'), 'UserGroup.id = Member.user_group'));
  498. $query->sortby('UserGroup.rank', 'DESC');
  499. $query->sortby('Member.rank', 'DESC');
  500. $ugss = $this->xpdo->getCollection('modUserGroupSetting', $query);
  501. /** @var modUserGroupSetting $ugs */
  502. foreach ($ugss as $ugs) {
  503. if ($ugs->get('group') === $this->get('primary_group')) {
  504. $primary[$ugs->get('key')] = $ugs->get('value');
  505. } else {
  506. $settings[$ugs->get('key')] = $ugs->get('value');
  507. }
  508. }
  509. return array_merge($settings, $primary);
  510. }
  511. /**
  512. * Gets all Resource Groups this user is assigned to. This may not work in
  513. * the new model.
  514. *
  515. * @access public
  516. * @param string $ctx The context in which to peruse for Resource Groups
  517. * @return array An array of Resource Group names.
  518. */
  519. public function getResourceGroups($ctx = '') {
  520. if (empty($ctx) && is_object($this->xpdo->context)) $ctx = $this->xpdo->context->get('key');
  521. $resourceGroups= array ();
  522. $id = $this->get('id') ? (string) $this->get('id') : '0';
  523. if (isset($_SESSION["modx.user.{$id}.resourceGroups"][$ctx])) {
  524. $resourceGroups= $_SESSION["modx.user.{$id}.resourceGroups"][$ctx];
  525. } else {
  526. $this->loadAttributes('modAccessResourceGroup',$ctx,true);
  527. if (isset($_SESSION["modx.user.{$id}.resourceGroups"][$ctx])) {
  528. $resourceGroups= $_SESSION["modx.user.{$id}.resourceGroups"][$ctx];
  529. }
  530. }
  531. return $resourceGroups;
  532. }
  533. /**
  534. * Gets all the User Group IDs of the groups this user belongs to.
  535. *
  536. * @access public
  537. * @return array An array of User Group IDs.
  538. */
  539. public function getUserGroups() {
  540. $groups= array();
  541. $id = $this->get('id') ? (string) $this->get('id') : '0';
  542. if (isset($_SESSION["modx.user.{$id}.userGroups"]) && $this->xpdo->user->get('id') == $this->get('id')) {
  543. $groups= $_SESSION["modx.user.{$id}.userGroups"];
  544. } else {
  545. $memberGroups= $this->xpdo->getCollectionGraph('modUserGroup', '{"UserGroupMembers":{}}', array('UserGroupMembers.member' => $this->get('id')));
  546. if ($memberGroups) {
  547. /** @var modUserGroup $group */
  548. foreach ($memberGroups as $group) $groups[]= $group->get('id');
  549. }
  550. $_SESSION["modx.user.{$id}.userGroups"]= $groups;
  551. }
  552. return $groups;
  553. }
  554. /**
  555. * Return the Primary Group of this User
  556. *
  557. * @return modUserGroup|null
  558. */
  559. public function getPrimaryGroup() {
  560. if (!$this->isAuthenticated($this->xpdo->context->get('key'))) {
  561. return null;
  562. }
  563. $userGroup = $this->getOne('PrimaryGroup');
  564. if (!$userGroup) {
  565. $c = $this->xpdo->newQuery('modUserGroup');
  566. $c->innerJoin('modUserGroupMember','UserGroupMembers');
  567. $c->where(array(
  568. 'UserGroupMembers.member' => $this->get('id'),
  569. ));
  570. $c->sortby('UserGroupMembers.rank','ASC');
  571. $userGroup = $this->xpdo->getObject('modUserGroup',$c);
  572. }
  573. return $userGroup;
  574. }
  575. /**
  576. * Gets all the User Group names of the groups this user belongs to.
  577. *
  578. * @access public
  579. * @return array An array of User Group names.
  580. */
  581. public function getUserGroupNames() {
  582. $groupNames= array();
  583. $id = $this->get('id') ? (string) $this->get('id') : '0';
  584. if (isset($_SESSION["modx.user.{$id}.userGroupNames"]) && $this->xpdo->user->get('id') == $this->get('id')) {
  585. $groupNames= $_SESSION["modx.user.{$id}.userGroupNames"];
  586. } else {
  587. $memberGroups= $this->xpdo->getCollectionGraph('modUserGroup', '{"UserGroupMembers":{}}', array('UserGroupMembers.member' => $this->get('id')));
  588. if ($memberGroups) {
  589. /** @var modUserGroup $group */
  590. foreach ($memberGroups as $group) $groupNames[]= $group->get('name');
  591. }
  592. $_SESSION["modx.user.{$id}.userGroupNames"]= $groupNames;
  593. }
  594. return $groupNames;
  595. }
  596. /**
  597. * States whether a user is a member of a group or groups. You may specify
  598. * either a string name of the group, or an array of names.
  599. *
  600. * @access public
  601. * @param string|array $groups Either a string of a group name or an array
  602. * of names.
  603. * @param boolean $matchAll If true, requires the user to be a member of all
  604. * the groups specified. If false, the user can be a member of only one to
  605. * pass. Defaults to false.
  606. * @return boolean True if the user is a member of any of the groups
  607. * specified.
  608. */
  609. public function isMember($groups,$matchAll = false) {
  610. $isMember= false;
  611. $groupNames= $this->getUserGroupNames();
  612. if ($groupNames) {
  613. if (is_array($groups)) {
  614. if ($matchAll) {
  615. $matches= array_diff($groups, $groupNames);
  616. $isMember= empty($matches);
  617. } else {
  618. $matches= array_intersect($groups, $groupNames);
  619. $isMember= !empty($matches);
  620. }
  621. } else {
  622. $isMember= (array_search($groups, $groupNames) !== false);
  623. }
  624. }
  625. return $isMember;
  626. }
  627. /**
  628. * Join a User Group, and optionally assign a Role.
  629. *
  630. * @access public
  631. * @param mixed $groupId Either the name or ID of the User Group to join.
  632. * @param mixed $roleId Optional. Either the name or ID of the Role to
  633. * @param integer $rank Optional.
  634. * assign to for the group.
  635. * @return boolean True if successful.
  636. */
  637. public function joinGroup($groupId,$roleId = null,$rank = null) {
  638. $joined = false;
  639. $groupPk = is_string($groupId) ? array('name' => $groupId) : $groupId;
  640. /** @var modUserGroup $userGroup */
  641. $userGroup = $this->xpdo->getObject('modUserGroup',$groupPk);
  642. if (empty($userGroup)) {
  643. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR,'User Group not found with key: '.$groupId);
  644. return $joined;
  645. }
  646. /** @var modUserGroupRole $role */
  647. if (!empty($roleId)) {
  648. $rolePk = is_string($roleId) ? array('name' => $roleId) : $roleId;
  649. $role = $this->xpdo->getObject('modUserGroupRole',$rolePk);
  650. if (empty($role)) {
  651. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, 'Role not found with key: ' . $roleId);
  652. return $joined;
  653. }
  654. }
  655. /** @var modUserGroupMember $member */
  656. $member = $this->xpdo->getObject('modUserGroupMember',array(
  657. 'member' => $this->get('id'),
  658. 'user_group' => $userGroup->get('id'),
  659. ));
  660. if (empty($member)) {
  661. if ($rank == null) {
  662. $rank = count($this->getMany('UserGroupMembers'));
  663. }
  664. $member = $this->xpdo->newObject('modUserGroupMember');
  665. $member->set('member',$this->get('id'));
  666. $member->set('user_group',$userGroup->get('id'));
  667. $member->set('rank', $rank);
  668. if (!empty($role)) {
  669. $member->set('role',$role->get('id'));
  670. }
  671. $joined = $member->save();
  672. if (!$joined) {
  673. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR,'An unknown error occurred preventing adding the User to the User Group.');
  674. } else {
  675. unset($_SESSION["modx.user.{$this->get('id')}.userGroupNames"],
  676. $_SESSION["modx.user.{$this->get('id')}.userGroups"]);
  677. }
  678. } else {
  679. $joined = true;
  680. }
  681. return $joined;
  682. }
  683. /**
  684. * Removes the User from the specified User Group.
  685. *
  686. * @access public
  687. * @param mixed $groupId Either the name or ID of the User Group to leave.
  688. * @return boolean True if successful.
  689. */
  690. public function leaveGroup($groupId) {
  691. $left = false;
  692. $c = $this->xpdo->newQuery('modUserGroupMember');
  693. $c->innerJoin('modUserGroup','UserGroup');
  694. $c->where(array('member' => $this->get('id')));
  695. $fk = is_string($groupId) ? 'name' : 'id';
  696. $c->where(array(
  697. 'member' => $this->get('id'),
  698. 'UserGroup.'.$fk => $groupId,
  699. ));
  700. /** @var modUserGroupMember $member */
  701. $member = $this->xpdo->getObject('modUserGroupMember',$c);
  702. if (empty($member)) {
  703. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR,'User could not leave group with key "'.$groupId.'" because the User was not a part of that group.');
  704. } else {
  705. $left = $member->remove();
  706. if (!$left) {
  707. $this->xpdo->log(xPDO::LOG_LEVEL_ERROR,'An unknown error occurred preventing removing the User from the User Group.');
  708. } else {
  709. unset($_SESSION["modx.user.{$this->get('id')}.userGroupNames"],
  710. $_SESSION["modx.user.{$this->get('id')}.userGroups"]);
  711. }
  712. }
  713. return $left;
  714. }
  715. /**
  716. * Remove any locks held by the user.
  717. *
  718. * @param array $options An array of options for controlling removal of specific locks or lock
  719. * types.
  720. * @return boolean True if the process was successful, or false if an error was encountered.
  721. */
  722. public function removeLocks(array $options = array()) {
  723. $removed = false;
  724. if ($this->xpdo instanceof modX) {
  725. if ($this->xpdo->getService('registry', 'registry.modRegistry')) {
  726. $this->xpdo->registry->addRegister('locks', 'registry.modDbRegister', array('directory' => 'locks'));
  727. $this->xpdo->registry->locks->connect();
  728. $this->xpdo->registry->locks->subscribe('/resource/');
  729. if ($msgs = $this->xpdo->registry->locks->read(array('remove_read' => false, 'poll_limit' => 1))) {
  730. foreach ($msgs as $resource => $user) {
  731. if ($user == $this->get('id')) {
  732. $this->xpdo->registry->locks->subscribe('/resource/' . md5($resource));
  733. $this->xpdo->registry->locks->read(array('remove_read' => true, 'poll_limit' => 1));
  734. }
  735. }
  736. }
  737. $removed = true;
  738. }
  739. }
  740. return $removed;
  741. }
  742. /**
  743. * Returns a randomly generated password
  744. *
  745. * @param integer $length The length of the password
  746. * @param array $options
  747. * @return string The newly generated password
  748. */
  749. public function generatePassword($length = null,array $options = array()) {
  750. if ($length === null) {
  751. $length = $this->xpdo->getOption('password_generated_length', null, 10, true);
  752. }
  753. $passwordMinimumLength = $this->xpdo->getOption('password_min_length', null, 8, true);
  754. if ($length < $passwordMinimumLength) {
  755. $length = $passwordMinimumLength;
  756. }
  757. $options = array_merge(array(
  758. 'allowable_characters' => 'abcdefghjkmnpqrstuvxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789',
  759. 'srand_seed_multiplier' => 1000000,
  760. ),$options);
  761. $ps_len = strlen($options['allowable_characters']);
  762. srand((double) microtime() * $options['srand_seed_multiplier']);
  763. $pass = '';
  764. for ($i = 0; $i < $length; $i++) {
  765. $pass .= $options['allowable_characters'][mt_rand(0, $ps_len -1)];
  766. }
  767. return $pass;
  768. }
  769. /**
  770. * Send an email to the user
  771. *
  772. * @param string $message The body of the email
  773. * @param array $options An array of options
  774. * @return boolean True if successful
  775. */
  776. public function sendEmail($message,array $options = array()) {
  777. if (!($this->xpdo instanceof modX)) return false;
  778. $profile = $this->getOne('Profile');
  779. if (empty($profile)) return false;
  780. $this->xpdo->getService('mail', 'mail.modPHPMailer');
  781. if (!$this->xpdo->mail) return false;
  782. $this->xpdo->mail->set(modMail::MAIL_BODY, $message);
  783. $this->xpdo->mail->set(modMail::MAIL_FROM, $this->xpdo->getOption('from',$options,$this->xpdo->getOption('emailsender')));
  784. $this->xpdo->mail->set(modMail::MAIL_FROM_NAME, $this->xpdo->getOption('fromName',$options,$this->xpdo->getOption('site_name')));
  785. $this->xpdo->mail->set(modMail::MAIL_SENDER, $this->xpdo->getOption('sender',$options,$this->xpdo->getOption('emailsender')));
  786. $this->xpdo->mail->set(modMail::MAIL_SUBJECT, $this->xpdo->getOption('subject',$options,$this->xpdo->getOption('emailsubject')));
  787. $this->xpdo->mail->address('to',$profile->get('email'),$profile->get('fullname'));
  788. $this->xpdo->mail->address('reply-to',$this->xpdo->getOption('sender',$options,$this->xpdo->getOption('emailsender')));
  789. $this->xpdo->mail->setHTML($this->xpdo->getOption('html',$options,true));
  790. $sent = $this->xpdo->mail->send();
  791. $this->xpdo->mail->reset();
  792. return $sent;
  793. }
  794. /**
  795. * Get the dashboard for this user
  796. *
  797. * @return modDashboard
  798. */
  799. public function getDashboard() {
  800. $this->xpdo->loadClass('modDashboard');
  801. /** @var modUserGroup $userGroup */
  802. $userGroup = $this->getPrimaryGroup();
  803. if ($userGroup) {
  804. /** @var modDashboard $dashboard */
  805. $dashboard = $userGroup->getOne('Dashboard');
  806. if (empty($dashboard)) {
  807. $dashboard = modDashboard::getDefaultDashboard($this->xpdo);
  808. }
  809. } else {
  810. $dashboard = modDashboard::getDefaultDashboard($this->xpdo);
  811. }
  812. return $dashboard;
  813. }
  814. /**
  815. * Wrapper method to retrieve this user image
  816. *
  817. * @param int $width The desired photo width
  818. * @param int $height The desired photo height (if applicable)
  819. * @param string $default An optional default photo URL
  820. *
  821. * @return string The photo URL
  822. */
  823. public function getPhoto($width = 128, $height = 128, $default = '') {
  824. $img = $default;
  825. if ($this->Profile->photo) {
  826. $img = $this->getProfilePhoto($width, $height);
  827. } elseif ($this->xpdo->getOption('enable_gravatar')) {
  828. $img = $this->getGravatar($width);
  829. }
  830. return $img;
  831. }
  832. /**
  833. * Retrieve the profile photo, if any
  834. *
  835. * @param int $width The desired photo width
  836. * @param int $height The desired photo height
  837. *
  838. * @return string The photo URL
  839. */
  840. public function getProfilePhoto($width = 128, $height = 128) {
  841. if (empty($this->Profile->photo)) {
  842. return '';
  843. }
  844. $this->xpdo->loadClass('sources.modMediaSource');
  845. /** @var modMediaSource $source */
  846. $source = modMediaSource::getDefaultSource($this->xpdo, $this->xpdo->getOption('photo_profile_source'));
  847. $source->initialize();
  848. $path = $source->prepareSrcForThumb($this->Profile->photo);
  849. return $this->xpdo->getOption('connectors_url', null, MODX_CONNECTORS_URL)
  850. . "system/phpthumb.php?" . http_build_query(array("zc" => 1, "h" => $height, "w" => $width, "src" => $path));
  851. }
  852. /**
  853. * Compute the Gravatar photo URL
  854. *
  855. * @param int $size The desired image size
  856. * @param string $default The default Gravatar photo
  857. *
  858. * @return string The Gravatar photo URL
  859. */
  860. public function getGravatar($size = 128, $default = 'mm') {
  861. $gravemail = md5(
  862. strtolower(
  863. trim($this->Profile->email)
  864. )
  865. );
  866. return 'https://www.gravatar.com/avatar/'
  867. . $gravemail . "?s={$size}&d={$default}";
  868. }
  869. }