PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/person.php

https://github.com/palfrey/phplib
PHP | 389 lines | 224 code | 35 blank | 130 comment | 46 complexity | 23cdc555519fc860110edbea64772954 MD5 | raw file
  1. <?php
  2. /*
  3. * person.php:
  4. * An individual user for the purpose of login etc.
  5. *
  6. * Copyright (c) 2005 UK Citizens Online Democracy. All rights reserved.
  7. * Email: chris@mysociety.org; WWW: http://www.mysociety.org/
  8. *
  9. * $Id: person.php,v 1.28 2008-12-01 13:46:50 matthew Exp $
  10. *
  11. */
  12. require_once 'utility.php';
  13. require_once 'stash.php';
  14. require_once 'rabx.php';
  15. require_once 'auth.php';
  16. /* person_cookie_domain
  17. * Return the domain to use for cookies. This is computed from HTTP_HOST
  18. * so we can have multiple domains in one vhost. */
  19. function person_cookie_domain() {
  20. $httphost = $_SERVER['HTTP_HOST'];
  21. # XXX there must be a better way of doing this. (Also, the .livesimply
  22. # entry is for Francis's local test domain pledge.livesimply)
  23. if (preg_match("/[^.]+(\.com|\.cat|\.org|\.net|\.co\.uk|\.org\.uk|\.livesimply)$/", $httphost, $matches)) {
  24. return "." . $matches[0];
  25. } else {
  26. return '.' . OPTION_WEB_DOMAIN;
  27. }
  28. }
  29. /* person_canonicalise_name NAME
  30. * Return NAME with all but alphabetic characters removed; this is used to
  31. * compare names entered by users to see when the record in the person table
  32. * should be updated. */
  33. function person_canonicalise_name($n) {
  34. return preg_replace('/[^A-Za-z-]/', '', strtolower($n));
  35. }
  36. class Person {
  37. /* person ID | EMAIL
  38. * Given a person ID or EMAIL address, return a person object describing
  39. * their account. */
  40. function Person($id) {
  41. if (preg_match('/@/', $id))
  42. $this->id = db_getOne('select id from person where lower(email) = ? for update', strtolower($email));
  43. elseif (preg_match('/^[1-9]\d*$/', $id))
  44. $this->id = db_getOne('select id from person where id = ? for update', $id);
  45. else
  46. err('value passed to person constructor must be person ID or email address');
  47. if (is_null($this->id))
  48. err("No such person '$id'");
  49. list($this->email, $this->name, $this->password, $this->website, $this->numlogins)
  50. = db_getRow_list('select email, name, password, website, numlogins from person where id = ?', $id);
  51. }
  52. /* id [ID]
  53. * Get the person ID. */
  54. function id() {
  55. return $this->id;
  56. }
  57. /* email [EMAIL]
  58. * Get or set the person's EMAIL address. */
  59. function email($email = null) {
  60. if (!is_null($email)) {
  61. db_query('update person set email = ? where id = ?', array($email, $this->id));
  62. $this->email = $email;
  63. }
  64. return $this->email;
  65. }
  66. /* name [NAME]
  67. * Get or set the person's NAME. */
  68. function name($name = null) {
  69. if (!is_null($name)) {
  70. db_query('update person set name = ? where id = ?', array($name, $this->id));
  71. db_commit();
  72. $this->name = $name;
  73. } elseif (is_null($this->name)) {
  74. err(_("Person has no name in name() function")); // try calling name_or_blank or has_name
  75. }
  76. return $this->name;
  77. }
  78. /* name_or_blank
  79. * Get the person's name, or empty string if unknown. Use this as
  80. * prefilled name field in forms. */
  81. function name_or_blank() {
  82. if ($this->name)
  83. return $this->name;
  84. else
  85. return "";
  86. }
  87. /* has_name
  88. * Returns true if we have a name for the person */
  89. function has_name() {
  90. return $this->name ? true : false;
  91. }
  92. /* set_website WEBSIte
  93. * Set name of person's website. */
  94. function set_website($website) {
  95. db_query('update person set website = ? where id = ?', array($website, $this->id));
  96. $this->website = $website;
  97. }
  98. /* website_or_blank
  99. * Get the person's website, or empty string if unknown. Use this
  100. * as prefilled website field in comment forms. */
  101. function website_or_blank() {
  102. if ($this->website)
  103. return $this->website;
  104. else
  105. return "";
  106. }
  107. /* matches_name [NEWNAME]
  108. * Is NEWNAME essentially the same as the person's existing name? */
  109. function matches_name($newname) {
  110. if (!$this->name)
  111. return false;
  112. if (!$newname)
  113. err(_("Name expected in matches_name"));
  114. return person_canonicalise_name($newname) == person_canonicalise_name($this->name);
  115. }
  116. /* password PASSWORD
  117. * Set the person's PASSWORD. */
  118. function password($password) {
  119. if (is_null($password))
  120. err(_("PASSWORD must not be null in password method"));
  121. db_query('update person set password = ? where id = ?', array(crypt($password), $this->id));
  122. }
  123. /* has_password
  124. * Return true if the user has set a password. */
  125. function has_password() {
  126. return !is_null($this->password);
  127. }
  128. /* check_password PASSWORD
  129. * Return true if PASSWORD is the person's password, or false otherwise. */
  130. function check_password($p) {
  131. $c = db_getOne('select password from person where id = ?', $this->id);
  132. if (is_null($c))
  133. return false;
  134. elseif (crypt($p, $c) != $c)
  135. return false;
  136. else
  137. return true;
  138. }
  139. /* numlogins
  140. * How many times has this person logged in? */
  141. function numlogins() {
  142. return $this->numlogins;
  143. }
  144. /* inc_numlogins
  145. * Record this person as having logged in an additional time. */
  146. function inc_numlogins() {
  147. ++$this->numlogins;
  148. db_query('update person set numlogins = numlogins + 1 where id = ?', $this->id);
  149. }
  150. }
  151. /* person_cookie_token ID [DURATION]
  152. * Return an opaque version of ID to identify a person in a cookie. If
  153. * supplied, DURATION is how long the cookie will last (verified by the
  154. * server); if not specified, a default of one year is used. */
  155. function person_cookie_token($id, $duration = null) {
  156. if (is_null($duration))
  157. $duration = 365 * 86400; /* one year */
  158. if (!preg_match('/^[1-9]\d*$/', $id))
  159. err("ID should be a decimal integer, not '$id'");
  160. if (!preg_match('/^[1-9]\d*$/', $duration) || $duration <= 0)
  161. err("DURATION should be a positive decimal integer, not '$duration'");
  162. $salt = bin2hex(urandom_bytes(8));
  163. $start = time();
  164. $sha = sha1("$id/$start/$duration/$salt/" . db_secret());
  165. return sprintf('%d/%d/%d/%s/%s', $id, $start, $duration, $salt, $sha);
  166. }
  167. /* person_check_cookie_token TOKEN
  168. * Given TOKEN, allegedly representing a person, test it and return the
  169. * associated person ID if it is valid, or null otherwise. On successful
  170. * return from this function the database row identifying the person will
  171. * have been locked with SELECT ... FOR UPDATE. */
  172. function person_check_cookie_token($token) {
  173. $a = array();
  174. if (!preg_match('#^([1-9]\d*)/([1-9]\d*)/([1-9]\d*)/([0-9a-f]+)/([0-9a-f]+)$#', $token, $a))
  175. return null;
  176. list($x, $id, $start, $duration, $salt, $sha) = $a;
  177. if (sha1("$id/$start/$duration/$salt/" . db_secret()) != $sha)
  178. return null;
  179. elseif ($start + $duration < time())
  180. return null;
  181. elseif (is_null(db_getOne('select id from person where id = ? for update', $id)))
  182. return null;
  183. else
  184. return $id;
  185. }
  186. /* person_cookie_token_duration TOKEN
  187. * Given a valid cookie TOKEN, return the duration for which it was issued. */
  188. function person_cookie_token_duration($token) {
  189. list($x, $start, $duration) = explode('/', $token);
  190. return $duration;
  191. }
  192. /* Global variable storing the identity of any signed-on person. Since
  193. * person_if_signed_on renews the user's cookie and multiple calls to
  194. * setcookie() with the same cookie name just add further Set-Cookie: headers,
  195. * we need to make sure the cookie is only sent once. Really the proper way to
  196. * do this is to have a flag which means "cookie sent", but that turned out to
  197. * be a historical impossibility.... */
  198. $person_signed_on = null;
  199. /* person_if_signed_on [NORENEW]
  200. * If the user has a valid login cookie, return the corresponding person
  201. * object; otherwise, return null. This function will renew any login cookie,
  202. * unless NORENEW is set. */
  203. function person_if_signed_on($norenew = false) {
  204. global $person_signed_on;
  205. if (!is_null($person_signed_on))
  206. return $person_signed_on;
  207. if (array_key_exists('pb_person_id', $_COOKIE)) {
  208. /* User has a cookie and may be logged in. */
  209. $id = person_check_cookie_token($_COOKIE['pb_person_id']);
  210. if (!is_null($id)) {
  211. $P = new Person($id);
  212. if (!$norenew) {
  213. /* Valid, so renew the cookie. */
  214. # XXX: This turns all session cookies into one-year ones!
  215. $duration = person_cookie_token_duration($_COOKIE['pb_person_id']);
  216. setcookie('pb_person_id', person_cookie_token($id, $duration), time() + $duration, '/', person_cookie_domain());
  217. $person_signed_on = $P; /* save this here so we will renew the cookie on a later call to this function without NORENEW */
  218. }
  219. return $P;
  220. }
  221. }
  222. return null;
  223. }
  224. function person_already_signed_on($email, $name, $person_if_signed_on_function = null) {
  225. if (!is_null($email) && !validate_email($email))
  226. err("'$email' is not a valid email address");
  227. if ($person_if_signed_on_function)
  228. $P = $person_if_signed_on_function();
  229. else
  230. $P = person_if_signed_on();
  231. if (!is_null($P) && (is_null($email) || strtolower($P->email()) == strtolower($email))) {
  232. if (!is_null($name) && !$P->matches_name($name))
  233. $P->name($name);
  234. return $P;
  235. }
  236. return null;
  237. }
  238. /* person_signon DATA [EMAIL] [NAME]
  239. * Return a record of a person, if necessary requiring them to sign on to an
  240. * existing account or to create a new one.
  241. *
  242. * DATA is an array of data about the pledge, including
  243. * 'reason_web' which is something like 'Before you can send a message to
  244. * all the signers, we need to check that you created the pledge.' and
  245. * appears above the send confirm email / login by password dialog.
  246. * 'template' which is the name of the template to use for the confirm
  247. * mail if the user authenticates by email rather than password.
  248. * 'reason_email' is used if and only if 'template' isn't present, and
  249. * goes into the generic-confirm template. It says something like 'Then
  250. * you will be able to send a message to everyone who has signed your
  251. * pledge.'
  252. * 'reason_email_subject' gives Subject: line of email, must be present
  253. * when 'reason_email' is present.
  254. * 'instantly_send_email' if present means the user is prompted as to whether
  255. * to log in by password or by email authentication, they are just sent the
  256. * email immediately
  257. * The rest of the DATA is passed through to the email template.
  258. *
  259. * EMAIL, if present, is the email address to log in with. Otherwise, an email
  260. * addresses is prompted for.
  261. *
  262. * NAME is also optional, and if present updates/creates the default name
  263. * record for the email address. If you do not specify a name here, then
  264. * calling the $this->name() function later will give an error. Instead call
  265. * $this->name_or_blank() or $this->has_name(). The intention here is that if
  266. * the action requires a name, you will have prompted for it in an earlier form
  267. * and included it in the call to this function.
  268. *
  269. * PERSON_IF_SIGNED_ON_FUNCTION, if present, is a function pointer to a wrapper
  270. * for the function person_if_signed_on(). person_signon() will call that wrapper
  271. * instead of person_if_signed_on() directly. This is totally ugly, but will do.
  272. * */
  273. function person_signon($template_data, $email = null, $name = null, $person_if_signed_on_function = null) {
  274. $P = person_already_signed_on($email, $name, $person_if_signed_on_function);
  275. if ($P)
  276. return $P;
  277. /* Get rid of any previous cookie -- if user is logging in again under a
  278. * different email, we don't want to remember the old one. */
  279. person_signoff();
  280. if (headers_sent())
  281. err("Headers have already been sent in person_signon without cookie being present");
  282. if (array_key_exists('instantly_send_email', $template_data)) {
  283. $send_email_part = "&SendEmail=1";
  284. unset($template_data['instantly_send_email']);
  285. } else
  286. $send_email_part = '';
  287. /* No or invalid cookie. We will need to redirect the user via another
  288. * page, either to log in or to prove their email address. */
  289. $st = stash_request(rabx_serialise($template_data), $email);
  290. db_commit();
  291. if ($email)
  292. $email_part = "&email=" . urlencode($email);
  293. else
  294. $email_part = "";
  295. if ($name)
  296. $name_part = "&name=" . urlencode($name);
  297. else
  298. $name_part = "";
  299. header("Location: /login?stash=$st$send_email_part$email_part$name_part");
  300. exit();
  301. }
  302. /* person_signoff
  303. * Log out anyone who is logged in */
  304. function person_signoff() {
  305. setcookie('pb_person_id', '', 0, '/', person_cookie_domain());
  306. # Remove old style cookies left around too
  307. if (person_cookie_domain() != OPTION_WEB_DOMAIN)
  308. setcookie('pb_person_id', '', 0, '/', '.' . OPTION_WEB_DOMAIN);
  309. }
  310. /* person_make_signon_url DATA EMAIL METHOD URL PARAMETERS
  311. * Returns a URL which, if clicked on, will log the user in as EMAIL and have
  312. * them do the request described by METHOD, URL and PARAMETERS (as used in
  313. * stash_new_request). DATA is as for person_signon (but the 'template' and
  314. * 'reason_' entires won't be used since presumably the caller is constructing
  315. * its own email to send). */
  316. function person_make_signon_url($data, $email, $method, $url, $params, $url_base = null) {
  317. if (!$url_base)
  318. $url_base = OPTION_BASE_URL . "/";
  319. $st = stash_new_request($method, $url, $params, $data);
  320. /* XXX should combine this and the similar code in login.php. */
  321. $token = auth_token_store('login', array(
  322. 'email' => $email,
  323. 'name' => null,
  324. 'stash' => $st,
  325. 'direct' => 1
  326. ));
  327. return $url_base . "L/$token";
  328. }
  329. /* person_get EMAIL
  330. * Return a person object for the account with the given EMAIL address, if one
  331. * exists, or null otherwise. */
  332. function person_get($email) {
  333. $id = db_getOne('select id from person where lower(email) = ? for update', strtolower($email));
  334. if (is_null($id))
  335. return null;
  336. else
  337. return new Person($id);
  338. }
  339. /* person_get_or_create EMAIL [NAME]
  340. * If there is an existing account for the given EMAIL address, return the
  341. * person object describing it. Otherwise, create a new account for EMAIL and
  342. * NAME, and return the object describing it. */
  343. function person_get_or_create($email, $name = null) {
  344. if (is_null($email))
  345. err('EMAIL null in person_get_or_create');
  346. $id = db_getOne('select id from person where lower(email) = ?', strtolower($email));
  347. if (is_null($id)) {
  348. db_query('lock table person in share mode'); /* Guard against double-insert. */
  349. $id = db_getOne("select nextval('person_id_seq')");
  350. db_query('insert into person (id, email, name) values (?, ?, ?)', array($id, $email, $name));
  351. }
  352. return new Person($id);
  353. }
  354. ?>