THEUSER   F
last analyzed

Complexity

Total Complexity 101

Size/Duplication

Total Lines 700
Duplicated Lines 0 %

Test Coverage

Coverage 30.55%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 322
c 3
b 0
f 0
dl 0
loc 700
rs 2
ccs 84
cts 275
cp 0.3055
wmc 101

17 Methods

Rating   Name   Duplication   Size   Complexity  
A set_postcode_cookie() 0 11 2
A confirm_without_token() 0 54 5
A login() 0 39 6
A isvalid() 0 36 5
B update_self_no_confirm() 0 39 7
A _login() 0 21 4
A loggedin() 0 2 1
A isloggedin() 0 11 2
A has_postcode() 0 6 4
B update_self() 0 78 11
B confirm() 0 78 11
A logout() 0 22 4
A update_lastvisit() 0 11 2
A facebook_login() 0 38 3
C __construct() 0 103 16
A unset_postcode_cookie() 0 3 2
C confirm_email() 0 84 16

How to fix   Complexity   

Complex Class

Complex classes like THEUSER often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use THEUSER, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
5
NO HTML IN THIS FILE!!
6
7
This file contains USER and THEUSER classes.
8
9
It automatically instantiates a $THEUSER global object. This refers to the person actually viewing the site. If they have a valid cookie set, $THEUSER's data will be fetched from the DB and they will be logged in. Otherwise they will have a minimum access level and will not be logged in.
10
11
12
The USER class allows us to fetch and alter data about any user (rather than $THEUSER).
13
14
To create a new user do:
15
    $USER = new USER;
16
    $USER->init($user_id);
17
18
You can then access all the user's variables with appropriately named functions, such as:
19
    $USER->user_id();
20
    $USER->email();
21
etc. Don't access the variables directly because I think that's bad.
22
23
USER is extended into the THEUSER class which is used only for the person currently using the site. ie, it adds functions for logging in and out, checking log in status, etc.
24
25
GUESTUSER:
26
In the database there should be a user with an id of 0 and a status of 'Viewer' (and probably a name of 'Guest').
27
28
The cookie set to indicate a logged in user is called "epuser_id". More on that in THEUSER().
29
30
Functions here:
31
32
USER
33
    init()              Send it a user id to fetch data from DB.
34
    add()               Add a new user to the DB.
35
    send_confirmation_email()   Done after add()ing the user.
36
    update_other_user() Update the data of another user.
37
    change_password()   Generate a new password and put in DB.
38
    id_exists()         Checks if a user_id is valid.
39
    email_exists()      Checks if a user exists with a certain email address.
40
    is_able_to()        Is the user allowed to perform this action?
41
    possible_statuses() Return an array of the possible security statuses for users.
42
    Accessor functions for each object variable (eg, user_id()  ).
43
    _update()           Private function that updates a user's data in DB.
44
45
THEUSER
46
    THEUSER()           Constructor that logs in if the cookie is correct.
47
    isloggedin()        Check if the user is logged in or not.
48
    isvalid()           Check to see if the user's login form details are OK.
49
    login()             Log the user in.
50
    logout()            Log the user out.
51
    confirm()           With the correct token, confirms the user then logs them in.
52
    update_self()       Update the user's own data in the DB.
53
    check_user_access() Check a the user is allowed to view this page.
54
55
*/
56
57
class USER {
58
    public $user_id = "0";         // So we have an ID for non-logged in users reporting comments etc.
59
    public $firstname = "Guest";   // So we have something to print for non-logged in users.
60
    public $lastname = "";
61
    public $password = "";         // This will be a hashed version of a plaintext pw.
62
    public $email = "";
63
    public $postcode = "";
64
    public $url = "";
65
    public $lastvisit = "";        // Last time the logged-in user loaded a page (GMT).
66
    public $registrationtime = ""; // When they registered (GMT).
67
    public $registrationip = "";   // Where they registered from.
68
    public $optin = "";            // Int containing multiple binary opt-ins. (See top of User.php)
69
    public $deleted = "";          // User can't log in or have their info displayed.
70
    public $confirmed = '';        // boolean - Has the user confirmed via email?
71
    public $facebook_id = '';      // Facebook ID for users who login with FB
72
    public $facebook_token = '';   // Facebook token for users who login with FB
73
    public $can_annotate = false;  // Can the user add annotations
74
    public $organisation = '';     // The organisation the user belongs to
75
    // Don't use the status to check access privileges - use the is_able_to() function.
76
    public $status = "Viewer";
77
78
    // If you add more user variables above you should also:
79
    //      Add the approrprate code to $this->add()
80
    //      Add the appropriate code to $this->_update()
81
    //      Add accessor functions way down below...
82
    //      Alter THEUSER->update_self() to update with the new vars, if appropriate.
83
    //      Change things in the add/edit/view user page.
84 1
85 1
    public function __construct() {
86 1
        $this->db = new ParlDB();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
87
    }
88 4
89
    public function init($user_id) {
90
        // Pass it a user id and it will fetch the user's data from the db
91
        // and put it all in the appropriate variables.
92
        // Returns true if we've found user_id in the DB, false otherwise.
93
94 4
        // Look for this user_id's details.
95
        $q = $this->db->query(
96
            "SELECT firstname,
97
                                lastname,
98
                                password,
99
                                email,
100
                                postcode,
101
                                url,
102
                                lastvisit,
103
                                registrationtime,
104
                                registrationtoken,
105
                                registrationip,
106
                                optin,
107
                                status,
108
                                deleted,
109
                                confirmed,
110
                                facebook_id,
111
                                facebook_token,
112 4
                                can_annotate,
113
                                organisation
114
                        FROM    users
115 4
                        WHERE   user_id = :user_id",
116
            [':user_id' => $user_id]
117
        )->first();
118 4
119 4
120 4
        if ($q) {
121 4
            // We've got a user, so set them up.
122 4
123 4
            $this->user_id              = $user_id;
124 4
            $this->firstname            = $q["firstname"];
125 4
            $this->lastname             = $q["lastname"];
126 4
            $this->password             = $q["password"];
127 4
            $this->email                = $q["email"];
128 4
            $this->postcode             = $q["postcode"];
129 4
            $this->facebook_id          = $q["facebook_id"];
130 4
            $this->facebook_token       = $q["facebook_token"];
131 4
            $this->url                  = $q["url"];
132 4
            $this->lastvisit            = $q["lastvisit"];
133 4
            $this->registrationtoken    = $q['registrationtoken'];
0 ignored issues
show
Bug Best Practice introduced by
The property registrationtoken does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
134 4
            $this->registrationtime     = $q["registrationtime"];
135
            $this->registrationip       = $q["registrationip"];
136 4
            $this->optin                = $q["optin"];
137
            $this->status               = $q["status"];
138
            $this->organisation         = $q["organisation"];
139
            $this->deleted = $q["deleted"] == 1 ? true : false;
140
            $this->confirmed = $q["confirmed"] == 1 ? true : false;
141
            $this->can_annotate = $q["can_annotate"] == 1 ? true : false;
142
143
            return true;
144
145 1
        } else {
146
            return false;
147
            twfy_debug("USER", "There is no user with an id of '" . _htmlentities($user_id) . "'");
0 ignored issues
show
Unused Code introduced by
twfy_debug('USER', 'Ther...tities($user_id) . ''') is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
148
        }
149
150
    }
151
152
    public function add($details, $confirmation_required = true) {
153
        // Adds a new user's info into the db.
154
        // Then optionally (and usually) calls another function to
155
        // send them a confirmation email.
156
157
        // $details is an associative array of all the user's details, of the form:
158 1
        // array (
159
        //      "firstname" => "Fred",
160 1
        //      "lastname"  => "Bloggs",
161
        //      etc... using the same keys as the object variable names.
162 1
        // )
163
        // The BOOL variables (eg, optin) will be true or false and will need to be
164 1
        // converted to 1/0 for MySQL.
165
        global $REMOTE_ADDR;
166
167
        $registrationtime = gmdate("YmdHis");
168 1
169 1
        $passwordforDB = password_hash($details["password"], PASSWORD_BCRYPT);
170
171
        if (!isset($details["status"])) {
172 1
            $details["status"] = "User";
173
        }
174
175
        if (!isset($details["facebook_id"])) {
176
            $details["facebook_id"] = "";
177
        }
178
179
        $q = $this->db->query("INSERT INTO users (
180
                firstname,
181
                lastname,
182
                email,
183
                postcode,
184
                url,
185
                password,
186
                optin,
187
                status,
188
                registrationtime,
189
                registrationip,
190
                facebook_id,
191
                deleted
192
            ) VALUES (
193
                :firstname,
194
                :lastname,
195
                :email,
196
                :postcode,
197
                :url,
198
                :password,
199
                :optin,
200 1
                :status,
201 1
                :registrationtime,
202 1
                :registrationip,
203 1
                :facebook_id,
204 1
                '0'
205 1
            )
206 1
        ", [
207 1
            ':firstname' => $details["firstname"],
208 1
            ':lastname' => $details["lastname"],
209 1
            ':email' => $details["email"],
210 1
            ':postcode' => $details["postcode"],
211
            ':url' => $details["url"],
212
            ':password' => $passwordforDB,
213 1
            ':optin' => $details["optin"],
214
            ':status' => $details["status"],
215
            ':registrationtime' => $registrationtime,
216
            ':facebook_id' => $details["facebook_id"],
217 1
            ':registrationip' => $REMOTE_ADDR,
218 1
        ]);
219 1
220
        if ($q->success()) {
221
            // Set these so we can log in.
222
            // Except we no longer automatically log new users in, we
223
            // send them an email. So this may not be required.
224
            $this->user_id = $q->insert_id();
225 1
            $this->password = $passwordforDB;
226
            $this->facebook_id = $details["facebook_id"];
227
228
            // We have to set the user's registration token.
229
            // This will be sent to them via email, so we can confirm they exist.
230
            // The token will be the first 16 characters of a hash.
231 1
232 1
            $token = substr(password_hash($details["email"] . microtime(), PASSWORD_BCRYPT), 29, 16);
233
234
            // Full stops don't work well at the end of URLs in emails, so
235 1
            // replace them. And double slash would be treated as single and
236
            // not work either. We won't be doing anything clever with the hash
237
            // stuff, just need to match this token.
238
            $token = strtr($token, './', 'Xx');
239 1
            $this->registrationtoken = $token;
0 ignored issues
show
Bug Best Practice introduced by
The property registrationtoken does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
240 1
241
            // Add that to the DB.
242
            $r = $this->db->query("UPDATE users
243 1
                            SET registrationtoken = :registrationtoken
244
                            WHERE   user_id = :user_id
245
                            ", [
246 1
                ':registrationtoken' => $this->registrationtoken,
247
                ':user_id' => $this->user_id,
248
            ]);
249
250
            if ($r->success()) {
251
                // Updated DB OK.
252
253
                if ($details['mp_alert'] && $details['postcode']) {
254
                    $MEMBER = new MEMBER(['postcode' => $details['postcode'], 'house' => HOUSE_TYPE_COMMONS]);
255
                    $pid = $MEMBER->person_id();
256
                    # No confirmation email, but don't automatically confirm
257
                    $ALERT = new ALERT();
258 1
                    $ALERT->add([
259
                        'email' => $details['email'],
260
                        'pid' => $pid,
261
                        'pc' => $details['postcode'],
262
                    ], false, false);
263
                }
264
265
                if ($confirmation_required) {
266
                    // Right, send the email...
267
                    $success = $this->send_confirmation_email($details);
268
269
                    if ($success) {
270
                        // All is good in the world!
271 1
                        return true;
272
                    } else {
273
                        // Couldn't send the email.
274
                        return false;
275
                    }
276
                } else {
277
                    // No confirmation email needed.
278
                    return true;
279
                }
280
            } else {
281
                // Couldn't add the registration token to the DB.
282
                return false;
283
            }
284
285
        } else {
286
            // Couldn't add the user's data to the DB.
287
            return false;
288
        }
289
    }
290
291
    public function add_facebook_id($facebook_id) {
292
        $q = $this->db->query(
293
            "UPDATE users SET facebook_id = :facebook_id WHERE email = :email",
294
            [
295
                ':facebook_id' => $facebook_id,
296
                ':email' => $this->email,
297
            ]
298
        );
299
300
        if ($q->success()) {
301
            $this->facebook_id = $facebook_id;
302
303
            return $facebook_id;
304
        } else {
305
            return false;
306
        }
307
    }
308
309
    public function send_email_confirmation_email($details) {
310
        // A brief check of the facts...
311
        if (!is_numeric($this->user_id)
312
            || !isset($details['email'])
313
            || $details['email'] == ''
314
            || !isset($details['token'])
315
            || $details['token'] == '') {
316
            return false;
317
        }
318
319
        // We prefix the registration token with the user's id and '-'.
320
        // Not for any particularly good reason, but we do.
321
322
        $urltoken = $this->user_id . '-' . $details['token'];
323
324
        $confirmurl = 'https://' . DOMAIN . '/E/' . $urltoken;
325
326
        // Arrays we need to send a templated email.
327
        $data =  [
328
            'to'        => $details['email'],
329
            'template'  => 'email_confirmation',
330
        ];
331
332
        $merge =  [
333
            'CONFIRMURL'    => $confirmurl,
334
        ];
335
336
        $success = send_template_email($data, $merge);
337
338
        if ($success) {
339
            return true;
340
        } else {
341
            return false;
342
        }
343
    }
344
345
    public function send_confirmation_email($details) {
346
        // After we've add()ed a user we'll probably be sending them
347
        // a confirmation email with a link to confirm their address.
348
349
        // $details is the array we just sent to add(), and which it's
350
        // passed on to us here.
351
352
        // A brief check of the facts...
353
        if (!is_numeric($this->user_id)
354
            || !isset($details['email'])
355
            || $details['email'] == '') {
356
            return false;
357
        }
358
359
        // We prefix the registration token with the user's id and '-'.
360
        // Not for any particularly good reason, but we do.
361
362
        $urltoken = $this->user_id . '-' . $this->registrationtoken;
363
364
        $confirmurl = 'https://' . DOMAIN . '/U/' . $urltoken;
365
        if (isset($details['ret'])) {
366
            $confirmurl .= '?ret=' . $details['ret'];
367
        }
368
369
        // Arrays we need to send a templated email.
370
        $data =  [
371
            'to'        => $details['email'],
372
            'template'  => 'join_confirmation',
373
        ];
374
375
        $merge =  [
376
            'CONFIRMURL'    => $confirmurl,
377
        ];
378
379
        $success = send_template_email($data, $merge);
380
381
        if ($success) {
382
            return true;
383
        } else {
384
            return false;
385
        }
386
    }
387
388
389
    public function update_other_user($details) {
390
        // If someone (like an admin) is updating another user, call this
391
        // function. It checks their privileges before letting them.
392
393
        // $details is an array like that in $this->add().
394
        // It must include a 'user_id' element!
395
396
        global $THEUSER;
397
398
        if (!isset($details["user_id"])) {
399
            return false;
400
401
        } elseif ($THEUSER->is_able_to("edituser")) {
402
403
            // If the user doing the updating has appropriate privileges...
404
405
            $newdetails = $this->_update($details);
406
407
            // $newdetails will be an array of details if all went well,
408
            // false otherwise.
409
            if ($newdetails) {
410
                return true;
411
            } else {
412
                return false;
413
            }
414
415
        } else {
416
            return false;
417
418
        }
419
    }
420
421
422
423
    public function change_password($email) {
424
425
        // This function is called from the Change Password page.
426
        // It will create a new password for the user with $email address.
427
        // If all goes OK it will return the plaintext version of the password.
428
        // Otherwise it returns false.
429
430
        if ($this->email_exists($email)) {
431
432
            $this->email = $email;
433
            for (;;) {
434
435
                $pwd = null;
436
                $o = null;
437
438
                // Generates the password ....
439
                for ($x = 0; $x < 6;) {
440
                    $y = rand(1, 1000);
441
                    if ($y > 350 && $y < 601) {
442
                        $d = chr(rand(48, 57));
443
                    }
444
                    if ($y < 351) {
445
                        $d = chr(rand(65, 90));
446
                    }
447
                    if ($y > 600) {
448
                        $d = chr(rand(97, 122));
449
                    }
450
                    if ($d != $o && !preg_match('#[O01lI]#', $d)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $d does not seem to be defined for all execution paths leading up to this point.
Loading history...
451
                        $o = $d;
452
                        $pwd .= $d;
453
                        $x++;
454
                    }
455
                }
456
457
                // If the PW fits your purpose (e.g. this regexpression) return it, else make a new one
458
                // (You can change this regular-expression how you want ....)
459
                if (preg_match("/^[a-zA-Z]{1}([a-zA-Z]+[0-9][a-zA-Z]+)+/", $pwd)) {
0 ignored issues
show
Bug introduced by
It seems like $pwd can also be of type null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

459
                if (preg_match("/^[a-zA-Z]{1}([a-zA-Z]+[0-9][a-zA-Z]+)+/", /** @scrutinizer ignore-type */ $pwd)) {
Loading history...
460
                    break;
461
                }
462
463
            }
464
            $pwd = strtoupper($pwd);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $pwd does not seem to be defined for all execution paths leading up to this point.
Loading history...
465
466
            // End password generating stuff.
467
468
        } else {
469
470
            // Email didn't exist.
471
            return false;
472
473
        }
474
475
        $passwordforDB = password_hash($pwd, PASSWORD_BCRYPT);
476
477
        $q = $this->db->query(
478
            "UPDATE users SET password = :password WHERE email = :email",
479
            [
480
                ':password' => $passwordforDB,
481
                ':email' => $email,
482
            ]
483
        );
484
485
        if ($q->success()) {
486
            $this->password = $pwd;
487
488
            return $pwd;
489
490
        } else {
491
            return false;
492
        }
493
494
    }
495
496
    public function send_password_reminder() {
497
        global $PAGE;
498
499
        // You'll probably have just called $this->change_password().
500
501
        if ($this->email() == '') {
502
            $PAGE->error_message("No email set for this user, so can't send a password reminder.");
503
504
            return false;
505
        }
506
507
        $data =  [
508
            'to'            => $this->email(),
509
            'template'      => 'new_password',
510
        ];
511
512
        $URL = new \MySociety\TheyWorkForYou\Url("userlogin");
513
514
        $merge =  [
515
            'EMAIL'         => $this->email(),
516
            'LOGINURL'      => "https://" . DOMAIN . $URL->generate(),
517
            'PASSWORD'      => $this->password(),
518
        ];
519
520
        // send_template_email in utility.php.
521
        $success = send_template_email($data, $merge);
522
523
        return $success;
524
525
    }
526
527
528
529 1
530
    public function id_exists($user_id) {
531
        // Returns true if there's a user with this user_id.
532 1
533 1
        if (is_numeric($user_id)) {
534 1
            $q = $this->db->query(
535
                "SELECT user_id FROM users WHERE user_id = :user_id",
536
                [':user_id' => $user_id]
537
            );
538
            if ($q->rows() > 0) {
539
                return true;
540 1
            } else {
541
                return false;
542
            }
543
        } else {
544
            return false;
545
        }
546
547
    }
548
549
550
    public function email_exists($email, $return_id = false) {
551
        // Returns true if there's a user with this email address.
552
553
        if ($email != "") {
554
            $q = $this->db->query("SELECT user_id FROM users WHERE email = :email", [':email' => $email])->first();
555
            if ($q) {
556
                if ($return_id) {
557
                    return $q['user_id'];
558
                }
559
                return true;
560
            } else {
561
                return false;
562
            }
563
        } else {
564
            return false;
565
        }
566
    }
567
568
    public function facebook_id_exists($id, $return_id = false) {
569
        // Returns true if there's a user with this facebook id.
570
571
        if ($id != "") {
572
            $q = $this->db->query("SELECT user_id FROM users WHERE facebook_id = :id", [':id' => $id])->first();
573
            if ($q) {
574
                if ($return_id) {
575
                    return $q['user_id'];
576
                }
577
                return true;
578
            } else {
579
                return false;
580
            }
581
        } else {
582
            return false;
583
        }
584
    }
585
586
    public function is_able_to($action) {
587
        // Call this function to find out if a user is allowed to do something.
588
        // It uses the user's status to return true or false.
589
        // Possible actions:
590
        //  "addcomment"
591
        //  "reportcomment"
592
        //  "edituser"
593
        global $PAGE;
594
595
        $status = $this->status();
596
597
        switch ($action) {
598
599
            // You can add more below as they're needed...
600
            // But keep them in alphabetical order!
601
602
            // no break
603
            case "addcomment":  // Post comments.
604
                if ($this->can_annotate) {
605
                    return true;
606
                }
607
608
                // no break
609
            case "addterm": // Add Glossary terms.
610
611
                switch ($status) {
612
                    case "User":            return false;
613
                    case "Moderator":       return false;
614
                    case "Administrator":   return true;
615
                    case "Superuser":       return true;
616
                    default: /* Viewer */   return false;
617
                }
618
619
                // no break
620
            case "deletecomment": // Delete comments.
621
622
                switch ($status) {
623
                    case "User":            return false;
624
                    case "Moderator":       return true;
625
                    case "Administrator":   return true;
626
                    case "Superuser":       return true;
627
                    default: /* Viewer */   return false;
628
                }
629
630
                // no break
631
            case "edituser":
632
633
                switch ($status) {
634
                    case "User":            return false;
635
                    case "Moderator":       return false;
636
                    case "Administrator":   return false;
637
                    case "Superuser":       return true;
638
                    default: /* Viewer */   return false;
639
                }
640
641
                // no break
642
            case "reportcomment":   // Report a comment for moderation.
643
644
                switch ($status) {
645
                    case "User":            return true;
646
                    case "Moderator":       return true;
647
                    case "Administrator":   return true;
648
                    case "Superuser":       return true;
649
                    default: /* Viewer */   return true;
650
                }
651
652
                // no break
653
            case "viewadminsection":    // Access pages in the Admin section.
654
655
                switch ($status) {
656
                    case "User":            return false;
657
                    case "Moderator":       return false;
658
                    case "Administrator":   return true;
659
                    case "Superuser":       return true;
660
                    default: /* Viewer */   return false;
661
                }
662
663
                // no break
664
            case "voteonhansard":   // Rate hansard things interesting/not.
665
                /* Everyone */              return true;
666
667
            default:
668
                $PAGE->error_message("You need to set permissions for '$action'!");
669
670
                return false;
671
672
673
        }
674
675
676
677 40
    }
678
679
    // Same for every user...
680 40
    // Just returns an array of the possible statuses a user could have.
681
    // Handy for forms where you edit/view users etc.
682
    public function possible_statuses() {
683 40
        // Maybe there's a way of fetching these from the DB,
684
        // so we don't duplicate them here...?
685
686
        $statuses =  ["Viewer", "User", "Moderator", "Administrator", "Superuser"];
687
688
        return $statuses;
689
690 2
    }
691
692
693
694
    // Functions for accessing the user's variables.
695
696 2
    public function user_id() {
697
        return $this->user_id;
698
    }
699 2
    public function firstname() {
700 1
        return $this->firstname;
701
    }
702 1
    public function lastname() {
703 1
        return $this->lastname;
704
    }
705
    public function password() {
706
        return $this->password;
707
    }
708
    public function email() {
709 2
        return $this->email;
710 2
    }
711 2
    public function postcode() {
712 2
        return $this->postcode;
713 2
    }
714
    public function url() {
715 2
        return $this->url;
716
    }
717 2
    public function lastvisit() {
718
        return $this->lastvisit;
719
    }
720
    public function facebook_id() {
721
        return $this->facebook_id;
722
    }
723
    public function facebook_token() {
724
        return $this->facebook_token;
725
    }
726
    public function facebook_user() {
727
        return $this->facebook_user;
728 2
    }
729
730
    public function registrationtime() {
731
        return $this->registrationtime;
732
    }
733
    public function registrationip() {
734
        return $this->registrationip;
735
    }
736
    public function optin() {
737
        return $this->optin;
738
    }
739
    // Don't use the status to check access privileges - use the is_able_to() function.
740
    // But you might use status() to return text to display, describing a user.
741 2
    // We can then change what status() does in the future if our permissions system
742
    // changes.
743
    public function status() {
744
        return $this->status;
745
    }
746
    public function deleted() {
747
        return $this->deleted;
748
    }
749
    public function confirmed() {
750
        return $this->confirmed;
751
    }
752
753
    public function can_annotate() {
754 2
        return $this->can_annotate;
755
    }
756
757
    public function organisation() {
758
        return $this->organisation;
759
    }
760
761
    public function postcode_is_set() {
762 2
        // So we can tell if the, er, postcode is set or not.
763 1
        // Could maybe put some validation in here at some point.
764 1
        if ($this->postcode != '') {
765
            return true;
766
        } else {
767 2
            return false;
768
        }
769
    }
770
771
772 2
    /////////// PRIVATE FUNCTIONS BELOW... ////////////////
773 2
774 2
    public function _update($details) {
775 2
        // Update a user's info.
776 2
        // DO NOT call this function direct.
777
        // Call either $this->update_other_user() or $this->update_self().
778
779 2
        // $details is an array like that in $this->add().
780 2
        global $PAGE;
781 2
782 2
        // Update email alerts if email address changed
783 2
        if (isset($details['email']) && $this->email != $details['email']) {
784 2
            $this->db->query(
785 2
                'UPDATE alerts SET email = :details_email WHERE email = :email',
786
                [
787
                    ':details_email' => $details['email'],
788
                    ':email' => $this->email,
789
                ]
790
            );
791 2
        }
792 2
793
        // These are used to put optional fragments of SQL in, depending
794
        // on whether we're changing those things or not.
795
        $passwordsql = "";
796
        $deletedsql = "";
797
        $confirmedsql = "";
798
        $statussql = "";
799
        $emailsql = '';
800
        $annotatesql = '';
801
802
        $params = [];
803
804
        if (isset($details["password"]) && $details["password"] != "") {
805
            // The password is being updated.
806
            // If not, the password fields on the form will be left blank
807
            // so we don't want to overwrite the user's pw in the DB!
808
809
            $passwordforDB = password_hash($details["password"], PASSWORD_BCRYPT);
810
811
            $passwordsql = "password = :password, ";
812
            $params[':password'] = $passwordforDB;
813
        }
814
815
        if (isset($details["deleted"])) {
816
            // 'deleted' won't always be an option (ie, if the user is updating
817
            // their own info).
818
            if ($details['deleted'] == true) {
819
                $del = '1';
820
            } elseif ($details['deleted'] == false) {
821
                $del = '0';
822
            }
823
            if (isset($del)) {
824 3
                $deletedsql = "deleted  = '$del', ";
825
            }
826
        }
827
828 3
        if (isset($details["confirmed"])) {
829
            // 'confirmed' won't always be an option (ie, if the user is updating
830
            // their own info).
831
            if ($details['confirmed'] == true) {
832
                $con = '1';
833
            } elseif ($details['confirmed'] == false) {
834
                $con = '0';
835
            }
836
            if (isset($con)) {
837
                $confirmedsql = "confirmed  = '$con', ";
838
            }
839
        }
840 3
841
        if (isset($details["status"]) && $details["status"] != "") {
842 3
            // 'status' won't always be an option (ie, if the user is updating
843
            // their own info.
844
            $statussql = "status = :status, ";
845
            $params[':status'] = $details['status'];
846
847
        }
848
849
        if (isset($details["can_annotate"])) {
850 3
            $annotatesql = "can_annotate = :can_annotate, organisation = :organisation, ";
851
            $params[':can_annotate'] = $details['can_annotate'] ? 1 : 0;
852
            $params[':organisation'] = $details['organisation'];
853
        }
854 3
855
        if (isset($details['email']) && $details['email']) {
856 3
            $emailsql = "email = :email, ";
857
            $params[':email'] = $details['email'];
858 3
        }
859
860 3
        $q = $this->db->query("UPDATE users
861
                        SET     firstname   = :firstname,
862
                                lastname    = :lastname,
863
                                postcode    = :postcode,
864
                                url         = :url,"
865
                                . $passwordsql
866 3
                                . $deletedsql
867
                                . $confirmedsql
868
                                . $emailsql
869
                                . $statussql
870
                                . $annotatesql . "
871
                                optin       = :optin
872
                        WHERE   user_id     = :user_id
873
                        ", array_merge($params, [
874
                                    ':firstname' => $details['firstname'],
875
                                    ':lastname' => $details['lastname'],
876
                                    ':postcode' => $details['postcode'],
877
                                    ':url' => $details['url'],
878 3
                                    ':optin' => $details['optin'],
879
                                    ':user_id' => $details['user_id'],
880
                                ]));
881
882
        // If we're returning to
883
        // $this->update_self() then $THEUSER will have its variables
884
        // updated if everything went well.
885
        if ($q->success()) {
886
            return $details;
887
888
        } else {
889
            $PAGE->error_message("Sorry, we were unable to update user id '" . _htmlentities($details["user_id"]) . "'");
890 3
891 3
            return false;
892 3
        }
893
894
895 3
    }
896
897
898
899
900
901 3
} // End USER class
902
903
904
905
906 3
907
908
class THEUSER extends USER {
909
    // Handles all the login/out functionality and checking for the user
910
    // who is using the site right NOW. Yes, him, over there.
911
912
    // This will become true if all goes well...
913
    public $loggedin = false;
914
    public $facebook_user = false;
915
916
917
    public function __construct() {
918 3
        // This function is run automatically when a THEUSER
919 3
        // object is instantiated.
920
921
        $this->db = new ParlDB();
0 ignored issues
show
Bug Best Practice introduced by
The property db does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
922
923
        // We look at the user's cookie and see if it's valid.
924
        // If so, we're going to log them in.
925
926 3
        // A user's cookie is of the form:
927
        // 123.blahblahblah
928 3
        // Where '123' is a user id, and 'blahblahblah' is an md5 hash of the
929
        // encrypted password we've stored in the db.
930 3
        // (Maybe we could just put the encrypted pw in the cookie and md5ing
931
        // it is overkill? Whatever, it works.)
932 3
933
        $cookie = get_cookie_var("epuser_id"); // In includes/utility.php.
934
935
        if ($cookie == '') {
936
            $cookie = get_cookie_var("facebook_id");
937
            if ($cookie != '') {
938
                $this->facebook_user = true;
939
                twfy_debug("THEUSER", "is facebook login");
940 3
            }
941
        }
942
943
        if ($cookie == '') {
944
            twfy_debug("THEUSER init FAILED", "No cookie set");
945
            $this->loggedin = false;
946
947
        } elseif (preg_match("/([[:alnum:]]*)\.([[:alnum:]]*)/", $cookie, $matches)) {
948 7
949
            if (is_numeric($matches[1])) {
950
951 7
                $success = $this->init($matches[1]);
952 2
953
                if ($success) {
954 2
                    // We got all the user's data from the DB.
955
956 7
                    // But we need to check the password before we log them in.
957
                    // And make sure the user hasn't been "deleted".
958 7
959
                    if ($this->facebook_user) {
960
                        if (md5($this->facebook_token()) == $matches[2] && $this->deleted() == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
961
                            twfy_debug("THEUSER", "init SUCCESS: setting as logged in");
962
                            $this->loggedin = true;
963
                        } elseif (md5($this->facebook_token()) != $matches[2]) {
964
                            twfy_debug("THEUSER", "init FAILED: Facebook token doesn't match cookie");
965
                            $this->loggedin = false;
966
                        } else {
967
                            twfy_debug("THEUSER", "init FAILED: User is deleted");
968
                            $this->loggedin = false;
969
                        }
970
                    } else {
971
                        if (md5($this->password()) == $matches[2] && $this->deleted() == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
972
                            // The correct password is in the cookie,
973
                            // and the user isn't deleted, so set the user to be logged in.
974
975
                            // This would be an appropriate place to call other functions
976
                            // that might set user info that only a logged-in user is going
977
                            // to need. Their preferences and saved things or something.
978
979
980
                            twfy_debug("THEUSER init SUCCEEDED", "setting as logged in");
981
                            $this->loggedin = true;
982
983
                        } elseif (md5($this->password()) != $matches[2]) {
984
                            twfy_debug("THEUSER init FAILED", "Password doesn't match cookie");
985
                            $this->loggedin = false;
986
                        } else {
987
                            twfy_debug("THEUSER init FAILED", "User is deleted");
988
                            $this->loggedin = false;
989
                        }
990
                    }
991
992
                } else {
993
                    twfy_debug("THEUSER init FAILED", "didn't get 1 row from db");
994
                    $this->loggedin = false;
995
                }
996
997
            } else {
998
                twfy_debug("THEUSER init FAILED", "cookie's user_id is not numeric");
999
                $this->loggedin = false;
1000
            }
1001
1002
        } else {
1003 4
            twfy_debug("THEUSER init FAILED", "cookie is not of the correct form");
1004 4
            $this->loggedin = false;
1005 4
        }
1006
1007
        // If a user is logged in they *might* have set their own postcode.
1008 4
        // If they aren't logged in, or they haven't set one, then we may
1009
        // have set a postcode for them when they searched for their MP.
1010
        // If so, we'll use that as $this->postcode.
1011
        if ($this->postcode == '') {
1012
            if (get_cookie_var(POSTCODE_COOKIE) != '') {
1013
                $pc = get_cookie_var(POSTCODE_COOKIE);
1014
1015
                $this->set_postcode_cookie($pc);
1016
            }
1017
        }
1018
1019
        $this->update_lastvisit();
1020
1021
    } // End THEUSER()
1022
1023
    public function update_lastvisit() {
1024
1025
        if ($this->isloggedin()) {
1026
            // Set last_visit to now.
1027
            $date_now = gmdate("Y-m-d H:i:s");
1028
            $this->db->query(
1029
                "UPDATE users SET lastvisit = :lastvisit WHERE user_id = :user_id",
1030
                [ ':lastvisit' => $date_now, ':user_id' => $this->user_id() ]
1031
            );
1032
1033
            $this->lastvisit = $date_now;
1034
        }
1035
    }
1036
1037
    // For completeness, but it's better to call $this->isloggedin()
1038
    // if you want to check the log in status.
1039
    public function loggedin() {
1040
        return $this->loggedin;
1041
    }
1042
1043
1044
1045
    public function isloggedin() {
1046
        // Call this function to check if the user is successfully logged in.
1047
1048
        if ($this->loggedin()) {
1049
            twfy_debug("THEUSER", "isloggedin: true");
1050
1051
            return true;
1052
        } else {
1053
            twfy_debug("THEUSER", "isloggedin: false");
1054
1055
            return false;
1056
        }
1057
    }
1058
1059
1060
    public function isvalid($email, $userenteredpassword) {
1061
        // Returns true if this email and plaintext password match a user in the db.
1062
        // If false returns an array of form error messages.
1063
1064
        // We use this on the log in page to check if the details the user entered
1065
        // are correct. We can then continue with logging the user in (taking into
1066
        // account their cookie remembering settings etc) with $this->login().
1067
1068
        // This error string is shared between both email and password errors to
1069
        // prevent leaking of account existence.
1070
1071
        $error_string = 'There is no user registered with an email of ' . _htmlentities($email) . ', or the given password is incorrect. If you are subscribed to email alerts, you are not necessarily registered on the website. If you register, you will be able to manage your email alerts, as well as leave annotations.';
1072
1073
        $q = $this->db->query("SELECT user_id, password, deleted, confirmed FROM users WHERE email = :email", [':email' => $email])->first();
1074
1075
        if ($q) {
1076
            // OK.
1077
            $dbpassword = $q["password"];
1078
            if (password_verify($userenteredpassword, $dbpassword)) {
1079
                $this->user_id  = $q["user_id"];
1080
                $this->password = $dbpassword;
1081
                // We'll need these when we're going to log in.
1082
                $this->deleted  = $q["deleted"] == 1 ? true : false;
1083
                $this->confirmed = $q["confirmed"] == 1 ? true : false;
1084
1085
                return true;
1086
1087
            } else {
1088
                // Failed.
1089
                return  ["invalidemail" => $error_string];
1090
1091
            }
1092
1093
        } else {
1094
            // Failed.
1095
            return  ["invalidemail" => $error_string];
1096
        }
1097
1098
    }
1099
1100
    public function has_postcode() {
1101
        $has_postcode = false;
1102
        if ($this->isloggedin() && $this->postcode() != '' || $this->postcode_is_set()) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->isloggedin() && ...this->postcode_is_set(), Probably Intended Meaning: $this->isloggedin() && (...his->postcode_is_set())
Loading history...
1103
            $has_postcode = true;
1104
        }
1105
        return $has_postcode;
1106
    }
1107
1108
1109
    public function facebook_login($returl, $expire, $accessToken) {
1110
        global $PAGE;
1111
1112
        twfy_debug("THEUSER", "Faceook login, user_id " . $this->user_id);
1113
        twfy_debug("THEUSER", "Faceook login, facebook_id " . $this->facebook_id);
1114
        twfy_debug("THEUSER", "Faceook login, email" . $this->email);
1115
        if ($this->facebook_id() == "") {
1116
            $PAGE->error_message("We don't have a facebook id for this user.", true);
1117
1118
            return;
1119
        }
1120
1121
        twfy_debug("THEUSER", "Faceook login, facebook_token: " . $accessToken);
1122
1123
        $q = $this->db->query(
1124
            "UPDATE users SET facebook_token = :token WHERE email = :email",
1125
            [
1126
                ':token' => $accessToken,
1127
                ':email' => $this->email,
1128
            ]
1129
        );
1130
1131
        if (!$q->success()) {
1132
            $PAGE->error_message("There was a problem logging you in", true);
1133
            twfy_debug("THEUSER", "Faceook login, failed to set accessToken");
1134
1135
            return false;
1136
        }
1137
1138
        // facebook login users probably don't have a password
1139
        $cookie = $this->user_id() . "." . md5($accessToken);
1140 2
        twfy_debug("THEUSER", "Faceook login, cookie: " . $cookie);
1141 2
1142 2
        twfy_debug("USER", "logging in user from facebook " . $this->user_id);
1143 2
1144 2
        $this->loggedin = true;
1145
        $this->_login($returl, $expire, $cookie, 'facebook_id');
1146 2
        return true;
1147
    }
1148
1149 2
    public function login($returl, $expire) {
1150
1151
        // This is used to log the user in. Duh.
1152
        // You should already have checked the user's email and password using
1153 2
        // $this->isvalid()
1154
        // That will have set $this->user_id and $this->password, allowing the
1155 2
        // login to proceed...
1156 2
1157 2
        // $expire is either 'session' or 'never' - for the cookie.
1158 2
1159 1
        // $returl is the URL to redirect the user to after log in, generally the
1160 1
        // page they were on before. But if it doesn't exist, they'll just go to
1161
        // the front page.
1162
        global $PAGE;
1163
1164 1
        if ($returl == "") {
1165
            $URL = new \MySociety\TheyWorkForYou\Url("home");
1166
            $returl = $URL->generate();
1167 1
        }
1168
1169
        // Various checks about the user - if they fail, we exit.
1170 1
        if ($this->user_id() == "" || $this->password == "") {
1171
            $PAGE->error_message("We don't have the user_id or password to make the cookie.", true);
1172
1173
            return;
1174
        } elseif ($this->deleted) {
1175
            $PAGE->error_message("This user has been deleted.", true);
1176 1
1177
            return;
1178
        } elseif (!$this->confirmed) {
1179
            $PAGE->error_message("You have not yet confirmed your account by clicking the link in the confirmation email we sent to you. If you don't have the email, you can <a href='/user/login/?resend=" . $this->user_id() . "'>have it resent</a>. If it still doesn't arrive, get in touch.", true);
1180
1181 1
            return;
1182 1
        }
1183 1
1184 1
        // Reminder: $this->password is actually a hashed version of the plaintext pw.
1185 1
        $cookie = $this->user_id() . "." . md5($this->password());
1186 1
1187 1
        $this->_login($returl, $expire, $cookie);
1188
    }
1189 1
1190
    private function _login($returl, $expire, $cookie, $cookie_name = 'epuser_id') {
1191 1
        // Unset any existing postcode cookie.
1192
        // This will be the postcode the user set for themselves as a non-logged-in
1193 1
        // user. We don't want it hanging around as it causes confusion.
1194
        $this->unset_postcode_cookie();
1195
1196
        twfy_debug("THEUSER", "expire is " . $expire);
1197 1
1198
        $cookie_expires = 0;
1199 1
        if ($expire == 'never') {
1200
            twfy_debug("THEUSER", "cookie never expires");
1201
            $cookie_expires = time() + 86400 * 365 * 20;
1202 1
        } elseif (is_int($expire) && $expire > time()) {
1203 1
            twfy_debug("THEUSER", "cookie expires at " . $expire);
1204 1
            $cookie_expires = $expire;
1205
        } else {
1206
            twfy_debug("THEUSER", "cookie expires with session");
1207 1
        }
1208 1
1209 1
        header("Location: $returl");
1210 1
        setcookie($cookie_name, $cookie, $cookie_expires, '/', COOKIEDOMAIN);
1211
    }
1212
1213 1
1214
    public function logout($returl) {
1215
1216
        // $returl is the URL to redirect the user to after log in, generally the
1217
        // page they were on before. But if it doesn't exist, they'll just go to
1218
        // the front page.
1219
1220
        if ($returl == '') {
1221
            $URL = new \MySociety\TheyWorkForYou\Url("home");
1222
            $returl = $URL->generate();
1223
        }
1224
1225
        // get_cookie_var() is in includes/utility.php
1226
        if (get_cookie_var("epuser_id") != "") {
1227
            // They're logged in, so set the cookie to empty.
1228
            header("Location: $returl");
1229
            setcookie('epuser_id', '', time() - 86400, '/', COOKIEDOMAIN);
1230
        }
1231
1232
        if (get_cookie_var("facebook_id") != "") {
1233
            // They're logged in, so set the cookie to empty.
1234
            header("Location: $returl");
1235
            setcookie('facebook_id', '', time() - 86400, '/', COOKIEDOMAIN);
1236
        }
1237
    }
1238
1239
    public function confirm_email($token, $redirect = true) {
1240
        $arg = '';
1241
        if (strstr($token, '::')) {
1242
            $arg = '::';
1243
        }
1244
        if (strstr($token, '-')) {
1245
            $arg = '-';
1246
        }
1247
        [$user_id, $registrationtoken] = explode($arg, $token);
1248
1249
        if (!is_numeric($user_id) || $registrationtoken == '') {
1250
            return false;
1251
        }
1252
        $q = $this->db->query("SELECT expires, data
1253
            FROM    tokens
1254
            WHERE   token = :token
1255
            AND   type = 'E'
1256
        ", [':token' => $registrationtoken])->first();
1257
1258
        if ($q) {
1259
            $expires = $q['expires'];
1260
            $expire_time = strtotime($expires);
1261
            if ($expire_time < time()) {
1262
                global $PAGE;
1263
                if ($PAGE && $redirect) {
1264
                    $PAGE->error_message("Sorry, that token seems to have expired");
1265
                }
1266
1267
                return false;
1268
            }
1269
1270
            [$user_id, $email] = explode('::', $q['data']);
1271
1272
            // if we are logged in as someone else don't change the email
1273
            if ($this->user_id() != 0 && $this->user_id() != $user_id) {
1274
                return false;
1275
            }
1276
1277
            // if the user isn't logged in then try and load the
1278
            // details
1279
            if ($this->user_id() == 0 && !$this->init($user_id)) {
1280
                return false;
1281
            }
1282
1283
            $details = [
1284
                'email' => $email,
1285
                'firstname' => $this->firstname(),
1286
                'lastname' => $this->lastname(),
1287
                'postcode' => $this->postcode(),
1288
                'url' => $this->url(),
1289
                'optin' => $this->optin(),
1290
                'user_id' => $user_id,
1291
            ];
1292
            $ret = $this->_update($details);
1293
1294
            if ($ret) {
1295
                // and remove the token to be tidy
1296
                $this->db->query("DELETE
1297
                    FROM    tokens
1298
                    WHERE   token = :token
1299
                    AND   type = 'E'
1300
                ", [':token' => $registrationtoken]);
1301
1302
                $this->email = $email;
1303
1304
                # Check Stripe email
1305
                $subscription = new MySociety\TheyWorkForYou\Subscription($this);
1306
                if ($subscription->stripe) {
1307
                    $subscription->update_email($email);
1308
                }
1309
1310
                $URL = new \MySociety\TheyWorkForYou\Url('userconfirmed');
1311
                $URL->insert(['email' => 't']);
1312
                $redirecturl = $URL->generate();
1313
                if ($redirect) {
1314
                    $this->login($redirecturl, 'session');
1315
                } else {
1316
                    return true;
1317
                }
1318
            } else {
1319
                return false;
1320
            }
1321
        } else {
1322
            return false;
1323
        }
1324
1325
    }
1326
1327
    public function confirm($token) {
1328
        // The user has clicked the link in their confirmation email
1329
        // and the confirm page has passed the token from the URL to here.
1330
        // If all goes well they'll be confirmed and then logged in.
1331
1332
        // Split the token into its parts.
1333
        $arg = '';
1334
        if (strstr($token, '::')) {
1335
            $arg = '::';
1336
        }
1337
        if (strstr($token, '-')) {
1338
            $arg = '-';
1339
        }
1340
        [$user_id, $registrationtoken] = explode($arg, $token);
1341
1342
        if (!is_numeric($user_id) || $registrationtoken == '') {
1343
            return false;
1344
        }
1345
1346
        $q = $this->db->query("SELECT email, password, postcode
1347
                        FROM    users
1348
                        WHERE   user_id = :user_id
1349
                        AND     registrationtoken = :token
1350
                        ", [
1351
            ':user_id' => $user_id,
1352
            ':token' => $registrationtoken,
1353
        ])->first();
1354
1355
        if ($q) {
1356
1357
            // We'll need these to be set before logging the user in.
1358
            $this->user_id  = $user_id;
1359
            $this->email    = $q['email'];
1360
            $this->password = $q['password'];
1361
1362
            // Set that they're confirmed in the DB.
1363
            $r = $this->db->query("UPDATE users
1364
                            SET     confirmed = '1'
1365
                            WHERE   user_id = :user_id
1366
                            ", [':user_id' => $user_id]);
1367
1368
            if ($q['postcode']) {
1369
                try {
1370
                    $MEMBER = new MEMBER(['postcode' => $q['postcode'], 'house' => HOUSE_TYPE_COMMONS]);
1371
                    $pid = $MEMBER->person_id();
1372
                    # This should probably be in the ALERT class
1373
                    $this->db->query('update alerts set confirmed=1 where email = :email and criteria = :criteria', [
1374
                        ':email' => $this->email,
1375
                        ':criteria' => 'speaker:' . $pid,
1376
                    ]);
1377
                } catch (MySociety\TheyWorkForYou\MemberException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1378
                }
1379
            }
1380
1381
            if ($r->success()) {
1382
1383
                $this->confirmed = true;
1384
1385
                $redirecturl = get_http_var('ret');
1386
                if (!$redirecturl || substr($redirecturl, 0, 1) != '/') {
1387
                    // Log the user in, redirecting them to the confirm page
1388
                    // where they should get a nice welcome message.
1389
                    $URL = new \MySociety\TheyWorkForYou\Url('userconfirmed');
1390
                    $URL->insert(['welcome' => 't']);
1391
                    $redirecturl = $URL->generate();
1392
                }
1393
1394
                $this->login($redirecturl, 'session');
1395
1396
            } else {
1397
                // Couldn't set them as confirmed in the DB.
1398
                return false;
1399
            }
1400
1401
        } else {
1402
            // Couldn't find this user in the DB. Maybe the token was
1403
            // wrong or incomplete?
1404
            return false;
1405
        }
1406
    }
1407
1408
    public function confirm_without_token() {
1409
        // If we want to confirm login without a token, e.g. during
1410
        // Facebook registration
1411
        //
1412
        // Note that this doesn't login or redirect the user.
1413
1414
        twfy_debug("THEUSER", "Confirming user without token: " . $this->user_id());
1415
        $q = $this->db->query("SELECT email, password, postcode
1416
                        FROM    users
1417
                        WHERE   user_id = :user_id
1418
                        ", [
1419
            ':user_id' => $this->user_id,
1420
        ])->first();
1421 2
1422
        if ($q) {
1423
1424
            twfy_debug("THEUSER", "User with ID found to confirm: " . $this->user_id());
1425
            // We'll need these to be set before logging the user in.
1426
            $this->email    = $q['email'];
1427
1428 2
            // Set that they're confirmed in the DB.
1429
            $r = $this->db->query("UPDATE users
1430 2
                            SET     confirmed = '1'
1431
                            WHERE   user_id = :user_id
1432
                            ", [':user_id' => $this->user_id]);
1433
1434 2
            if ($q['postcode']) {
1435 2
                try {
1436 1
                    $MEMBER = new MEMBER(['postcode' => $q['postcode'], 'house' => HOUSE_TYPE_COMMONS]);
1437
                    $pid = $MEMBER->person_id();
1438
                    # This should probably be in the ALERT class
1439 1
                    $this->db->query('update alerts set confirmed=1 where email = :email and criteria = :criteria', [
1440 1
                        ':email' => $this->email,
1441
                        ':criteria' => 'speaker:' . $pid,
1442 2
                    ]);
1443 2
                } catch (MySociety\TheyWorkForYou\MemberException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1444
                }
1445
            }
1446
1447
            if ($r->success()) {
1448 2
                twfy_debug("THEUSER", "User with ID confirmed: " . $this->user_id());
1449
                $this->confirmed = true;
1450
                return true;
1451
            } else {
1452 2
                twfy_debug("THEUSER", "User with ID not confirmed: " . $this->user_id());
1453 2
                // Couldn't set them as confirmed in the DB.
1454 2
                return false;
1455 2
            }
1456 2
1457 2
        } else {
1458
            // Couldn't find this user in the DB. Maybe the token was
1459
            // wrong or incomplete?
1460
            twfy_debug("THEUSER", "User with ID not found to confirm: " . $this->user_id());
1461 2
            return false;
1462 1
        }
1463 1
    }
1464 1
1465
1466
    public function set_postcode_cookie($pc) {
1467
        // Set the user's postcode.
1468
        // Doesn't change it in the DB, as it's probably mainly for
1469
        // not-logged-in users.
1470
1471
        $this->postcode = $pc;
1472
        if (!headers_sent()) { // if in debug mode
1473
            setcookie(POSTCODE_COOKIE, $pc, time() + 7 * 86400, "/", COOKIEDOMAIN);
1474 1
        }
1475 1
1476
        twfy_debug('USER', "Set the cookie named '" . POSTCODE_COOKIE . " to '$pc' for " . COOKIEDOMAIN . " domain");
1477
    }
1478
1479 1
    public function unset_postcode_cookie() {
1480 1
        if (!headers_sent()) { // if in debug mode
1481 1
            setcookie(POSTCODE_COOKIE, '', time() - 3600, '/', COOKIEDOMAIN);
1482 1
        }
1483
    }
1484
1485 1
    // mostly here for updating from facebook where we do not need
1486
    // to confirm the email address
1487
    public function update_self_no_confirm($details) {
1488
        global $THEUSER;
1489
1490
        if ($this->isloggedin()) {
1491
            twfy_debug("THEUSER", "is logged in for update_self");
1492 1
1493
            // this is checked elsewhere but just in case we check here and
1494
            // bail out to be on the safe side
1495
            if (isset($details['email'])) {
1496
                if ($details['email'] != $this->email() && $this->email_exists($details['email'])) {
1497
                    return false;
1498
                }
1499
            }
1500
1501
            $details["user_id"] = $this->user_id;
1502
1503
            $newdetails = $this->_update($details);
1504
1505
            if ($newdetails) {
1506
                // The user's data was updated, so we'll change the object
1507
                // variables accordingly.
1508
1509
                $this->firstname        = $newdetails["firstname"];
1510
                $this->lastname         = $newdetails["lastname"];
1511
                $this->postcode         = $newdetails["postcode"];
1512
                $this->url              = $newdetails["url"];
1513
                $this->optin            = $newdetails["optin"];
1514
                $this->email            = $newdetails['email'];
1515
                if ($newdetails["password"] != "") {
1516
                    $this->password = $newdetails["password"];
1517
                }
1518
1519
                return true;
1520
            } else {
1521
                return false;
1522
            }
1523
1524
        } else {
1525
            return false;
1526
        }
1527
1528
    }
1529
1530
    public function update_self($details, $confirm_email = true) {
1531
        // If the user wants to update their details, call this function.
1532
        // It checks that they're logged in before letting them.
1533
1534
1535
        // $details is an array like that in $this->add().
1536
1537
        global $THEUSER;
1538
1539
        if ($this->isloggedin()) {
1540
1541
            // this is checked elsewhere but just in case we check here and
1542
            // bail out to be on the safe side
1543
            $email = '';
1544
            if (isset($details['email'])) {
1545
                if ($details['email'] != $this->email() && $this->email_exists($details['email'])) {
1546
                    return false;
1547
                }
1548
                $email = $details['email'];
1549
                unset($details['email']);
1550
            }
1551
            $details["user_id"] = $this->user_id;
1552
            $newdetails = $this->_update($details);
1553
1554
            // $newdetails will be an array of details if all went well,
1555
            // false otherwise.
1556
1557
            if ($newdetails) {
1558
                // The user's data was updated, so we'll change the object
1559
                // variables accordingly.
1560
1561
                $this->firstname        = $newdetails["firstname"];
1562
                $this->lastname         = $newdetails["lastname"];
1563
                $this->postcode         = $newdetails["postcode"];
1564
                $this->url              = $newdetails["url"];
1565
                $this->optin            = $newdetails["optin"];
1566
                if ($newdetails["password"] != "") {
1567
                    $this->password = $newdetails["password"];
1568
                }
1569
1570
                if ($email && $email != $this->email) {
1571
                    $token = substr(password_hash($email . microtime(), PASSWORD_BCRYPT), 29, 16);
1572
                    $data = $this->user_id() . '::' . $email;
1573
                    $r = $this->db->query("INSERT INTO tokens
1574
                        ( expires, token, type, data )
1575
                        VALUES
1576
                        (
1577
                            DATE_ADD(CURRENT_DATE(), INTERVAL 30 DAY),
1578
                            :token,
1579
                            'E',
1580
                            :data
1581
                        )
1582
                    ", [
1583
                        ':token' => $token,
1584
                        ':data' => $data,
1585
                    ]);
1586
1587
                    // send confirmation email here
1588
                    if ($r->success()) {
1589
                        $newdetails['email'] = $email;
1590
                        $newdetails['token'] = $token;
1591
                        if ($confirm_email) {
1592
                            return $this->send_email_confirmation_email($newdetails);
1593
                        } else {
1594
                            return true;
1595
                        }
1596
                    } else {
1597
                        return false;
1598
                    }
1599
                }
1600
1601
                return true;
1602
            } else {
1603
                return false;
1604
            }
1605
1606
        } else {
1607
            return false;
1608
        }
1609
1610
    }
1611
1612
}
1613
1614
// Yes, we instantiate a new global $THEUSER object when every page loads.
1615
$THEUSER = new THEUSER();
1616