PageRenderTime 46ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/models/ETMemberModel.class.php

https://github.com/Ramir1/esoTalk
PHP | 598 lines | 272 code | 104 blank | 222 comment | 35 complexity | 553d0b572b408582ab457088db479f0d MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. // Copyright 2011 Toby Zerner, Simon Zerner
  3. // This file is part of esoTalk. Please see the included license file for usage information.
  4. if (!defined("IN_ESOTALK")) exit;
  5. /**
  6. * The member model provides functions for retrieving and managing member data. It also provides methods to
  7. * handle "last action" types.
  8. *
  9. * @package esoTalk
  10. */
  11. class ETMemberModel extends ETModel {
  12. /**
  13. * Reserved user names which cannot be used.
  14. * @var array
  15. */
  16. protected static $reservedNames = array("guest", "member", "members", "moderator", "moderators", "administrator", "administrators", "suspended");
  17. /**
  18. * An array of last action types => their callback functions.
  19. * @var array
  20. **/
  21. protected static $lastActionTypes = array();
  22. /**
  23. * Class constructor; sets up the base model functions to use the member table.
  24. *
  25. * @return void
  26. */
  27. public function __construct()
  28. {
  29. parent::__construct("member");
  30. }
  31. /**
  32. * Create a member.
  33. *
  34. * @param array $values An array of fields and their values to insert.
  35. * @return bool|int The new member ID, or false if there were errors.
  36. */
  37. public function create(&$values)
  38. {
  39. // Validate the username, email, and password.
  40. $this->validate("username", $values["username"], array($this, "validateUsername"));
  41. $this->validate("email", $values["email"], array($this, "validateEmail"));
  42. $this->validate("password", $values["password"], array($this, "validatePassword"));
  43. // Hash the password and set the join time.
  44. $values["password"] = $this->hashPassword($values["password"]);
  45. $values["joinTime"] = time();
  46. // MD5 the "reset password" hash for storage (for extra safety).
  47. $oldHash = isset($values["resetPassword"]) ? $values["resetPassword"] : null;
  48. if (isset($values["resetPassword"])) $values["resetPassword"] = md5($values["resetPassword"]);
  49. // Set default preferences.
  50. if (empty($values["preferences"])) {
  51. $preferences = array("email.privateAdd", "email.post", "starOnReply");
  52. foreach ($preferences as $p) {
  53. $values["preferences"][$p] = C("esoTalk.preferences.".$p);
  54. }
  55. }
  56. $values["preferences"] = serialize($values["preferences"]);
  57. if ($this->errorCount()) return false;
  58. // Delete any members with the same email or username but who haven't confirmed their email address.
  59. ET::SQL()
  60. ->delete()
  61. ->from("member")
  62. ->where("email=:email OR username=:username")
  63. ->bind(":email", $values["email"])
  64. ->bind(":username", $values["username"])
  65. ->where("confirmedEmail", 0)
  66. ->exec();
  67. $memberId = parent::create($values);
  68. $values["memberId"] = $memberId;
  69. // Create "join" activity for this member.
  70. ET::activityModel()->create("join", $values);
  71. // Go through the list of channels and unsubscribe from any ones that have that attribute set.
  72. $channels = ET::channelModel()->getAll();
  73. $inserts = array();
  74. foreach ($channels as $channel) {
  75. if (!empty($channel["attributes"]["defaultUnsubscribed"]))
  76. $inserts[] = array($memberId, $channel["channelId"], 1);
  77. }
  78. if (count($inserts)) {
  79. ET::SQL()
  80. ->insert("member_channel")
  81. ->setMultiple(array("memberId", "channelId", "unsubscribed"), $inserts)
  82. ->exec();
  83. }
  84. // Revert the "reset password" hash to what it was before we MD5'd it.
  85. $values["resetPassword"] = $oldHash;
  86. return $memberId;
  87. }
  88. /**
  89. * Update a member's details.
  90. *
  91. * @param array $values An array of fields to update and their values.
  92. * @param array $wheres An array of WHERE conditions.
  93. * @return bool|ETSQLResult
  94. */
  95. public function update($values, $wheres = array())
  96. {
  97. if (isset($values["username"]))
  98. $this->validate("username", $values["username"], array($this, "validateUsername"));
  99. if (isset($values["email"]))
  100. $this->validate("email", $values["email"], array($this, "validateEmail"));
  101. if (isset($values["password"])) {
  102. $this->validate("password", $values["password"], array($this, "validatePassword"));
  103. $values["password"] = $this->hashPassword($values["password"]);
  104. }
  105. // Serialize preferences.
  106. if (isset($values["preferences"])) $values["preferences"] = serialize($values["preferences"]);
  107. // MD5 the "reset password" hash for storage (for extra safety).
  108. if (isset($values["resetPassword"])) $values["resetPassword"] = md5($values["resetPassword"]);
  109. if ($this->errorCount()) return false;
  110. return parent::update($values, $wheres);
  111. }
  112. /**
  113. * Get standardized member data given an SQL query (which can specify WHERE conditions, for example.)
  114. *
  115. * @param ETSQLQuery $sql The SQL query to use as a basis.
  116. * @return array An array of members and their details.
  117. */
  118. public function getWithSQL($sql)
  119. {
  120. $sql->select("m.*")
  121. ->select("GROUP_CONCAT(g.groupId) AS groups")
  122. ->select("GROUP_CONCAT(g.name) AS groupNames")
  123. ->select("BIT_OR(g.canSuspend) AS canSuspend")
  124. ->from("member m")
  125. ->from("member_group mg", "mg.memberId=m.memberId", "left")
  126. ->from("group g", "g.groupId=mg.groupId", "left")
  127. ->groupBy("m.memberId");
  128. if (ET::$session and ET::$session->user) {
  129. $sql->select("mm.*")
  130. ->from("member_member mm", "mm.memberId2=m.memberId AND mm.memberId1=:userId", "left")
  131. ->bind(":userId", ET::$session->userId);
  132. }
  133. $members = $sql->exec()->allRows();
  134. // Expand the member data.
  135. foreach ($members as &$member) $this->expand($member);
  136. return $members;
  137. }
  138. /**
  139. * Get standardized member data.
  140. *
  141. * @param array $wheres An array of where conditions.
  142. * @return array An array of members and their details.
  143. */
  144. public function get($wheres = array())
  145. {
  146. $sql = ET::SQL();
  147. $sql->where($wheres);
  148. return $this->getWithSQL($sql);
  149. }
  150. /**
  151. * Get member data for the specified post ID.
  152. *
  153. * @param int $memberId The ID of the member.
  154. * @return array An array of the member's details.
  155. */
  156. public function getById($memberId)
  157. {
  158. return reset($this->get(array("m.memberId" => $memberId)));
  159. }
  160. /**
  161. * Get member data for the specified post IDs, in the same order.
  162. *
  163. * @param array $ids The IDs of the members to fetch.
  164. * @return array An array of member details, ordered by the order of the IDs.
  165. */
  166. public function getByIds($ids)
  167. {
  168. $sql = ET::SQL()
  169. ->where("m.memberId IN (:memberIds)")
  170. ->orderBy("FIELD(m.memberId,:memberIdsOrder)")
  171. ->bind(":memberIds", $ids, PDO::PARAM_INT)
  172. ->bind(":memberIdsOrder", $ids, PDO::PARAM_INT);
  173. return $this->getWithSQL($sql);
  174. }
  175. /**
  176. * Expand raw member data into more readable values.
  177. *
  178. * @param array $member The member to expand data for.
  179. * @return void
  180. */
  181. public function expand(&$member)
  182. {
  183. // Make the groups into an array of groupId => names. (Possibly consider using ETGroupModel::getAll()
  184. // instead of featching the groupNames in getWithSQL()?)
  185. $member["groups"] = array_combine(explode(",", $member["groups"]), explode(",", $member["groupNames"]));
  186. // Unserialize the member's preferences.
  187. $member["preferences"] = unserialize($member["preferences"]);
  188. }
  189. /**
  190. * Generate a password hash using phpass.
  191. *
  192. * @param string $password The plain-text password.
  193. * @return string The hashed password.
  194. */
  195. public function hashPassword($password)
  196. {
  197. require_once PATH_LIBRARY."/vendor/phpass/PasswordHash.php";
  198. $hasher = new PasswordHash(8, FALSE);
  199. return $hasher->HashPassword($password);
  200. }
  201. /**
  202. * Check if a plain-text password matches an encrypted password.
  203. *
  204. * @param string $password The plain-text password to check.
  205. * @param string $hash The password hash to check against.
  206. * @return bool Whether or not the password is correct.
  207. */
  208. public function checkPassword($password, $hash)
  209. {
  210. require_once PATH_LIBRARY."/vendor/phpass/PasswordHash.php";
  211. $hasher = new PasswordHash(8, FALSE);
  212. return $hasher->CheckPassword($password, $hash);
  213. }
  214. /**
  215. * Validate a username.
  216. *
  217. * @param string $username The username to validate.
  218. * @param bool $checkForDuplicate Whether or not to check if a member with this username already exists.
  219. * @return null|string An error code, or null if there were no errors.
  220. */
  221. public function validateUsername($username, $checkForDuplicate = true)
  222. {
  223. // Make sure the name isn't a reserved word.
  224. if (in_array(strtolower($username), self::$reservedNames)) return "nameTaken";
  225. // Make sure the username is not too small or large, and only contains word characters.
  226. if (strlen($username) < 3 or strlen($username) > 20 or preg_match("/\W/", $username)) return "invalidUsername";
  227. // Make sure there's no other member with the same username.
  228. if ($checkForDuplicate and ET::SQL()->select("1")->from("member")->where("username=:username")->where("confirmedEmail=1")->bind(":username", $username)->exec()->numRows())
  229. return "nameTaken";
  230. }
  231. /**
  232. * Validate an email.
  233. *
  234. * @param string $email The email to validate.
  235. * @param bool $checkForDuplicate Whether or not to check if a member with this email already exists.
  236. * @return null|string An error code, or null if there were no errors.
  237. */
  238. public function validateEmail($email, $checkForDuplicate = true)
  239. {
  240. // Check it against a regular expression to make sure it's a valid email address.
  241. if (!filter_var($email, FILTER_VALIDATE_EMAIL)) return "invalidEmail";
  242. // Make sure there's no other member with the same email.
  243. if ($checkForDuplicate and ET::SQL()->select("1")->from("member")->where("email=:email")->where("confirmedEmail=1")->bind(":email", $email)->exec()->numRows())
  244. return "emailTaken";
  245. }
  246. /**
  247. * Validate a password.
  248. *
  249. * @param string $password The password to validate.
  250. * @return null|string An error code, or null if there were no errors.
  251. */
  252. public function validatePassword($password)
  253. {
  254. // Make sure the password isn't too short.
  255. if (strlen($password) < C("esoTalk.minPasswordLength")) return "passwordTooShort";
  256. }
  257. /**
  258. * Returns whether or not the current user can rename a member.
  259. *
  260. * @return bool
  261. */
  262. public function canRename($member)
  263. {
  264. // The user must be an administrator.
  265. return ET::$session->isAdmin();
  266. }
  267. /**
  268. * Returns whether or not the current user can delete a member.
  269. *
  270. * @return bool
  271. */
  272. public function canDelete($member)
  273. {
  274. return $this->canChangePermissions($member);
  275. }
  276. /**
  277. * Returns whether or not the current user can change a member's permissions.
  278. *
  279. * @return bool
  280. */
  281. public function canChangePermissions($member)
  282. {
  283. // The user must be an administrator, and the root admin's permissions can't be changed. A user also
  284. // cannot change their own permissions.
  285. return ET::$session->isAdmin() and $member["memberId"] != C("esoTalk.rootAdmin") and $member["memberId"] != ET::$session->userId;
  286. }
  287. /**
  288. * Returns whether or not the current user can suspend/unsuspend a member.
  289. *
  290. * @return bool
  291. */
  292. public function canSuspend($member)
  293. {
  294. // The user must be an administrator, or they must have the "canSuspend" permission and the member's
  295. // account be either "member" or "suspended". A user cannot suspend or unsuspend themselves, and the root
  296. // admin cannot be suspended.
  297. return
  298. (
  299. ET::$session->isAdmin()
  300. or (ET::$session->user["canSuspend"] and ($member["account"] == ACCOUNT_MEMBER or $member["account"] == ACCOUNT_SUSPENDED))
  301. )
  302. and $member["memberId"] != C("esoTalk.rootAdmin") and $member["memberId"] != ET::$session->userId;
  303. }
  304. /**
  305. * Set a member's account and groups.
  306. *
  307. * @param array $member The details of the member to set the account/groups for.
  308. * @param string $account The new account.
  309. * @param array $groups The new group IDs.
  310. * @return bool true on success, false on error.
  311. */
  312. public function setGroups($member, $account, $groups = array())
  313. {
  314. // Make sure the account is valid.
  315. if (!in_array($account, array(ACCOUNT_MEMBER, ACCOUNT_ADMINISTRATOR, ACCOUNT_SUSPENDED, ACCOUNT_PENDING)))
  316. $this->error("account", "invalidAccount");
  317. if ($this->errorCount()) return false;
  318. // Set the member's new account.
  319. $this->updateById($member["memberId"], array("account" => $account));
  320. // Delete all of the member's existing group associations.
  321. ET::SQL()
  322. ->delete()
  323. ->from("member_group")
  324. ->where("memberId", $member["memberId"])
  325. ->exec();
  326. // Insert new member-group associations.
  327. $inserts = array();
  328. foreach ($groups as $id) $inserts[] = array($member["memberId"], $id);
  329. if (count($inserts))
  330. ET::SQL()
  331. ->insert("member_group")
  332. ->setMultiple(array("memberId", "groupId"), $inserts)
  333. ->exec();
  334. // Now we need to create a new activity item, and to do that we need the names of the member's groups.
  335. $groupData = ET::groupModel()->getAll();
  336. $groupNames = array();
  337. foreach ($groups as $id) $groupNames[$id] = $groupData[$id]["name"];
  338. ET::activityModel()->create("groupChange", $member, ET::$session->user, array("account" => $account, "groups" => $groupNames));
  339. return true;
  340. }
  341. /**
  342. * Set a member's preferences.
  343. *
  344. * @param array $member An array of the member's details.
  345. * @param array $preferences A key => value array of preferences to set.
  346. * @return array The member's new preferences array.
  347. */
  348. public function setPreferences($member, $preferences)
  349. {
  350. // Merge the member's old preferences with the new ones, giving preference to the new ones. Geddit?!
  351. $preferences = array_merge((array)$member["preferences"], $preferences);
  352. $this->updateById($member["memberId"], array(
  353. "preferences" => $preferences
  354. ));
  355. return $preferences;
  356. }
  357. /**
  358. * Set a member's status entry for another member (their record in the member_member table.)
  359. *
  360. * @param int $memberId1 The ID of the primary member (usually the currently-logged-in user).
  361. * @param int $memberId2 The ID of the other member to set the status about.
  362. * @param array $data An array of key => value data to save to the database.
  363. * @return void
  364. */
  365. public function setStatus($memberId1, $memberId2, $data)
  366. {
  367. $keys = array(
  368. "memberId1" => $memberId1,
  369. "memberId2" => $memberId2
  370. );
  371. ET::SQL()->insert("member_member")->set($keys + $data)->setOnDuplicateKey($data)->exec();
  372. }
  373. /**
  374. * Delete a member with the specified ID, along with all of their associated records.
  375. *
  376. * @param int $memberId The ID of the member to delete.
  377. * @param bool $deletePosts Whether or not to mark the member's posts as deleted.
  378. * @return void
  379. */
  380. public function deleteById($memberId, $deletePosts = false)
  381. {
  382. // Delete the member's posts if necessary.
  383. if ($deletePosts) {
  384. ET::SQL()
  385. ->update("post")
  386. ->set("deleteMemberId", ET::$session->userId)
  387. ->set("deleteTime", time())
  388. ->where("memberId", $memberId)
  389. ->exec();
  390. }
  391. // Delete member and other records associated with the member.
  392. ET::SQL()
  393. ->delete()
  394. ->from("member")
  395. ->where("memberId", $memberId)
  396. ->exec();
  397. ET::SQL()
  398. ->delete()
  399. ->from("member_channel")
  400. ->where("memberId", $memberId)
  401. ->exec();
  402. ET::SQL()
  403. ->delete()
  404. ->from("member_conversation")
  405. ->where("id", $memberId)
  406. ->where("type", "member")
  407. ->exec();
  408. ET::SQL()
  409. ->delete()
  410. ->from("member_group")
  411. ->where("memberId", $memberId)
  412. ->exec();
  413. }
  414. /**
  415. * Update the user's last action.
  416. *
  417. * @todo Probably move the serialize part into update().
  418. * @param string $type The type of last action.
  419. * @param array $data An array of custom data that can be used by the last action type callback function.
  420. * @return bool|ETSQLResult
  421. */
  422. public function updateLastAction($type, $data = array())
  423. {
  424. if (!ET::$session->user) return false;
  425. $data["type"] = $type;
  426. ET::$session->updateUser("lastActionTime", time());
  427. ET::$session->updateUser("lastActionDetail", $data);
  428. return $this->updateById(ET::$session->userId, array(
  429. "lastActionTime" => time(),
  430. "lastActionDetail" => serialize($data)
  431. ));
  432. }
  433. /**
  434. * Register a type of "last action".
  435. *
  436. * @param string $type The name of the last action type.
  437. * @param mixed The callback function that will be called to format the last action for display. The function
  438. * should return an array:
  439. * 0 => the last action description (eg. Viewing [title])
  440. * 1 => an optional associated URL (eg. a conversation URL)
  441. * @return void
  442. */
  443. public static function addLastActionType($type, $callback)
  444. {
  445. self::$lastActionTypes[$type] = $callback;
  446. }
  447. /**
  448. * Get formatted last action info for a member, given their lastActionTime and lastActionDetail fields.
  449. *
  450. * @todo Probably move the serialize part into expand().
  451. * @param int $time The member's lastActionTime field.
  452. * @param string $action The member's lastActionDetail field.
  453. */
  454. public static function getLastActionInfo($time, $action)
  455. {
  456. // If there is no action, or the time passed since the user was last seen is too great, then return no info.
  457. if (!$action or $time < time() - C("esoTalk.userOnlineExpire"))
  458. return false;
  459. $data = unserialize($action);
  460. if (!isset($data["type"])) return false;
  461. // If there's a callback for this last action type, return its output.
  462. if (isset(self::$lastActionTypes[$data["type"]]))
  463. return call_user_func(self::$lastActionTypes[$data["type"]], $data) + array(null, null);
  464. // Otherwise, return an empty array.
  465. else return array(null, null);
  466. }
  467. /**
  468. * Return a formatted last action array for the "viewingConversation" type.
  469. *
  470. * @param array $data An array of data associated with the last action.
  471. * @return array 0 => last action description, 1 => URL
  472. */
  473. public static function lastActionViewingConversation($data)
  474. {
  475. if (empty($data["conversationId"])) return array(sprintf(T("Viewing %s"), T("a private conversation")));
  476. return array(
  477. sprintf(T("Viewing: %s"), $data["title"]),
  478. URL(conversationURL($data["conversationId"], $data["title"]))
  479. );
  480. }
  481. /**
  482. * Return a formatted last action array for the "startingConversation" type.
  483. *
  484. * @param array $data An array of data associated with the last action.
  485. * @return array
  486. */
  487. public static function lastActionStartingConversation($action)
  488. {
  489. return array(T("Starting a conversation"));
  490. }
  491. }
  492. // Add default last action types.
  493. ETMemberModel::addLastActionType("viewingConversation", array("ETMemberModel", "lastActionViewingConversation"));
  494. ETMemberModel::addLastActionType("startingConversation", array("ETMemberModel", "lastActionStartingConversation"));