PageRenderTime 42ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/apps/user/models/User.php

https://bitbucket.org/jonphipps/elefant-vocabhub
PHP | 486 lines | 186 code | 33 blank | 267 comment | 32 complexity | 52705b5f319d576f64657ba515f2786c MD5 | raw file
  1. <?php
  2. /**
  3. * Elefant CMS - http://www.elefantcms.com/
  4. *
  5. * Copyright (c) 2011 Johnny Broadway
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in
  15. * all copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. * THE SOFTWARE.
  24. */
  25. /**
  26. * This is the default user authentication source for Elefant. Provides the
  27. * basic `User::require_login()` and `User::require_admin()` methods, as
  28. * well as `User::is_valid()` and `User::logout()`. If a user is logged in,
  29. * the first call to any validation method will initialize the `$user`
  30. * property to contain the static User object.
  31. *
  32. * Note that this class extends [[ExtendedModel]], so all of the [[ExtendedModel]]
  33. * and [[Model]] methods are available for querying the user list, and for user
  34. * management, as well.
  35. *
  36. * Fields:
  37. *
  38. * - id
  39. * - email
  40. * - password
  41. * - session_id
  42. * - expires
  43. * - name
  44. * - type
  45. * - signed_up
  46. * - updated
  47. * - userdata
  48. *
  49. * Basic usage of additional methods:
  50. *
  51. * <?php
  52. *
  53. * // Send unauth users to myapp/login view
  54. * if (! User::require_login ()) {
  55. * $page->title = __ ('Members');
  56. * echo $this->run ('user/login');
  57. * return;
  58. * }
  59. *
  60. * // Check if a user is valid at any point
  61. * if (! User::is_valid ()) {
  62. * // Not allowed
  63. * }
  64. *
  65. * // Check the user's type
  66. * if (User::is ('member')) {
  67. * // Access granted
  68. * }
  69. *
  70. * // Get the name value
  71. * $name = User::val ('name');
  72. *
  73. * // Get the actual user object
  74. * info (User::$user);
  75. *
  76. * // Update and save a user's name
  77. * User::val ('name', 'Bob Diggity');
  78. * User::save ();
  79. *
  80. * // Encrypt a password
  81. * $encrypted = User::encrypt_pass ($password);
  82. *
  83. * // Log out and send them home
  84. * User::logout ('/');
  85. *
  86. * ?>
  87. * @property integer id
  88. * @property string email
  89. * @property string password
  90. * @property string session_id
  91. * @property Datetime expires
  92. * @property string name
  93. * @property string type
  94. * @property Datetime signed_up
  95. * @property Datetime updated
  96. * @property string userdata
  97. *
  98. */
  99. class User extends ExtendedModel {
  100. /**
  101. * The database table name.
  102. *
  103. * @var string
  104. */
  105. public $table = '#prefix#user';
  106. /**
  107. * Tell the ExtendedModel which field should contain the extended properties.
  108. */
  109. public $_extended_field = 'userdata';
  110. /**
  111. * This is the static User object for the current user.
  112. *
  113. * @var User|bool
  114. */
  115. public static $user = false;
  116. /**
  117. * Acl object for `require_acl()` method. Get and set via `User::acl()`.
  118. *
  119. * @var Acl|null
  120. */
  121. public static $acl = null;
  122. /**
  123. * Generates a random salt and encrypts a password using MD5.
  124. *
  125. * @param string $plain
  126. *
  127. * @return string
  128. */
  129. public static function encrypt_pass ($plain) {
  130. $base = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  131. $salt = '$2a$07$';
  132. for ($i = 0; $i < 22; $i++) {
  133. $salt .= $base[rand (0, 61)];
  134. }
  135. return crypt ($plain, $salt . '$');
  136. }
  137. /**
  138. * Takes a length and returns a random string of characters of that
  139. * length for use in passwords. String may contain any number, lower
  140. * or uppercase letters, or common symbols.
  141. *
  142. * @param int $length
  143. *
  144. * @return string
  145. */
  146. public static function generate_pass ($length = 8) {
  147. $list = '123467890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_-+=~:;|<>[]{}?"\'';
  148. $pass = '';
  149. while (strlen ($pass) < $length) {
  150. $pass .= substr ($list, mt_rand (0, strlen ($list)), 1);
  151. }
  152. return $pass;
  153. }
  154. /**
  155. * Verifies a username/password combo against the database.
  156. * Username is matched to the email field. If things check out,
  157. * a session_id is generated and initialized in the database
  158. * and for the user. Also creates the global $user object
  159. * as well, since we have the data (no sense requesting it
  160. * twice).
  161. *
  162. * @param string $user
  163. * @param string $pass
  164. *
  165. * @return array|bool
  166. */
  167. public static function verifier ($user, $pass) {
  168. // If it's been called before for this user, return cached result
  169. static $called = array ();
  170. if (isset ($called[$user])) {
  171. return $called[$user];
  172. }
  173. $u = DB::single (
  174. 'select * from `#prefix#user` where email = ?',
  175. $user
  176. );
  177. // Check if they've exceeded their login attempt limit
  178. global $cache, $controller;
  179. $appconf = parse_ini_file ('apps/user/conf/config.php', true);
  180. $attempts = $cache->get ('_user_login_attempts_' . session_id ());
  181. if (! $attempts) {
  182. $attempts = 0;
  183. }
  184. if ($attempts > $appconf['User']['login_attempt_limit']) {
  185. $called[$user] = false;
  186. $controller->redirect ('/user/too-many-attempts');
  187. }
  188. if ($u && crypt ($pass, $u->password) == $u->password) {
  189. $class = get_called_class ();
  190. self::$user = new $class ((array) $u, false);
  191. self::$user->session_id = md5 (uniqid (mt_rand (), 1));
  192. self::$user->expires = gmdate ('Y-m-d H:i:s', time () + 2592000); // 1 month
  193. $try = 0;
  194. while (! self::$user->put ()) {
  195. self::$user->session_id = md5 (uniqid (mt_rand (), 1));
  196. $try++;
  197. if ($try == 5) {
  198. $called[$user] = false;
  199. return false;
  200. }
  201. }
  202. $_SESSION['session_id'] = self::$user->session_id;
  203. // Save the user agent so we can verify it against future sessions,
  204. // and remove the login attempts cache item
  205. $cache->add ('_user_session_agent_' . $_SESSION['session_id'], $_SERVER['HTTP_USER_AGENT'], 0, time () + 2592000);
  206. $cache->delete ('_user_login_attempts_' . session_id ());
  207. $called[$user] = true;
  208. return true;
  209. }
  210. // Increment the number of attempts they've made
  211. $attempts++;
  212. if (! $cache->add ('_user_login_attempts_' . session_id (), $attempts, 0, $appconf['User']['block_attempts_for'])) {
  213. $cache->replace ('_user_login_attempts_' . session_id (), $attempts, 0, $appconf['User']['block_attempts_for']);
  214. }
  215. $called[$user] = false;
  216. return false;
  217. }
  218. /**
  219. * A custom handler for `simple_auth()`. Note: Calls `session_start()`
  220. * for you, and creates the global `$user` object if a session is
  221. * valid, since we have the data already.
  222. *
  223. * @param string $callback
  224. *
  225. * @return bool|mixed
  226. */
  227. public static function method ($callback) {
  228. if (! isset ($_SESSION)) {
  229. $domain = conf ('General', 'session_domain');
  230. if ($domain === 'full') {
  231. $domain = $_SERVER['HTTP_HOST'];
  232. } elseif ($domain === 'top') {
  233. $parts = explode ('.', $_SERVER['HTTP_HOST']);
  234. $tld = array_pop ($parts);
  235. $domain = '.' . array_pop ($parts) . '.' . $tld;
  236. }
  237. @session_set_cookie_params (time () + conf ('General', 'session_duration'), '/', $domain);
  238. @session_start ();
  239. }
  240. if (isset ($_POST['username']) && isset ($_POST['password'])) {
  241. return call_user_func ($callback, $_POST['username'], $_POST['password']);
  242. } elseif (isset ($_SESSION['session_id'])) {
  243. $u = DB::single (
  244. 'select * from `#prefix#user` where session_id = ? and expires > ?',
  245. $_SESSION['session_id'],
  246. gmdate ('Y-m-d H:i:s')
  247. );
  248. if ($u) {
  249. // Verify user agent as a last step (make hijacking harder)
  250. global $cache;
  251. $ua = $cache->get ('_user_session_agent_' . $_SESSION['session_id']);
  252. if ($ua && $ua !== $_SERVER['HTTP_USER_AGENT']) {
  253. return false;
  254. }
  255. $class = get_called_class ();
  256. self::$user = new $class ((array) $u, false);
  257. return true;
  258. }
  259. }
  260. return false;
  261. }
  262. /**
  263. * Simplifies authorization down to:
  264. *
  265. * <?php
  266. *
  267. * if (! User::require_login ()) {
  268. * // unauthorized
  269. * }
  270. *
  271. * ?>
  272. *
  273. * @return bool
  274. */
  275. public static function require_login () {
  276. $class = get_called_class ();
  277. return simple_auth (array ($class, 'verifier'), array ($class, 'method'));
  278. }
  279. /**
  280. * Alias of `require_acl('admin')`. Simplifies authorization
  281. * for general admin access down to:
  282. *
  283. * <?php
  284. *
  285. * if (! User::require_admin ()) {
  286. * // unauthorized
  287. * }
  288. *
  289. * ?>
  290. *
  291. * @return bool
  292. */
  293. public static function require_admin () {
  294. return self::require_acl ('admin');
  295. }
  296. /**
  297. * Determine whether the current user is allowed to access
  298. * a given resource.
  299. *
  300. * @param $resource
  301. *
  302. * @return bool
  303. */
  304. public static function require_acl ($resource) {
  305. if (! User::is_valid ()) {
  306. return false;
  307. }
  308. $acl = self::acl ();
  309. $resources = func_get_args ();
  310. foreach ($resources as $resource) {
  311. if (! $acl->allowed ($resource, self::$user)) {
  312. return false;
  313. }
  314. }
  315. return true;
  316. }
  317. /**
  318. * Check if a user is valid.
  319. *
  320. * @return bool
  321. */
  322. public static function is_valid () {
  323. if (is_object (self::$user) && self::$user->session_id == $_SESSION['session_id']) {
  324. return true;
  325. }
  326. return self::require_login ();
  327. }
  328. /**
  329. * Check if a user is of a certain type.
  330. *
  331. * @param string $type
  332. *
  333. * @return bool
  334. */
  335. public static function is ($type) {
  336. return (self::$user->type === $type);
  337. }
  338. /**
  339. * Fetch or set the currently active user.
  340. *
  341. * @param User $current
  342. *
  343. * @return User
  344. */
  345. public static function current (User $current = null) {
  346. if ($current !== null) {
  347. self::$user = $current;
  348. }
  349. return self::$user;
  350. }
  351. /**
  352. * Alias of `require_acl('content/' . $access)`, prepending the
  353. * `content/` string to the resource name before comparing it.
  354. * Where `User::require_acl('resource')` is good for validating
  355. * access to any resource type, `User::access('member')` is used
  356. * for content access levels.
  357. *
  358. * @param string $access
  359. *
  360. * @return bool
  361. */
  362. public static function access ($access) {
  363. return self::require_acl ('content/' . $access);
  364. }
  365. /**
  366. * Returns the list of access levels for content. This is a list
  367. * of resources that begin with `content/` e.g., `content/private`,
  368. * with keys as the resource and values as a display name for that
  369. * resource:
  370. *
  371. * array (
  372. * 'public' => 'Public',
  373. * 'member' => 'Member',
  374. * 'private' => 'Private'
  375. * )
  376. *
  377. * Note: Public is hard-coded, since there's no need to verify
  378. * access to public resources, but you still need an access level
  379. * to specify it.
  380. *
  381. * @return array
  382. */
  383. public static function access_list () {
  384. $acl = self::acl ();
  385. $resources = $acl->resources ();
  386. $access = array ('public' => __ ('Public'));
  387. foreach ($resources as $key => $value) {
  388. if (strpos ($key, 'content/') === 0) {
  389. $resource = str_replace ('content/', '', $key);
  390. $access[$resource] = __ (ucfirst ($resource));
  391. }
  392. }
  393. return $access;
  394. }
  395. /**
  396. * Get or set the Acl object.
  397. *
  398. * @param Acl|null $acl
  399. *
  400. * @return Acl
  401. */
  402. public static function acl ($acl = null) {
  403. if ($acl !== null) {
  404. self::$acl = $acl;
  405. }
  406. if (self::$acl === null) {
  407. self::$acl = new Acl (conf ('Paths', 'access_control_list'));
  408. }
  409. return self::$acl;
  410. }
  411. /**
  412. * Get or set a specific field's value.
  413. *
  414. * @param string $key
  415. * @param mixed $val
  416. *
  417. * @return array|bool|null
  418. */
  419. public static function val ($key, $val = null) {
  420. if ($val !== null) {
  421. self::$user->{$key} = $val;
  422. }
  423. return self::$user->{$key};
  424. }
  425. /**
  426. * Save the user's data to the database.
  427. *
  428. * @return bool
  429. */
  430. public static function save () {
  431. return self::$user->put ();
  432. }
  433. /**
  434. * Log out and optionally redirect to the specified URL.
  435. *
  436. * @param string|bool $redirect_to
  437. */
  438. public static function logout ($redirect_to = false) {
  439. if (self::$user === false) {
  440. self::require_login ();
  441. }
  442. if (! empty (self::$user->session_id)) {
  443. self::$user->expires = gmdate ('Y-m-d H:i:s', time () - 100000);
  444. self::$user->put ();
  445. }
  446. $_SESSION['session_id'] = null;
  447. if ($redirect_to) {
  448. global $controller;
  449. $controller->redirect ($redirect_to);
  450. }
  451. }
  452. }
  453. ?>