• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-server / 997e0f4f-3cfe-42a9-a6a2-be212777be4a

25 May 2024 05:16PM UTC coverage: 94.869% (-0.01%) from 94.88%
997e0f4f-3cfe-42a9-a6a2-be212777be4a

push

circleci

edwh
Test fixes.

25425 of 26800 relevant lines covered (94.87%)

31.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.63
/include/session/Session.php
1
<?php
2
namespace Freegle\Iznik;
3

4
use Firebase\JWT\JWT;
5

6
$sessionPrepared = FALSE;
7

8
class Session {
9
    # Based on http://stackoverflow.com/questions/244882/what-is-the-best-way-to-implement-remember-me-for-a-website
10
    private $dbhr;
11
    private $dbhm;
12
    private $id;
13

14
    public static function modtools() {
15
        # Whether we are in ModTools or Freegle Direct.  For API calls this is passed as a request parameter.
16
        if (isset($_REQUEST) && array_key_exists('modtools', $_REQUEST)) {
576✔
17
            return filter_var($_REQUEST['modtools'], FILTER_VALIDATE_BOOLEAN);
575✔
18
        } else {
19
            # Default TRUE for scripts.
20
            return TRUE;
1✔
21
        }
22
    }
23

24
    public static function prepareSession($dbhr, $dbhm) {
25
        # We only want to do the prepare once, otherwise we will generate many headers.
26

27
        # Backbone may send requests wrapped insode a model; extract them.
28
        if (array_key_exists ( 'model', $_REQUEST )) {
593✔
29
            $_REQUEST = array_merge ( $_REQUEST, json_decode ( $_REQUEST ['model'], true ) );
×
30
        }
31

32
        if (!Utils::pres('sessionPrepared', $GLOBALS)) {
593✔
33
            $GLOBALS['sessionPrepared'] = TRUE;
593✔
34

35
            if (Utils::pres('api_key', $_REQUEST)) {
593✔
36
                # We have been passed a session id.
37
                #
38
                # One example of this is when we are called from Swagger.
39
                session_id($_REQUEST['api_key']);
×
40
            }
41

42
            # The IOS app needs to be able to set the session id.  This is as secure as the Cookie header would be.
43
            $headers = Session::getallheaders();
593✔
44
            $sessid = Utils::presdef('X-Iznik-Php-Session', $headers, NULL);
593✔
45
            if ($sessid && strlen($sessid) >= 26 && ctype_alnum($sessid)) {
593✔
46
                @session_destroy();
1✔
47
                session_id($sessid);
1✔
48
            }
49

50
            # We need to also be prepared to do a session_start here, because if we're running in the UT then the session_start
51
            # above will happen once at the start of the test, when the script is first included, and we will later on destroy
52
            # it.
53
            #error_log("prepare " . isset($_SESSION) . " id " . session_id());
54
            if (session_status() == PHP_SESSION_NONE) {
593✔
55
                # Don't return PHP session cookie.  There is a risk of conflict between different servers, and our
56
                # persistent/JWT code means we don't need it.
57
                ini_set('session.use_cookies', '0');
1✔
58
                @setcookie('PHPSESSID', '', -1, '/');
1✔
59
                @session_start();
1✔
60
            }
61

62
            # We might have a partner key which allows us access to the API when not logged in as a user.
63
            $_SESSION['partner'] = FALSE;
593✔
64

65
            if (Utils::pres('partner', $_REQUEST)) {
593✔
66
                list ($partner, $domain) = Session::partner($dbhr, $_REQUEST['partner']);
14✔
67
                $_SESSION['partner'] = $partner;
14✔
68
                $_SESSION['partnerdomain'] = $domain;
14✔
69
            }
70

71
            # Always verify the persistent session if passed.  This guards against
72
            # session id collisions, which can happen (albeit quite rarely).
73
            $cookie = Utils::presdef('persistent', $_REQUEST, NULL);
593✔
74

75
            if (!$cookie) {
593✔
76
                # Check headers too.
77
                $headers = Session::getallheaders();
592✔
78
                if (Utils::pres('Authorization', $headers)) {
592✔
79
                    $auth = $headers['Authorization'];
5✔
80

81
                    if (strpos($auth, 'Iznik ') === 0) {
5✔
82
                        $cookie = json_decode(substr($auth, 6), TRUE);
5✔
83
                    }
84
                }
85
            }
86

87
            if ($cookie) {
593✔
88
                # Check our cookie to see if it's a valid session
89
                #error_log("Cookie " . var_export($cookie, TRUE));
90

91
                if ((array_key_exists('id', $cookie)) &&
8✔
92
                    (array_key_exists('series', $cookie)) &&
8✔
93
                    (array_key_exists('token', $cookie))
8✔
94
                ) {
95
                    $sesscook = Utils::presdef('persistent', $_SESSION, NULL);
8✔
96
                    #error_log("Check vs " . var_export($sesscook, TRUE));
97

98
                    if (Utils::pres('id', $_SESSION) && Utils::pres('userid', $cookie) && Utils::pres('userid', $cookie) != $_SESSION['id']) {
8✔
99
                        # The cookie we have been passed doesn't match the one we are logged in as.  That's a bit
100
                        # worrying.  Log it, and don't switch.
101
                        $msg = "Persistent session cookie doesn't match logged in user; logged in as {$_SESSION['id']} passed " . json_encode($cookie);
1✔
102
                        \Sentry\captureMessage($msg);
1✔
103
                        error_log($msg);
1✔
104
                    } else if (!Utils::presdef('id', $_SESSION, NULL) || $sesscook != $cookie) {
8✔
105
                        # We are not logged in as the correct user (or at all).  Try to switch to the persistent one.
106
                        #error_log("Logged in wrongly as " . var_export($sesscook, TRUE) . " when should be " . var_export($cookie, TRUE));
107
                        $_SESSION['id'] = NULL;
8✔
108
                        $s = new Session($dbhr, $dbhm);
8✔
109
                        $s->verify($cookie['id'], $cookie['series'], $cookie['token']);
8✔
110
                    }
111
                }
112
            }
113

114
            if (!Utils::pres('id', $_SESSION)) {
593✔
115
                # We might not have a cookie, but we might have push credentials.  This happens when we are logged out
116
                # on the client but get a notification.  That is sufficient to log us in.
117
                $pushcreds = Utils::presdef('pushcreds', $_REQUEST, NULL);
593✔
118
                #error_log("No session, pushcreds $pushcreds " . var_exporT($_REQUEST, TRUE));
119
                if ($pushcreds) {
593✔
120
                    $sql = "SELECT * FROM users_push_notifications WHERE subscription = ?;";
2✔
121
                    $pushes = $dbhr->preQuery($sql, [$pushcreds]);
2✔
122
                    foreach ($pushes as $push) {
2✔
123
                        $s = new Session($dbhr, $dbhm);
1✔
124
                        #error_log("Log in as {$push['userid']}");
125
                        $s->create($push['userid']);
1✔
126
                    }
127
                }
128
            }
129
        }
130
    }
131

132
    public static function getallheaders() {
133
        $headers = [];
619✔
134
        foreach ($_SERVER as $name => $value) {
619✔
135
            if (substr($name, 0, 5) == 'HTTP_') {
619✔
136
                $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
619✔
137
            }
138
        }
139
        return $headers;
619✔
140
    }
141

142
    public static function partner($dbhr, $key) {
143
        $ret = FALSE;
17✔
144
        $domain = NULL;
17✔
145

146
        $partners = $dbhr->preQuery("SELECT * FROM partners_keys WHERE `key` = ?;", [ $key ]);
17✔
147
        foreach ($partners as $partner) {
17✔
148
            $ret = $partner;
13✔
149
            $domain = $partner['domain'];
13✔
150
        }
151

152
        return([$ret, $domain]);
17✔
153
    }
154

155
    public static function whoAmId(LoggedPDO $dbhr, LoggedPDO $dbhm) {
156
        Session::prepareSession($dbhr, $dbhm);
592✔
157

158
        $id = Utils::presdef('id', $_SESSION, NULL);
592✔
159

160
        if ($id) {
592✔
161
            # Add context for Sentry exceptions.
162
            \Sentry\configureScope(function (\Sentry\State\Scope $scope) use ($id): void {
290✔
163
                $scope->setUser(['id' => $id]);
290✔
164
            });
290✔
165
        }
166
        return $id;
592✔
167
    }
168

169
    public static function whoAmI(LoggedPDO $dbhr, $dbhm)
170
    {
171
        $id = self::whoAmId($dbhr, $dbhm);
584✔
172
        $ret = NULL;
584✔
173
        #error_log("Session::whoAmI $id in " . session_id());
174

175
        if ($id) {
584✔
176
            # We are logged in.  Get our details
177
            $r = User::get($dbhr, $dbhm, $id);
286✔
178

179
            if ($r->getId() == $id) {
286✔
180
                #error_log("Return cached user $id");
181
                $ret = $r;
286✔
182
            }
183
            #error_log("Found " . $ret->getId() . " role " . $ret->isModerator());
184
        }
185

186
        return($ret);
584✔
187
    }
188

189
    public static function clearSessionCache() {
190
        # We cache some information.
191
        $_SESSION['notification'] = [];
454✔
192
        $_SESSION['modorowner'] = [];
454✔
193
    }
194

195
    public function getUserId() {
196
        $sql = "SELECT userid FROM sessions WHERE id = ?;";
2✔
197
        $sessions = $this->dbhr->preQuery($sql, [ $this->id ]);
2✔
198
        $ret = NULL;
2✔
199
        foreach ($sessions as $session) {
2✔
200
            $ret = $session['userid'];
2✔
201
        }
202

203
        return($ret);
2✔
204
    }
205

206
    function __construct(LoggedPDO $dbhr, LoggedPDO $dbhm) {
207
        $this->dbhr = $dbhr;
285✔
208
        $this->dbhm = $dbhm;
285✔
209
    }
210

211
    public function create($userid) {
212
        # Spammers can't create sessions, but we make it look as though they did.
213
        $s = new Spam($this->dbhr, $this->dbhm);
283✔
214

215
        if (!$s->isSpammerUid($userid, Spam::TYPE_SPAMMER)) {
283✔
216
            # If we wanted to only allow login from a single device/browser, we'd destroy cookies at this point.  But
217
            # we want to allow login on as many devices as the user wants.  We want to leave any existing sessions around
218
            # so that if they are used later on by other clients, they'll still work.  This can happen if they're stored
219
            # in local storage - we can get different sessions on different clients, and unless we allow them all, one
220
            # device can effectively log another one out.  They get tidied up via a cron script.
221
            # TODO SHA1 is no longer brilliantly secure.
222
            $series = Utils::devurandom_rand();
282✔
223
            $token  = Utils::devurandom_rand();
282✔
224
            $thash  = sha1($token);
282✔
225

226
            $sql = "INSERT INTO sessions (`userid`, `series`, `token`) VALUES (?,?,?) ON DUPLICATE KEY UPDATE id=LAST_INSERT_ID(id);";
282✔
227

228
            $this->dbhm->preExec($sql, [
282✔
229
                $userid,
282✔
230
                $series,
282✔
231
                $thash
282✔
232
            ]);
282✔
233

234
            $id = $this->dbhm->lastInsertId();
282✔
235
            $this->id = $id;
282✔
236

237
            $_SESSION['id'] = $userid;
282✔
238
            #error_log("Created session for $userid in " . session_id());
239
            $_SESSION['logged_in'] = TRUE;
282✔
240

241
            $_SESSION['persistent'] = [
282✔
242
                'id' => $id,
282✔
243
                'series' => $series,
282✔
244
                'token' => $thash,
282✔
245
                'userid' => $userid
282✔
246
            ];
282✔
247

248
            return ($_SESSION['persistent']);
282✔
249
        }
250

251
        return NULL;
1✔
252
    }
253

254
    public function verify($id, $series, $token) {
255
        # Look for a session.
256
        $sql = "SELECT * FROM sessions WHERE id = ? AND series = ? AND token = ?;";
9✔
257
        $sessions = $this->dbhr->preQuery($sql, [
9✔
258
            $id,
9✔
259
            $series,
9✔
260
            $token
9✔
261
        ]);
9✔
262

263
        #error_log("SELECT * FROM sessions WHERE id = $id AND series = $series AND token = '$token';");
264

265
        /** @noinspection PhpUnusedLocalVariableInspection */
266
        foreach ($sessions as $session) {
9✔
267
            # Leave the cookie in existence, marked as active.  This is a bit less secure, but it does mean that we
268
            # can frequently use the cookie to recover a session when the PHP session is no longer there, including
269
            # from multiple devices.
270
            $userid = $session['userid'];
4✔
271
            $_SESSION['id'] = $userid;
4✔
272
            $_SESSION['logged_in'] = TRUE;
4✔
273

274
            # Store this away in our PHP session, so that it gets returned the client, and will then work again
275
            # next time.
276
            $_SESSION['persistent'] = [
4✔
277
                'id' => $id,
4✔
278
                'series' => $series,
4✔
279
                'token' => $token,
4✔
280
                'userid' => $userid
4✔
281
            ];
4✔
282

283
            # Update the last access time, unless we have done so recently.
284
            $age = time() - strtotime($session['lastactive']);
4✔
285
            if ($age > 600) {
4✔
286
                $this->dbhm->preExec("UPDATE sessions SET lastactive = NOW() WHERE  id = ? AND series = ? AND token = ?;", [
×
287
                    $id,
×
288
                    $series,
×
289
                    $token
×
290
                ]);
×
291
            }
292

293
            return($userid);
4✔
294
        }
295

296
        # We failed to verify.  Some systems would zap the existing sessions here in case there was a theft, but
297
        # this means that a bad cookie on one device will log out other devices, which can create a ping-pong
298
        # from which you don't recover.
299

300
        return(NULL);
7✔
301
    }
302

303
    public function destroy($userid, $series) {
304
        # Deleting the cookie will mean that we can no longer use this cookie to sign in on any device - which means
305
        # that if you log out on one device, the others will get logged out too (once the PHP session goes, anyway).
306
        #error_log(var_export($this->dbhr, true));
307
        if ($userid) {
3✔
308
            # If we're doing an explicit logout we're called with a null $series and want to zap all sessions for this
309
            # user.  Otherwise we only want to delete the session with this series, otherwise a failed login for this
310
            # user would log out other users.
311
            $sql = $series ? "DELETE FROM sessions WHERE userid = ? AND series = ?;" : "DELETE FROM sessions WHERE userid = ?;";
3✔
312
            $parms = $series ? [ $userid, $series ] : [ $userid ];
3✔
313
            $this->dbhm->preExec($sql, $parms);
3✔
314

315
            $l = new Log($this->dbhr, $this->dbhm);
3✔
316
            $l->log([
3✔
317
                'type' => Log::TYPE_USER,
3✔
318
                'subtype' => Log::SUBTYPE_LOGOUT,
3✔
319
                'byuser' => $userid,
3✔
320
                'text' => "Series $series"
3✔
321
            ]);
3✔
322
        }
323

324
        $_SESSION['id'] = NULL;
3✔
325
        $_SESSION['logged_in'] = FALSE;
3✔
326
    }
327

328
    public static function JWT($dbhr, $dbhm) {
329
        $ret = NULL;
35✔
330

331
        # Generate a JWT for this user.
332
        $id = Session::whoAmId($dbhr, $dbhm);
35✔
333

334
        $privateKey = trim(file_get_contents('/etc/iznik_jwt_secret'));
35✔
335

336
        if ($privateKey) {
35✔
337
            // Create a JWT.  This can be very long-lived because the Go API will check it against the database.  We
338
            // need to do that check in order to be able to force logout.
339
            $ret = JWT::encode([
35✔
340
                'id' => $id . "",
35✔
341
                'sessionid' => $_SESSION['persistent']['id'] . "",
35✔
342
                'exp' => (time() + 365 * 30 * 24 * 60 * 60)
35✔
343
           ],
35✔
344
           $privateKey,
35✔
345
           'HS256');
35✔
346
        }
347

348
        return $ret;
35✔
349
    }
350
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc