/person.php
PHP | 389 lines | 224 code | 35 blank | 130 comment | 46 complexity | 23cdc555519fc860110edbea64772954 MD5 | raw file
- <?php
- /*
- * person.php:
- * An individual user for the purpose of login etc.
- *
- * Copyright (c) 2005 UK Citizens Online Democracy. All rights reserved.
- * Email: chris@mysociety.org; WWW: http://www.mysociety.org/
- *
- * $Id: person.php,v 1.28 2008-12-01 13:46:50 matthew Exp $
- *
- */
- require_once 'utility.php';
- require_once 'stash.php';
- require_once 'rabx.php';
- require_once 'auth.php';
- /* person_cookie_domain
- * Return the domain to use for cookies. This is computed from HTTP_HOST
- * so we can have multiple domains in one vhost. */
- function person_cookie_domain() {
- $httphost = $_SERVER['HTTP_HOST'];
- # XXX there must be a better way of doing this. (Also, the .livesimply
- # entry is for Francis's local test domain pledge.livesimply)
- if (preg_match("/[^.]+(\.com|\.cat|\.org|\.net|\.co\.uk|\.org\.uk|\.livesimply)$/", $httphost, $matches)) {
- return "." . $matches[0];
- } else {
- return '.' . OPTION_WEB_DOMAIN;
- }
- }
- /* person_canonicalise_name NAME
- * Return NAME with all but alphabetic characters removed; this is used to
- * compare names entered by users to see when the record in the person table
- * should be updated. */
- function person_canonicalise_name($n) {
- return preg_replace('/[^A-Za-z-]/', '', strtolower($n));
- }
- class Person {
- /* person ID | EMAIL
- * Given a person ID or EMAIL address, return a person object describing
- * their account. */
- function Person($id) {
- if (preg_match('/@/', $id))
- $this->id = db_getOne('select id from person where lower(email) = ? for update', strtolower($email));
- elseif (preg_match('/^[1-9]\d*$/', $id))
- $this->id = db_getOne('select id from person where id = ? for update', $id);
- else
- err('value passed to person constructor must be person ID or email address');
- if (is_null($this->id))
- err("No such person '$id'");
- list($this->email, $this->name, $this->password, $this->website, $this->numlogins)
- = db_getRow_list('select email, name, password, website, numlogins from person where id = ?', $id);
- }
- /* id [ID]
- * Get the person ID. */
- function id() {
- return $this->id;
- }
- /* email [EMAIL]
- * Get or set the person's EMAIL address. */
- function email($email = null) {
- if (!is_null($email)) {
- db_query('update person set email = ? where id = ?', array($email, $this->id));
- $this->email = $email;
- }
- return $this->email;
- }
- /* name [NAME]
- * Get or set the person's NAME. */
- function name($name = null) {
- if (!is_null($name)) {
- db_query('update person set name = ? where id = ?', array($name, $this->id));
- db_commit();
- $this->name = $name;
- } elseif (is_null($this->name)) {
- err(_("Person has no name in name() function")); // try calling name_or_blank or has_name
- }
- return $this->name;
- }
-
- /* name_or_blank
- * Get the person's name, or empty string if unknown. Use this as
- * prefilled name field in forms. */
- function name_or_blank() {
- if ($this->name)
- return $this->name;
- else
- return "";
- }
- /* has_name
- * Returns true if we have a name for the person */
- function has_name() {
- return $this->name ? true : false;
- }
- /* set_website WEBSIte
- * Set name of person's website. */
- function set_website($website) {
- db_query('update person set website = ? where id = ?', array($website, $this->id));
- $this->website = $website;
- }
- /* website_or_blank
- * Get the person's website, or empty string if unknown. Use this
- * as prefilled website field in comment forms. */
- function website_or_blank() {
- if ($this->website)
- return $this->website;
- else
- return "";
- }
- /* matches_name [NEWNAME]
- * Is NEWNAME essentially the same as the person's existing name? */
- function matches_name($newname) {
- if (!$this->name)
- return false;
- if (!$newname)
- err(_("Name expected in matches_name"));
- return person_canonicalise_name($newname) == person_canonicalise_name($this->name);
- }
- /* password PASSWORD
- * Set the person's PASSWORD. */
- function password($password) {
- if (is_null($password))
- err(_("PASSWORD must not be null in password method"));
- db_query('update person set password = ? where id = ?', array(crypt($password), $this->id));
- }
- /* has_password
- * Return true if the user has set a password. */
- function has_password() {
- return !is_null($this->password);
- }
- /* check_password PASSWORD
- * Return true if PASSWORD is the person's password, or false otherwise. */
- function check_password($p) {
- $c = db_getOne('select password from person where id = ?', $this->id);
- if (is_null($c))
- return false;
- elseif (crypt($p, $c) != $c)
- return false;
- else
- return true;
- }
- /* numlogins
- * How many times has this person logged in? */
- function numlogins() {
- return $this->numlogins;
- }
- /* inc_numlogins
- * Record this person as having logged in an additional time. */
- function inc_numlogins() {
- ++$this->numlogins;
- db_query('update person set numlogins = numlogins + 1 where id = ?', $this->id);
- }
- }
- /* person_cookie_token ID [DURATION]
- * Return an opaque version of ID to identify a person in a cookie. If
- * supplied, DURATION is how long the cookie will last (verified by the
- * server); if not specified, a default of one year is used. */
- function person_cookie_token($id, $duration = null) {
- if (is_null($duration))
- $duration = 365 * 86400; /* one year */
- if (!preg_match('/^[1-9]\d*$/', $id))
- err("ID should be a decimal integer, not '$id'");
- if (!preg_match('/^[1-9]\d*$/', $duration) || $duration <= 0)
- err("DURATION should be a positive decimal integer, not '$duration'");
- $salt = bin2hex(urandom_bytes(8));
- $start = time();
- $sha = sha1("$id/$start/$duration/$salt/" . db_secret());
- return sprintf('%d/%d/%d/%s/%s', $id, $start, $duration, $salt, $sha);
- }
- /* person_check_cookie_token TOKEN
- * Given TOKEN, allegedly representing a person, test it and return the
- * associated person ID if it is valid, or null otherwise. On successful
- * return from this function the database row identifying the person will
- * have been locked with SELECT ... FOR UPDATE. */
- function person_check_cookie_token($token) {
- $a = array();
- if (!preg_match('#^([1-9]\d*)/([1-9]\d*)/([1-9]\d*)/([0-9a-f]+)/([0-9a-f]+)$#', $token, $a))
- return null;
- list($x, $id, $start, $duration, $salt, $sha) = $a;
- if (sha1("$id/$start/$duration/$salt/" . db_secret()) != $sha)
- return null;
- elseif ($start + $duration < time())
- return null;
- elseif (is_null(db_getOne('select id from person where id = ? for update', $id)))
- return null;
- else
- return $id;
- }
- /* person_cookie_token_duration TOKEN
- * Given a valid cookie TOKEN, return the duration for which it was issued. */
- function person_cookie_token_duration($token) {
- list($x, $start, $duration) = explode('/', $token);
- return $duration;
- }
- /* Global variable storing the identity of any signed-on person. Since
- * person_if_signed_on renews the user's cookie and multiple calls to
- * setcookie() with the same cookie name just add further Set-Cookie: headers,
- * we need to make sure the cookie is only sent once. Really the proper way to
- * do this is to have a flag which means "cookie sent", but that turned out to
- * be a historical impossibility.... */
- $person_signed_on = null;
- /* person_if_signed_on [NORENEW]
- * If the user has a valid login cookie, return the corresponding person
- * object; otherwise, return null. This function will renew any login cookie,
- * unless NORENEW is set. */
- function person_if_signed_on($norenew = false) {
- global $person_signed_on;
- if (!is_null($person_signed_on))
- return $person_signed_on;
- if (array_key_exists('pb_person_id', $_COOKIE)) {
- /* User has a cookie and may be logged in. */
- $id = person_check_cookie_token($_COOKIE['pb_person_id']);
- if (!is_null($id)) {
- $P = new Person($id);
- if (!$norenew) {
- /* Valid, so renew the cookie. */
- # XXX: This turns all session cookies into one-year ones!
- $duration = person_cookie_token_duration($_COOKIE['pb_person_id']);
- setcookie('pb_person_id', person_cookie_token($id, $duration), time() + $duration, '/', person_cookie_domain());
- $person_signed_on = $P; /* save this here so we will renew the cookie on a later call to this function without NORENEW */
- }
- return $P;
- }
- }
- return null;
- }
- function person_already_signed_on($email, $name, $person_if_signed_on_function = null) {
- if (!is_null($email) && !validate_email($email))
- err("'$email' is not a valid email address");
- if ($person_if_signed_on_function)
- $P = $person_if_signed_on_function();
- else
- $P = person_if_signed_on();
- if (!is_null($P) && (is_null($email) || strtolower($P->email()) == strtolower($email))) {
- if (!is_null($name) && !$P->matches_name($name))
- $P->name($name);
- return $P;
- }
- return null;
- }
- /* person_signon DATA [EMAIL] [NAME]
- * Return a record of a person, if necessary requiring them to sign on to an
- * existing account or to create a new one.
- *
- * DATA is an array of data about the pledge, including
- * 'reason_web' which is something like 'Before you can send a message to
- * all the signers, we need to check that you created the pledge.' and
- * appears above the send confirm email / login by password dialog.
- * 'template' which is the name of the template to use for the confirm
- * mail if the user authenticates by email rather than password.
- * 'reason_email' is used if and only if 'template' isn't present, and
- * goes into the generic-confirm template. It says something like 'Then
- * you will be able to send a message to everyone who has signed your
- * pledge.'
- * 'reason_email_subject' gives Subject: line of email, must be present
- * when 'reason_email' is present.
- * 'instantly_send_email' if present means the user is prompted as to whether
- * to log in by password or by email authentication, they are just sent the
- * email immediately
- * The rest of the DATA is passed through to the email template.
- *
- * EMAIL, if present, is the email address to log in with. Otherwise, an email
- * addresses is prompted for.
- *
- * NAME is also optional, and if present updates/creates the default name
- * record for the email address. If you do not specify a name here, then
- * calling the $this->name() function later will give an error. Instead call
- * $this->name_or_blank() or $this->has_name(). The intention here is that if
- * the action requires a name, you will have prompted for it in an earlier form
- * and included it in the call to this function.
- *
- * PERSON_IF_SIGNED_ON_FUNCTION, if present, is a function pointer to a wrapper
- * for the function person_if_signed_on(). person_signon() will call that wrapper
- * instead of person_if_signed_on() directly. This is totally ugly, but will do.
- * */
- function person_signon($template_data, $email = null, $name = null, $person_if_signed_on_function = null) {
- $P = person_already_signed_on($email, $name, $person_if_signed_on_function);
- if ($P)
- return $P;
- /* Get rid of any previous cookie -- if user is logging in again under a
- * different email, we don't want to remember the old one. */
- person_signoff();
- if (headers_sent())
- err("Headers have already been sent in person_signon without cookie being present");
- if (array_key_exists('instantly_send_email', $template_data)) {
- $send_email_part = "&SendEmail=1";
- unset($template_data['instantly_send_email']);
- } else
- $send_email_part = '';
- /* No or invalid cookie. We will need to redirect the user via another
- * page, either to log in or to prove their email address. */
- $st = stash_request(rabx_serialise($template_data), $email);
- db_commit();
- if ($email)
- $email_part = "&email=" . urlencode($email);
- else
- $email_part = "";
- if ($name)
- $name_part = "&name=" . urlencode($name);
- else
- $name_part = "";
- header("Location: /login?stash=$st$send_email_part$email_part$name_part");
- exit();
- }
- /* person_signoff
- * Log out anyone who is logged in */
- function person_signoff() {
- setcookie('pb_person_id', '', 0, '/', person_cookie_domain());
- # Remove old style cookies left around too
- if (person_cookie_domain() != OPTION_WEB_DOMAIN)
- setcookie('pb_person_id', '', 0, '/', '.' . OPTION_WEB_DOMAIN);
- }
- /* person_make_signon_url DATA EMAIL METHOD URL PARAMETERS
- * Returns a URL which, if clicked on, will log the user in as EMAIL and have
- * them do the request described by METHOD, URL and PARAMETERS (as used in
- * stash_new_request). DATA is as for person_signon (but the 'template' and
- * 'reason_' entires won't be used since presumably the caller is constructing
- * its own email to send). */
- function person_make_signon_url($data, $email, $method, $url, $params, $url_base = null) {
- if (!$url_base)
- $url_base = OPTION_BASE_URL . "/";
- $st = stash_new_request($method, $url, $params, $data);
- /* XXX should combine this and the similar code in login.php. */
- $token = auth_token_store('login', array(
- 'email' => $email,
- 'name' => null,
- 'stash' => $st,
- 'direct' => 1
- ));
- return $url_base . "L/$token";
- }
- /* person_get EMAIL
- * Return a person object for the account with the given EMAIL address, if one
- * exists, or null otherwise. */
- function person_get($email) {
- $id = db_getOne('select id from person where lower(email) = ? for update', strtolower($email));
- if (is_null($id))
- return null;
- else
- return new Person($id);
- }
- /* person_get_or_create EMAIL [NAME]
- * If there is an existing account for the given EMAIL address, return the
- * person object describing it. Otherwise, create a new account for EMAIL and
- * NAME, and return the object describing it. */
- function person_get_or_create($email, $name = null) {
- if (is_null($email))
- err('EMAIL null in person_get_or_create');
- $id = db_getOne('select id from person where lower(email) = ?', strtolower($email));
- if (is_null($id)) {
- db_query('lock table person in share mode'); /* Guard against double-insert. */
- $id = db_getOne("select nextval('person_id_seq')");
- db_query('insert into person (id, email, name) values (?, ?, ?)', array($id, $email, $name));
- }
- return new Person($id);
- }
- ?>