This repository has been archived by the owner on Feb 10, 2020. It is now read-only.
/
person.php
389 lines (354 loc) · 14.8 KB
/
person.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
<?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);
}
?>