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

Freegle / iznik-server / #2540

17 Dec 2025 10:43AM UTC coverage: 89.531% (-0.7%) from 90.259%
#2540

push

php-coveralls

edwh
Fix flaky EngageTest by cleaning up engage table in setUp

The engage table tracks when users were sent engagement emails and
prevents re-sending within 7 days. This caused test isolation issues
when test users from previous runs still had entries.

Clean up engage table entries for test users at the start of each test.

26596 of 29706 relevant lines covered (89.53%)

31.64 hits per line

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

84.04
/include/API.php
1
<?php
2

3
namespace Freegle\Iznik;
4

5

6
require_once(IZNIK_BASE . '/include/db.php');
1✔
7
require_once(IZNIK_BASE . '/include/misc/Loki.php');
1✔
8
global $dbhr, $dbhm;
1✔
9

10
use GeoIp2\Database\Reader;
11

12
class API
13
{
14
    public static function headers() {
15
        $call = array_key_exists('call', $_REQUEST) ? $_REQUEST['call'] : NULL;
329✔
16
        $type = array_key_exists('type', $_REQUEST) ? $_REQUEST['type'] : 'GET';
329✔
17

18
        // We allow anyone to use our API.
19
        //
20
        // Suppress errors on the header command for UT
21
        if (!(($call == 'image' || $call == 'profile') && $type == 'GET')) {
329✔
22
            # For images we'll set the content type later.
23
            @header('Content-type: application/json');
328✔
24
        }
25

26
        // Access-Control-Allow-Origin not now added by nginx.
27
        @header('Access-Control-Allow-Origin: *');
329✔
28
        @header('Access-Control-Allow-Headers: ' . (array_key_exists('HTTP_ACCESS_CONTROL_REQUEST_HEADERS', $_SERVER) ? $_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] : "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Iznik-PHP-Session")); // X-HTTP-Method-Override not needed
329✔
29
        @header('Access-Control-Allow-Credentials: TRUE');
329✔
30
        @header('P3P:CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"');
329✔
31
    }
32

33
    public static function call()
34
    {
35
        global $dbhr, $dbhm;
329✔
36

37
        $ip = Utils::presdef('REMOTE_ADDR', $_SERVER, '');
329✔
38

39
        if (file_exists("/tmp/noheaders-$ip")) {
329✔
40
            echo json_encode(array('ret' => 1, 'status' => 'Not  logged in'));
×
41
            exit(0);
×
42
        }
43

44
        $scriptstart = microtime(TRUE);
329✔
45

46
        $entityBody = file_get_contents('php://input');
329✔
47

48
        if ($entityBody) {
329✔
49
            $parms = json_decode($entityBody, TRUE);
×
50
            if (json_last_error() == JSON_ERROR_NONE) {
×
51
                # We have been passed parameters in JSON.
52
                foreach ($parms as $parm => $val) {
×
53
                    $_REQUEST[$parm] = $val;
×
54
                }
55
            } else {
56
                # In some environments (e.g. PHP-FPM 7.2) parameters passed as an encoded form for PUT aren't parsed correctly,
57
                # probably because we have a rewrite that adds a parameter to the URL, and PHP-FPM doesn't want to have both
58
                # URL and form parameters.
59
                #
60
                # This may be a bug or a feature, but it messes us up.  So decode anything we can find that has not already
61
                # been decoded by our interpreter (if it did it, it's likely to be better).
62
                #
63
                # We needed this code when the app didn't contain the use of HTTP_X_HTTP_METHOD_OVERRIDE, and it's useful
64
                # anyway in case the client forgets.
65
                parse_str($entityBody, $params);
×
66
                foreach ($params as $key => $val) {
×
67
                    if (!array_key_exists($key, $_REQUEST)) {
×
68
                        $_REQUEST[$key] = $val;
×
69
                    }
70
                }
71
            }
72
        }
73

74
        if (array_key_exists('REQUEST_METHOD', $_SERVER)) {
329✔
75
            $_SERVER['REQUEST_METHOD'] = strtoupper($_SERVER['REQUEST_METHOD']);
329✔
76
            $_REQUEST['type'] = $_SERVER['REQUEST_METHOD'];
329✔
77
        }
78

79
        if (array_key_exists('HTTP_X_HTTP_METHOD_OVERRIDE', $_SERVER)) {
329✔
80
            # Used by Backbone's emulateHTTP to work around servers which don't handle verbs like PATCH very well.
81
            #
82
            # We use this because when we issue a PATCH we don't seem to be able to get the body parameters.
83
            $_REQUEST['type'] = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
1✔
84
            #error_log("Request method override to {$_REQUEST['type']}");
85
        }
86

87
        // Record timing.
88
        list ($tusage, $rusage) = API::requestStart();
329✔
89

90
        API::headers();
329✔
91

92
        // @codeCoverageIgnoreStart
93
        if (file_exists(IZNIK_BASE . '/http/maintenance_on.html')) {
94
            echo json_encode(array('ret' => 111, 'status' => 'Down for maintenance'));
95
            exit(0);
96
        }
97
        // @codeCoverageIgnoreEnd
98

99
        # Check if IP is blocked
100
        $ip = Utils::presdef('REMOTE_ADDR', $_SERVER, '');
329✔
101
        $ipBlocker = new IPBlocker($dbhr, $dbhm);
329✔
102

103
        if ($ipBlocker->isBlocked($ip)) {
329✔
104
            echo json_encode(array(
×
105
                'ret' => 403,
×
106
                'status' => 'Access denied'
×
107
            ));
×
108
            exit(0);
×
109
        }
110

111
        $includetime = microtime(TRUE) - $scriptstart;
329✔
112

113
        # All API calls come through here.
114
        #error_log("Request " . var_export($_REQUEST, TRUE));
115
        #error_log("Server " . var_export($_SERVER, TRUE));
116

117
        if (Utils::pres('HTTP_X_REAL_IP', $_SERVER)) {
329✔
118
            # We jump through hoops to get the real IP address. This is one of them.
119
            $_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_REAL_IP'];
×
120
        }
121

122
        if (array_key_exists('model', $_REQUEST)) {
329✔
123
            # Used by Backbone's emulateJSON to work around servers which don't handle requests encoded as
124
            # application/json.
125
            $_REQUEST = array_merge($_REQUEST, json_decode($_REQUEST['model'], TRUE));
1✔
126
            unset($_REQUEST['model']);
1✔
127
        }
128

129
        # Include the API call
130
        $call = Utils::pres('call', $_REQUEST);
329✔
131

132
        if ($call) {
329✔
133
            $fn = IZNIK_BASE . '/http/api/' . $call . '.php';
329✔
134
            if (file_exists($fn)) {
329✔
135
                require_once($fn);
322✔
136
            }
137
        }
138

139
        if (Utils::presdef('type', $_REQUEST, null) == 'OPTIONS') {
329✔
140
            # We don't bother returning different values for different calls.
141
            http_response_code(204);
1✔
142
            @header('Allow: POST, GET, DELETE, PUT');
1✔
143
            @header('Access-Control-Allow-Methods:  POST, GET, DELETE, PUT');
1✔
144
        } else {
145
            # Actual API calls
146
            $ret = array('ret' => 1000, 'status' => 'Invalid API call');
328✔
147
            $t = microtime(TRUE);
328✔
148

149
            # We wrap the whole request in a retry handler.  This is so that we can deal with errors caused by
150
            # conflicts within the Percona cluster.
151
            $apicallretries = 0;
328✔
152

153
            # This is an optimisation for User.php.
154
            if (session_status() !== PHP_SESSION_NONE) {
328✔
155
                $_SESSION['modorowner'] = Utils::presdef('modorowner', $_SESSION, []);
328✔
156
            }
157

158
            $encoded_ret = NULL;
328✔
159

160
            do {
161
                if (Utils::presdef('type', $_REQUEST, null) != 'GET') {
328✔
162
                    # Check that we're not posting from a blocked country.
163
                    try {
164
                        $reader = new Reader(MMDB);
249✔
165
                        $ip = Utils::presdef('REMOTE_ADDR', $_SERVER, null);
249✔
166

167
                        if ($ip) {
249✔
168
                            $record = $reader->country($ip);
166✔
169
                            $country = $record->country->name;
×
170
                            # Failed to look it up.
171
                            $countries = $dbhr->preQuery("SELECT * FROM spam_countries WHERE country = ?;", [$country]);
×
172
                            foreach ($countries as $country) {
83✔
173
                                error_log("Block post from {$country['country']} " . var_export($_REQUEST, TRUE));
×
174
                                $encoded_ret = json_encode(array('ret' => 0, 'status' => 'Success'));
×
175
                                echo $encoded_ret;
×
176
                                break 2;
×
177
                            }
178
                        }
179
                    } catch (\Exception $e) {
166✔
180
                        // Don't log to Sentry - this can happen validly for some IPs (e.g. on CircleCI).
181
                    }
182
                }
183

184
                # Duplicate protection.  We upload multiple images so don't protect against those.
185
                if ((DUPLICATE_POST_PROTECTION > 0) &&
328✔
186
                    array_key_exists('REQUEST_METHOD', $_SERVER) &&
328✔
187
                    ((Utils::presdef('type', $_REQUEST, null) == 'POST' || Utils::presdef('type', $_REQUEST, null) == 'PUT')) &&
328✔
188
                    $call != 'image') {
328✔
189
                    # We want to make sure that we don't get duplicate POST requests within the same session.  We can't do this
190
                    # using information stored in the session because when Redis is used as the session handler, there is
191
                    # no session locking, and therefore two requests in quick succession could be allowed.  So instead
192
                    # we use Redis directly with a roll-your-own mutex.  We use the IP address as a key - if there are
193
                    # multiple sessions from the same IP then we might get FALSE positives/negatives.
194
                    $reqData = $_REQUEST;
204✔
195
                    unset($reqData['requestid']);
204✔
196
                    $reqData['call'] = preg_replace('/(\?|&)requestid=[0-9]+?/', '', $reqData['call']);
204✔
197
                    $url = preg_replace('/(\?|&)requestid=[0-9]+?/', '', $_SERVER['REQUEST_URI']);
204✔
198
                    $req = $url . serialize($reqData);
204✔
199
                    $ip = Utils::presdef('REMOTE_ADDR', $_SERVER, NULL);
204✔
200
                    $lockkey = "POST_LOCK_$ip";
204✔
201
                    $datakey = "POST_DATA_$ip";
204✔
202
                    $uid = uniqid('', TRUE);
204✔
203
                    $predis = new \Redis();
204✔
204
                    $predis->pconnect(REDIS_CONNECT);
204✔
205

206
                    # Get a lock.
207
                    $start = time();
204✔
208
                    $count = 0;
204✔
209

210
                    do {
211
                        $rc = $predis->setNx($lockkey, $uid);
204✔
212

213
                        if ($rc) {
204✔
214
                            # We managed to set it.  Ideally we would set an expiry time to make sure that if we got
215
                            # killed right now, this session wouldn't hang.  But that's an extra round trip on each
216
                            # API call, and the worst case would be a single session hanging, which we can live with.
217

218
                            # Sound out the last POST.
219
                            $last = $predis->get($datakey);
204✔
220

221
                            # Some actions are ok, so we exclude those.
222
                            if (!in_array($call, ['session', 'correlate', 'chatrooms', 'upload']) &&
204✔
223
                                $last ==  $req) {
204✔
224
                                # The last POST request was the same.  So this is a duplicate.
225
                                $predis->del($lockkey);
1✔
226
                                $ret = array(
1✔
227
                                    'ret' => 999,
1✔
228
                                    'text' => 'Duplicate request - rejected.',
1✔
229
                                    'data' => $_REQUEST
1✔
230
                                );
1✔
231
                                $encoded_ret = json_encode($ret);
1✔
232
                                echo $encoded_ret;
1✔
233
                                break 2;
1✔
234
                            }
235

236
                            # The last request wasn't the same.  Save this one.
237
                            $predis->set($datakey, $req);
204✔
238
                            $predis->expire($datakey, DUPLICATE_POST_PROTECTION);
204✔
239

240
                            # We're good to go - release the lock.
241
                            $predis->del($lockkey);
204✔
242
                            break;
204✔
243
                            // @codeCoverageIgnoreStart
244
                        } else {
245
                            # We didn't get the lock - another request for this session must have it.
246
                            $count++;
247
                            #error_log("Sleep for duplicate POST lock key $lockkey attempt $count, waited " . (time() - $start));
248
                            usleep(100000);
249
                        }
250
                    } while (time() < $start + 45);
251
                    // @codeCoverageIgnoreEnd
252
                }
253

254
                try {
255
                    # Each call is inside a file with a suitable name.
256
                    #
257
                    # call_user_func doesn't scale well on multicores with some versions of PHP, so we need can't figure out the function from
258
                    # the call name - use a switch instead.
259
                    switch ($call) {
260
                        case 'abtest':
328✔
261
                            $ret = abtest();
1✔
262
                            break;
1✔
263
                        case 'activity':
327✔
264
                            $ret = activity();
1✔
265
                            break;
1✔
266
                        case 'authority':
326✔
267
                            $ret = authority();
1✔
268
                            break;
1✔
269
                        case 'address':
325✔
270
                            $ret = address();
3✔
271
                            break;
3✔
272
                        case 'alert':
323✔
273
                            $ret = alert();
1✔
274
                            break;
1✔
275
                        case 'adview':
322✔
276
                            # Remove after 2021-05-05.
277
                            $ret = [
×
278
                                'ret' => 0,
×
279
                                'status' => 'Success',
×
280
                                'data' => []
×
281
                            ];
×
282
                            break;
×
283
                        case 'admin':
322✔
284
                            $ret = admin();
4✔
285
                            break;
4✔
286
                        case 'config':
319✔
287
                            $ret = config();
1✔
288
                            break;
1✔
289
                        case 'changes':
318✔
290
                            $ret = changes();
1✔
291
                            break;
1✔
292
                        case 'dashboard':
318✔
293
                            $ret = dashboard();
5✔
294
                            break;
5✔
295
                        case 'error':
315✔
296
                            $ret = error();
1✔
297
                            break;
1✔
298
                        case 'export':
314✔
299
                            $ret = export();
1✔
300
                            break;
1✔
301
                        case 'exception':
313✔
302
                            # For UT
303
                            throw new \Exception();
1✔
304
                        case 'image':
313✔
305
                            $ret = image();
26✔
306
                            break;
26✔
307
                        case 'isochrone':
293✔
308
                            $ret = isochrone();
2✔
309
                            break;
2✔
310
                        case 'jobs':
293✔
311
                            $ret = jobs();
1✔
312
                            break;
1✔
313
                        case 'profile':
292✔
314
                            $ret = profile();
1✔
315
                            break;
1✔
316
                        case 'socialactions':
291✔
317
                            $ret = socialactions();
×
318
                            break;
×
319
                        case 'messages':
291✔
320
                            $ret = messages();
25✔
321
                            break;
25✔
322
                        case 'message':
277✔
323
                            $ret = message();
75✔
324
                            break;
75✔
325
                        case 'invitation':
215✔
326
                            $ret = invitation();
1✔
327
                            break;
1✔
328
                        case 'item':
214✔
329
                            $ret = item();
4✔
330
                            break;
4✔
331
                        case 'usersearch':
210✔
332
                            $ret = usersearch();
1✔
333
                            break;
1✔
334
                        case 'memberships':
209✔
335
                            $ret = memberships();
30✔
336
                            break;
30✔
337
                        case 'merge':
184✔
338
                            $ret = merge();
2✔
339
                            break;
2✔
340
                        case 'spammers':
182✔
341
                            $ret = spammers();
4✔
342
                            break;
4✔
343
                        case 'session':
179✔
344
                            $ret = session();
50✔
345
                            break;
50✔
346
                        case 'simulation':
144✔
347
                            $ret = simulation();
9✔
348
                            break;
9✔
349
                        case 'group':
135✔
350
                            $ret = group();
9✔
351
                            break;
9✔
352
                        case 'groups':
126✔
353
                            $ret = groups();
1✔
354
                            break;
1✔
355
                        case 'communityevent':
125✔
356
                            $ret = communityevent();
2✔
357
                            break;
2✔
358
                        case 'domains':
123✔
359
                            $ret = domains();
1✔
360
                            break;
1✔
361
                        case 'locations':
122✔
362
                            $ret = locations();
5✔
363
                            break;
5✔
364
                        case 'logo':
117✔
365
                            $ret = logo();
1✔
366
                            break;
1✔
367
                        case 'modconfig':
116✔
368
                            $ret = modconfig();
10✔
369
                            break;
10✔
370
                        case 'stdmsg':
113✔
371
                            $ret = stdmsg();
3✔
372
                            break;
3✔
373
                        case 'bulkop':
110✔
374
                            $ret = bulkop();
4✔
375
                            break;
4✔
376
                        case 'comment':
106✔
377
                            $ret = comment();
4✔
378
                            break;
4✔
379
                        case 'user':
102✔
380
                            $ret = user();
29✔
381
                            break;
29✔
382
                        case 'chatrooms':
77✔
383
                            $ret = chatrooms();
19✔
384
                            break;
19✔
385
                        case 'chatmessages':
71✔
386
                            $ret = chatmessages();
16✔
387
                            break;
16✔
388
                        case 'poll':
55✔
389
                            $ret = poll();
2✔
390
                            break;
2✔
391
                        case 'request':
53✔
392
                            $ret = request();
1✔
393
                            break;
1✔
394
                        case 'shortlink':
52✔
395
                            $ret = shortlink();
2✔
396
                            break;
2✔
397
                        case 'stories':
50✔
398
                            $ret = stories();
3✔
399
                            break;
3✔
400
                        case 'donations':
48✔
401
                            $ret = donations();
2✔
402
                            break;
2✔
403
                        case 'giftaid':
46✔
404
                            $ret = giftaid();
4✔
405
                            break;
4✔
406
                        case 'status':
42✔
407
                            $ret = status();
1✔
408
                            break;
1✔
409
                        case 'volunteering':
41✔
410
                            $ret = volunteering();
4✔
411
                            break;
4✔
412
                        case 'logs':
37✔
413
                            $ret = logs();
1✔
414
                            break;
1✔
415
                        case 'newsfeed':
36✔
416
                            $ret = newsfeed();
14✔
417
                            break;
14✔
418
                        case 'noticeboard':
24✔
419
                            $ret = noticeboard();
3✔
420
                            break;
3✔
421
                        case 'notification':
22✔
422
                            $ret = notification();
3✔
423
                            break;
3✔
424
                        case 'mentions':
19✔
425
                            $ret = mentions();
1✔
426
                            break;
1✔
427
                        case 'microvolunteering':
18✔
428
                            $ret = microvolunteering();
5✔
429
                            break;
5✔
430
                        case 'team':
13✔
431
                            $ret = team();
1✔
432
                            break;
1✔
433
                        case 'tryst':
12✔
434
                            $ret = tryst();
4✔
435
                            break;
4✔
436
                        case 'src':
8✔
437
                            $ret = src();
1✔
438
                            break;
1✔
439
                        case 'visualise':
7✔
440
                            $ret = visualise();
1✔
441
                            break;
1✔
442
                        case 'stripecreateintent':
6✔
443
                            $ret = stripecreateintent();
×
444
                            break;
×
445
                        case 'stripecreatesubscription':
6✔
446
                            $ret = stripecreatesubscription();
×
447
                            break;
×
448
                        case 'echo':
6✔
449
                            $ret = array_merge($_REQUEST, $_SERVER);
2✔
450
                            break;
2✔
451
                        case 'DBexceptionWork':
4✔
452
                            # For UT
453
                            if ($apicallretries < 2) {
1✔
454
                                error_log("Fail DBException $apicallretries");
1✔
455
                                throw new DBException();
1✔
456
                            }
457

458
                            break;
1✔
459
                        case 'DBexceptionFail':
4✔
460
                            # For UT
461
                            throw new DBException();
1✔
462
                        case 'DBleaveTrans':
3✔
463
                            # For UT
464
                            $dbhm->beginTransaction();
1✔
465

466
                            break;
1✔
467
                    }
468

469
                    # If we get here, everything worked.
470
                    if ($call == 'upload') {
328✔
471
                        # Output is handled within the lib.
472
                    } else {
473
                        if (Utils::pres('redirectto', $ret)) {
328✔
474
                            header("Location: {$ret['redirectto']}", TRUE, 302);
1✔
475
                        } else if (Utils::pres('img', $ret)) {
328✔
476
                            # This is an image we want to output.  Can cache forever - if an image changes it would get a new id
477
                            @header('Content-Type: image/jpeg');
×
478
                            @header('Content-Length: ' . strlen($ret['img']));
×
479
                            @header('Cache-Control: max-age=5360000');
×
480
                            print $ret['img'];
×
481
                        } else {
482
                            # This is a normal API call.  Add profiling info.
483
                            $duration = (microtime(TRUE) - $scriptstart);
328✔
484
                            $ret['call'] = $call;
328✔
485
                            $ret['type'] = Utils::presdef('type', $_REQUEST, null);
328✔
486
                            $ret['session'] = session_id();
328✔
487
                            $ret['duration'] = $duration;
328✔
488
                            $ret['cpucost'] = API::getCpuUsage($tusage, $rusage);
328✔
489
                            $ret['dbwaittime'] = $dbhr->getWaitTime() + $dbhm->getWaitTime();
328✔
490
                            $ret['includetime'] = $includetime;
328✔
491
                            //                $ret['remoteaddr'] = Utils::presdef('REMOTE_ADDR', $_SERVER, '-');
492
                            //                $ret['_server'] = $_SERVER;
493

494
                            Utils::filterResult($ret);
328✔
495

496
                            $encoded_ret = json_encode($ret, JSON_PARTIAL_OUTPUT_ON_ERROR);
328✔
497
                            echo $encoded_ret;
328✔
498

499
                            if ($duration > 5000) {
328✔
500
                                # Slow call.
501
                                $stamp = microtime(TRUE);
×
502
                                error_log("Slow API call for user " . Utils::presdef('id', $_SESSION, NULL) . " $call stamp $stamp");
×
503
                                file_put_contents("/tmp/iznik.slowapi.$stamp", "User # " . Utils::presdef('id', $_SESSION, NULL) . " request " . var_export($_REQUEST, TRUE) . " response " . var_export($ret, TRUE));
×
504
                            }
505
                        }
506
                    }
507

508
                    if ($apicallretries > 0) {
328✔
509
                        error_log("API call $call worked after $apicallretries");
1✔
510
                    }
511

512
                    break;
328✔
513
                } catch (\Exception $e) {
1✔
514
                    # This is our retry handler.
515
                    if ($e instanceof DBException) {
1✔
516
                        # This is a DBException.  We want to retry, which means we just go round the loop
517
                        # again.
518
                        error_log(
1✔
519
                            "DB Exception try $apicallretries," . $e->getMessage() . ", " . $e->getTraceAsString()
1✔
520
                        );
1✔
521
                        $apicallretries++;
1✔
522

523
                        if ($apicallretries >= API_RETRIES) {
1✔
524
                            if ($call != 'DBexceptionFail') {
1✔
525
                                # Don't log deliberate exceptions in UT.
526
                                \Sentry\captureException($e);
×
527
                            }
528

529
                            $ret = [
1✔
530
                                'ret' => 997,
1✔
531
                                'status' => 'DB operation failed after retry',
1✔
532
                                'exception' => $e->getMessage()
1✔
533
                            ];
1✔
534
                            $encoded_ret = json_encode($ret);
1✔
535
                            echo $encoded_ret;
1✔
536
                        }
537
                    } else {
538
                        # Something else.
539
                        if ($call != 'exception') {
1✔
540
                            # Don't log deliberate exceptions in UT.
541
                            \Sentry\captureException($e);
×
542
                        }
543

544
                        error_log(
1✔
545
                            "Uncaught exception at " . $e->getFile() . " line " . $e->getLine() . " " . $e->getMessage()
1✔
546
                        );
1✔
547
                        $ret = ['ret' => 998, 'status' => 'Unexpected error', 'exception' => $e->getMessage()];
1✔
548
                        $encoded_ret = json_encode($ret);
1✔
549
                        echo $encoded_ret;
1✔
550
                        break;
1✔
551
                    }
552

553
                    # Make sure the duplicate POST detection doesn't throw us.
554
                    $_REQUEST['retry'] = uniqid('', TRUE);
1✔
555
                }
556
            } while ($apicallretries < API_RETRIES);
1✔
557

558
            if (BROWSERTRACKING && (Utils::presdef('type', $_REQUEST, null) != 'GET') &&
328✔
559
                (gettype($ret) == 'array' && !array_key_exists('nolog', $ret)) &&
328✔
560
                (Utils::presdef('action', $_REQUEST, null) != 'MarkSeen')) {
328✔
561
                # Save off the API call and result, except for the (very frequent) event tracking calls.  Don't
562
                # save GET calls as they don't change the DB and there are a lot of them.  Don't save MarkSeen
563
                # calls as they are very frequent and not useful for debugging.
564
                #
565
                # Beanstalk has a limit on the size of job that it accepts; no point trying to log absurdly large
566
                # API requests.
567
                $r = $_REQUEST;
246✔
568
                $headers = Session::getallheaders();
246✔
569
                $r['headers'] = $headers;
246✔
570
                $req = json_encode($r);
246✔
571
                $rsp = $encoded_ret;
246✔
572

573
                if (strlen($req) + strlen($rsp) > 180000) {
246✔
574
                    $req = substr($req, 0, 1000);
1✔
575
                    $rsp = substr($rsp, 0, 1000);
1✔
576
                }
577

578
                $sql = "INSERT INTO logs_api (`userid`, `ip`, `session`, `request`, `response`) VALUES (" .
246✔
579
                    (session_status() !== PHP_SESSION_NONE ? Utils::presdef('id', $_SESSION,'NULL') : 'NULL') .
246✔
580
                    ", '" . Utils::presdef('REMOTE_ADDR', $_SERVER, '') . "', " . $dbhr->quote(session_id()) .
246✔
581
                    ", " . $dbhr->quote($req) . ", " . $dbhr->quote($rsp) . ");";
246✔
582
                $dbhm->background($sql);
246✔
583
            }
584

585
            # Log API request to Loki (fire-and-forget, doesn't block response).
586
            # This is done via shutdown function to ensure response is sent first.
587
            $lokiLogData = [
328✔
588
                'call' => $call,
328✔
589
                'method' => Utils::presdef('type', $_REQUEST, 'GET'),
328✔
590
                'statusCode' => Utils::presdef('ret', $ret, 0),
328✔
591
                'duration' => (microtime(TRUE) - $scriptstart) * 1000, // ms
328✔
592
                'userId' => session_status() !== PHP_SESSION_NONE ? Utils::presdef('id', $_SESSION, NULL) : NULL,
328✔
593
                'ip' => Utils::presdef('REMOTE_ADDR', $_SERVER, ''),
328✔
594
                'queryParams' => $_GET,
328✔
595
                'requestBody' => Utils::presdef('type', $_REQUEST, 'GET') === 'POST' ? $_POST : [],
328✔
596
                'responseBody' => is_array($ret) ? $ret : [],
328✔
597
            ];
328✔
598

599
            # Capture request headers from $_SERVER (HTTP_* format).
600
            $requestHeaders = [];
328✔
601
            foreach ($_SERVER as $key => $value) {
328✔
602
                if (strpos($key, 'HTTP_') === 0) {
328✔
603
                    # Convert HTTP_USER_AGENT to User-Agent format.
604
                    $headerName = str_replace('_', '-', substr($key, 5));
328✔
605
                    $headerName = ucwords(strtolower($headerName), '-');
328✔
606
                    $requestHeaders[$headerName] = $value;
328✔
607
                } elseif (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) {
328✔
608
                    $headerName = str_replace('_', '-', $key);
×
609
                    $headerName = ucwords(strtolower($headerName), '-');
×
610
                    $requestHeaders[$headerName] = $value;
×
611
                }
612
            }
613

614
            # Capture response headers (must be done before output).
615
            $responseHeaders = [];
328✔
616
            foreach (headers_list() as $header) {
328✔
617
                $parts = explode(':', $header, 2);
×
618
                if (count($parts) === 2) {
×
619
                    $responseHeaders[trim($parts[0])] = trim($parts[1]);
×
620
                }
621
            }
622

623
            $lokiHeaderData = [
328✔
624
                'requestHeaders' => $requestHeaders,
328✔
625
                'responseHeaders' => $responseHeaders,
328✔
626
            ];
328✔
627

628
            register_shutdown_function(function() use ($lokiLogData, $lokiHeaderData) {
328✔
629
                $loki = Loki::getInstance();
×
630
                if ($loki->isEnabled()) {
×
631
                    $loki->logApiRequestFull(
×
632
                        'v1',
×
633
                        $lokiLogData['method'],
×
634
                        $lokiLogData['call'],
×
635
                        $lokiLogData['statusCode'],
×
636
                        $lokiLogData['duration'],
×
637
                        $lokiLogData['userId'],
×
638
                        ['ip' => $lokiLogData['ip']],
×
639
                        $lokiLogData['queryParams'],
×
640
                        $lokiLogData['requestBody'],
×
641
                        $lokiLogData['responseBody']
×
642
                    );
×
643

644
                    # Log headers separately (7-day retention for debugging).
645
                    $loki->logApiHeaders(
×
646
                        'v1',
×
647
                        $lokiLogData['method'],
×
648
                        $lokiLogData['call'],
×
649
                        $lokiHeaderData['requestHeaders'],
×
650
                        $lokiHeaderData['responseHeaders'],
×
651
                        $lokiLogData['userId']
×
652
                    );
×
653

654
                    $loki->flush();
×
655
                }
656
            });
328✔
657

658
            # Any outstanding transaction is a bug; force a rollback to avoid locks lasting beyond this call.
659
            if ($dbhm->inTransaction()) {
328✔
660
                $dbhm->rollBack();
1✔
661
            }
662

663
            if (session_status() !== PHP_SESSION_NONE) {
328✔
664
                if (Utils::presdef('type', $_REQUEST, null) != 'GET') {
328✔
665
                    # This might have changed things.
666
                    $_SESSION['modorowner'] = [];
249✔
667
                }
668

669
                # Update our last access time for this user.  his is used to return our
670
                # roster status in ChatRoom.php, and also for spotting idle members.
671
                #
672
                # Do this here, as we might not be logged in at the start if we had a persistent token but no PHP session.
673
                $id = Utils::pres('id', $_SESSION);
328✔
674
                $last = intval(Utils::presdef('lastaccessupdate', $_SESSION, 0));
328✔
675
                if ($id && (abs(time() - $last) > 600)) {
328✔
676
                    $dbhm->background("UPDATE users SET lastaccess = NOW() WHERE id = $id;");
250✔
677
                    $_SESSION['lastaccessupdate'] = time();
250✔
678
                }
679
            }
680
        }
681
    }
682

683
    public static function requestStart() {
684
        $dat = getrusage();
329✔
685
        return ([ microtime(TRUE), $dat["ru_utime.tv_sec"]*1e6+$dat["ru_utime.tv_usec"] ]);
329✔
686
    }
687

688
    public static function getCpuUsage($tusage, $rusage) {
689
        $dat = getrusage();
328✔
690
        $dat["ru_utime.tv_usec"] = ($dat["ru_utime.tv_sec"]*1e6 + $dat["ru_utime.tv_usec"]) - $rusage;
328✔
691
        $time = (microtime(TRUE) - $tusage) * 1000000;
328✔
692

693
        // cpu per request
694
        $cpu = $time > 0 ? $dat["ru_utime.tv_usec"] / $time / 1000 : 0;
328✔
695

696
        return $cpu;
328✔
697
    }
698
}
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

© 2026 Coveralls, Inc