Issues (186)

includes/Helpers/LogHelper.php (11 issues)

1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 * ACC Development Team. Please see team.json for a list of contributors.     *
5
 *                                                                            *
6
 * This is free and unencumbered software released into the public domain.    *
7
 * Please see LICENSE.md for the full licencing statement.                    *
8
 ******************************************************************************/
9
10
namespace Waca\Helpers;
11
12
use Exception;
13
use PDO;
14
use Waca\DataObject;
15
use Waca\DataObjects\Ban;
16
use Waca\DataObjects\Comment;
17
use Waca\DataObjects\Domain;
18
use Waca\DataObjects\EmailTemplate;
19
use Waca\DataObjects\JobQueue;
20
use Waca\DataObjects\Log;
21
use Waca\DataObjects\Request;
22
use Waca\DataObjects\RequestForm;
23
use Waca\DataObjects\RequestQueue;
24
use Waca\DataObjects\User;
25
use Waca\DataObjects\WelcomeTemplate;
26
use Waca\Helpers\SearchHelpers\LogSearchHelper;
27
use Waca\Helpers\SearchHelpers\UserSearchHelper;
28
use Waca\PdoDatabase;
29
use Waca\Security\ISecurityManager;
30
use Waca\SiteConfiguration;
31
32
class LogHelper
33
{
34
    /**
35
     * @param int             $requestId
36
     *
37
     * @return DataObject[]
38
     */
39
    public static function getRequestLogsWithComments(
40
        $requestId,
41
        PdoDatabase $db,
42
        ISecurityManager $securityManager
43
    ): array {
44
        // FIXME: domains
45
        $logs = LogSearchHelper::get($db, 1)->byObjectType('Request')->byObjectId($requestId)->fetch();
46
47
        $currentUser = User::getCurrent($db);
48
        $showRestrictedComments = $securityManager->allows('RequestData', 'seeRestrictedComments', $currentUser) === ISecurityManager::ALLOWED;
49
        $showCheckuserComments = $securityManager->allows('RequestData', 'seeCheckuserComments', $currentUser) === ISecurityManager::ALLOWED;
50
51
        $comments = Comment::getForRequest($requestId, $db, $showRestrictedComments, $showCheckuserComments, $currentUser->getId());
52
53
        $items = array_merge($logs, $comments);
54
55
        $sortKey = function(DataObject $item): int {
56
            if ($item instanceof Log) {
57
                return $item->getTimestamp()->getTimestamp();
58
            }
59
60
            if ($item instanceof Comment) {
61
                return $item->getTime()->getTimestamp();
62
            }
63
64
            return 0;
65
        };
66
67
        do {
68
            $flag = false;
69
70
            $loopLimit = (count($items) - 1);
71
            for ($i = 0; $i < $loopLimit; $i++) {
72
                // are these two items out of order?
73
                if ($sortKey($items[$i]) > $sortKey($items[$i + 1])) {
74
                    // swap them
75
                    $swap = $items[$i];
76
                    $items[$i] = $items[$i + 1];
77
                    $items[$i + 1] = $swap;
78
79
                    // set a flag to say we've modified the array this time around
80
                    $flag = true;
81
                }
82
            }
83
        }
84
        while ($flag);
85
86
        return $items;
87
    }
88
89
    public static function getLogDescription(Log $entry): string
90
    {
91
        $text = "Deferred to ";
92
        if (substr($entry->getAction(), 0, strlen($text)) == $text) {
93
            // Deferred to a different queue
94
            // This is exactly what we want to display.
95
            return $entry->getAction();
96
        }
97
98
        $text = "Closed custom-n";
99
        if ($entry->getAction() == $text) {
100
            // Custom-closed
101
            return "closed (custom reason - account not created)";
102
        }
103
104
        $text = "Closed custom-y";
105
        if ($entry->getAction() == $text) {
106
            // Custom-closed
107
            return "closed (custom reason - account created)";
108
        }
109
110
        $text = "Closed 0";
111
        if ($entry->getAction() == $text) {
112
            // Dropped the request - short-circuit the lookup
113
            return "dropped request";
114
        }
115
116
        $text = "Closed ";
117
        if (substr($entry->getAction(), 0, strlen($text)) == $text) {
118
            // Closed with a reason - do a lookup here.
119
            $id = substr($entry->getAction(), strlen($text));
120
            /** @var EmailTemplate|false $template */
121
            $template = EmailTemplate::getById((int)$id, $entry->getDatabase());
122
123
            if ($template !== false) {
0 ignored issues
show
The condition $template !== false is always true.
Loading history...
124
                return 'closed (' . $template->getName() . ')';
125
            }
126
        }
127
128
        // Fall back to the basic stuff
129
        $lookup = array(
130
            'Reserved'            => 'reserved',
131
            'Email Confirmed'     => 'email-confirmed',
132
            'Manually Confirmed'  => 'manually confirmed the request',
133
            'Unreserved'          => 'unreserved',
134
            'Approved'            => 'approved',
135
            'DeactivatedUser'     => 'deactivated user',
136
            'RoleChange'          => 'changed roles',
137
            'GlobalRoleChange'    => 'changed global roles',
138
            'RequestedReactivation' => 'requested reactivation',
139
            'Banned'              => 'banned',
140
            'Edited'              => 'edited interface message',
141
            'EditComment-c'       => 'edited a comment',
142
            'EditComment-r'       => 'edited a comment',
143
            'FlaggedComment'      => 'flagged a comment',
144
            'UnflaggedComment'    => 'unflagged a comment',
145
            'Unbanned'            => 'unbanned',
146
            'BanReplaced'         => 'replaced ban',
147
            'Promoted'            => 'promoted to tool admin',
148
            'BreakReserve'        => 'forcibly broke the reservation',
149
            'Prefchange'          => 'changed user preferences',
150
            'Renamed'             => 'renamed',
151
            'Demoted'             => 'demoted from tool admin',
152
            'ReceiveReserved'     => 'received the reservation',
153
            'SendReserved'        => 'sent the reservation',
154
            'EditedEmail'         => 'edited email',
155
            'DeletedTemplate'     => 'deleted template',
156
            'EditedTemplate'      => 'edited template',
157
            'CreatedEmail'        => 'created email',
158
            'CreatedTemplate'     => 'created template',
159
            'SentMail'            => 'sent an email to the requester',
160
            'Registered'          => 'registered a tool account',
161
            'JobIssue'            => 'ran a background job unsuccessfully',
162
            'JobCompleted'        => 'completed a background job',
163
            'JobAcknowledged'     => 'acknowledged a job failure',
164
            'JobRequeued'         => 'requeued a job for re-execution',
165
            'JobCancelled'        => 'cancelled execution of a job',
166
            'EnqueuedJobQueue'    => 'scheduled for creation',
167
            'Hospitalised'        => 'sent to the hospital',
168
            'QueueCreated'        => 'created a request queue',
169
            'QueueEdited'         => 'edited a request queue',
170
            'DomainCreated'       => 'created a domain',
171
            'DomainEdited'        => 'edited a domain',
172
            'RequestFormCreated'  => 'created a request form',
173
            'RequestFormEdited'   => 'edited a request form',
174
        );
175
176
        if (array_key_exists($entry->getAction(), $lookup)) {
177
            return $lookup[$entry->getAction()];
178
        }
179
180
        // OK, I don't know what this is. Fall back to something sane.
181
        return "performed an unknown action ({$entry->getAction()})";
182
    }
183
184
    public static function getLogActions(PdoDatabase $database): array
185
    {
186
        $lookup = array(
187
            "Requests" => [
188
                'Reserved'            => 'reserved',
189
                'Email Confirmed'     => 'email-confirmed',
190
                'Manually Confirmed'  => 'manually confirmed',
191
                'Unreserved'          => 'unreserved',
192
                'EditComment-c'       => 'edited a comment (by comment ID)',
193
                'EditComment-r'       => 'edited a comment (by request)',
194
                'FlaggedComment'      => 'flagged a comment',
195
                'UnflaggedComment'    => 'unflagged a comment',
196
                'BreakReserve'        => 'forcibly broke the reservation',
197
                'ReceiveReserved'     => 'received the reservation',
198
                'SendReserved'        => 'sent the reservation',
199
                'SentMail'            => 'sent an email to the requester',
200
                'Closed 0'            => 'dropped request',
201
                'Closed custom-y'     => 'closed (custom reason - account created)',
202
                'Closed custom-n'     => 'closed (custom reason - account not created)',
203
            ],
204
            'Users' => [
205
                'Approved'            => 'approved',
206
                'DeactivatedUser'     => 'deactivated user',
207
                'RoleChange'          => 'changed roles',
208
                'GlobalRoleChange'    => 'changed global roles',
209
                'Prefchange'          => 'changed user preferences',
210
                'Renamed'             => 'renamed',
211
                'Promoted'            => 'promoted to tool admin',
212
                'Demoted'             => 'demoted from tool admin',
213
                'Registered'          => 'registered a tool account',
214
                'RequestedReactivation' => 'requested reactivation',
215
            ],
216
            "Bans" => [
217
                'Banned'              => 'banned',
218
                'Unbanned'            => 'unbanned',
219
                'BanReplaced'         => 'replaced ban',
220
            ],
221
            "Site notice" => [
222
                'Edited'              => 'edited interface message',
223
            ],
224
            "Email close templates" => [
225
                'EditedEmail'         => 'edited email',
226
                'CreatedEmail'        => 'created email',
227
            ],
228
            "Welcome templates" => [
229
                'DeletedTemplate'     => 'deleted template',
230
                'EditedTemplate'      => 'edited template',
231
                'CreatedTemplate'     => 'created template',
232
            ],
233
            "Job queue" => [
234
                'JobIssue'            => 'ran a background job unsuccessfully',
235
                'JobCompleted'        => 'completed a background job',
236
                'JobAcknowledged'     => 'acknowledged a job failure',
237
                'JobRequeued'         => 'requeued a job for re-execution',
238
                'JobCancelled'        => 'cancelled execution of a job',
239
                'EnqueuedJobQueue'    => 'scheduled for creation',
240
                'Hospitalised'        => 'sent to the hospital',
241
            ],
242
            "Request queues" => [
243
                'QueueCreated'        => 'created a request queue',
244
                'QueueEdited'         => 'edited a request queue',
245
            ],
246
            "Domains" => [
247
                'DomainCreated'       => 'created a domain',
248
                'DomainEdited'        => 'edited a domain',
249
            ],
250
            "Request forms" => [
251
                'RequestFormCreated'        => 'created a request form',
252
                'RequestFormEdited'         => 'edited a request form',
253
            ],
254
        );
255
256
        $databaseDrivenLogKeys = $database->query(<<<SQL
257
SELECT CONCAT('Closed ', id) AS k, CONCAT('closed (',name,')') AS v FROM emailtemplate
258
UNION ALL
259
SELECT CONCAT('Deferred to ', logname) AS k, CONCAT('deferred to ', displayname) AS v FROM requestqueue;
260
SQL
261
        );
262
        foreach ($databaseDrivenLogKeys->fetchAll(PDO::FETCH_ASSOC) as $row) {
263
            $lookup["Requests"][$row['k']] = $row['v'];
264
        }
265
266
        return $lookup;
267
    }
268
269
    public static function getObjectTypes(): array
270
    {
271
        return array(
272
            'Ban'             => 'Ban',
273
            'Comment'         => 'Comment',
274
            'EmailTemplate'   => 'Email template',
275
            'JobQueue'        => 'Job queue item',
276
            'Request'         => 'Request',
277
            'SiteNotice'      => 'Site notice',
278
            'User'            => 'User',
279
            'WelcomeTemplate' => 'Welcome template',
280
            'RequestQueue'    => 'Request queue',
281
            'Domain'          => 'Domain',
282
            'RequestForm'     => 'Request form'
283
        );
284
    }
285
286
    /**
287
     * This returns an HTML representation of the object
288
     *
289
     * @param int               $objectId
290
     * @param string            $objectType
291
     *
292
     * @category Security-Critical
293
     */
294
    private static function getObjectDescription(
295
        $objectId,
296
        $objectType,
297
        PdoDatabase $database,
298
        SiteConfiguration $configuration
299
    ): ?string {
300
        if ($objectType == '') {
301
            return null;
302
        }
303
304
        $baseurl = $configuration->getBaseUrl();
305
306
        switch ($objectType) {
307
            case 'Ban':
308
                /** @var Ban $ban */
309
                $ban = Ban::getById($objectId, $database);
310
311
                if ($ban === false) {
0 ignored issues
show
The condition $ban === false is always false.
Loading history...
312
                    return 'Ban #' . $objectId;
313
                }
314
315
                return <<<HTML
316
<a href="{$baseurl}/internal.php/bans/show?id={$objectId}">Ban #{$objectId}</a>
317
HTML;
318
            case 'EmailTemplate':
319
                /** @var EmailTemplate $emailTemplate */
320
                $emailTemplate = EmailTemplate::getById($objectId, $database);
321
322
                if ($emailTemplate === false) {
0 ignored issues
show
The condition $emailTemplate === false is always false.
Loading history...
323
                    return 'Email Template #' . $objectId;
324
                }
325
326
                $name = htmlentities($emailTemplate->getName(), ENT_COMPAT, 'UTF-8');
327
328
                return <<<HTML
329
<a href="{$baseurl}/internal.php/emailManagement/view?id={$objectId}">Email Template #{$objectId} ({$name})</a>
330
HTML;
331
            case 'SiteNotice':
332
                return "<a href=\"{$baseurl}/internal.php/siteNotice\">the site notice</a>";
333
            case 'Request':
334
                /** @var Request $request */
335
                $request = Request::getById($objectId, $database);
336
337
                if ($request === false) {
0 ignored issues
show
The condition $request === false is always false.
Loading history...
338
                    return 'Request #' . $objectId;
339
                }
340
341
                $name = htmlentities($request->getName(), ENT_COMPAT, 'UTF-8');
342
343
                return <<<HTML
344
<a href="{$baseurl}/internal.php/viewRequest?id={$objectId}">Request #{$objectId} ({$name})</a>
345
HTML;
346
            case 'User':
347
                /** @var User $user */
348
                $user = User::getById($objectId, $database);
349
350
                // Some users were merged out of existence
351
                if ($user === false) {
0 ignored issues
show
The condition $user === false is always false.
Loading history...
352
                    return 'User #' . $objectId;
353
                }
354
355
                $username = htmlentities($user->getUsername(), ENT_COMPAT, 'UTF-8');
356
357
                return "<a href=\"{$baseurl}/internal.php/statistics/users/detail?user={$objectId}\">{$username}</a>";
358
            case 'WelcomeTemplate':
359
                /** @var WelcomeTemplate $welcomeTemplate */
360
                $welcomeTemplate = WelcomeTemplate::getById($objectId, $database);
361
362
                // some old templates have been completely deleted and lost to the depths of time.
363
                if ($welcomeTemplate === false) {
0 ignored issues
show
The condition $welcomeTemplate === false is always false.
Loading history...
364
                    return "Welcome template #{$objectId}";
365
                }
366
                else {
367
                    $userCode = htmlentities($welcomeTemplate->getUserCode(), ENT_COMPAT, 'UTF-8');
368
369
                    return "<a href=\"{$baseurl}/internal.php/welcomeTemplates/view?template={$objectId}\">{$userCode}</a>";
370
                }
371
            case 'JobQueue':
372
                /** @var JobQueue $job */
373
                $job = JobQueue::getById($objectId, $database);
374
375
                $taskDescriptions = JobQueue::getTaskDescriptions();
376
377
                if ($job === false) {
0 ignored issues
show
The condition $job === false is always false.
Loading history...
378
                    return 'Job Queue Task #' . $objectId;
379
                }
380
381
                $task = $job->getTask();
382
                if (isset($taskDescriptions[$task])) {
383
                    $description = $taskDescriptions[$task];
384
                }
385
                else {
386
                    $description = 'Unknown task';
387
                }
388
389
                return "<a href=\"{$baseurl}/internal.php/jobQueue/view?id={$objectId}\">Job #{$job->getId()} ({$description})</a>";
390
            case 'RequestQueue':
391
                /** @var RequestQueue $queue */
392
                $queue = RequestQueue::getById($objectId, $database);
393
394
                if ($queue === false) {
0 ignored issues
show
The condition $queue === false is always false.
Loading history...
395
                    return "Request Queue #{$objectId}";
396
                }
397
398
                $queueHeader = htmlentities($queue->getHeader(), ENT_COMPAT, 'UTF-8');
399
400
                return "<a href=\"{$baseurl}/internal.php/queueManagement/edit?queue={$objectId}\">{$queueHeader}</a>";
401
            case 'Domain':
402
                /** @var Domain $domain */
403
                $domain = Domain::getById($objectId, $database);
404
405
                if ($domain === false) {
0 ignored issues
show
The condition $domain === false is always false.
Loading history...
406
                    return "Domain #{$objectId}";
407
                }
408
409
                $domainName = htmlentities($domain->getShortName(), ENT_COMPAT, 'UTF-8');
410
                return "<a href=\"{$baseurl}/internal.php/domainManagement/edit?domain={$objectId}\">{$domainName}</a>";
411
            case 'RequestForm':
412
                /** @var RequestForm $queue */
413
                $queue = RequestForm::getById($objectId, $database);
414
415
                if ($queue === false) {
0 ignored issues
show
The condition $queue === false is always false.
Loading history...
416
                    return "Request Form #{$objectId}";
417
                }
418
419
                $formName = htmlentities($queue->getName(), ENT_COMPAT, 'UTF-8');
420
421
                return "<a href=\"{$baseurl}/internal.php/requestFormManagement/edit?form={$objectId}\">{$formName}</a>";
422
            case 'Comment':
423
                /** @var Comment $comment */
424
                $comment = Comment::getById($objectId, $database);
425
                /** @var Request $request */
426
                $request = Request::getById($comment->getRequest(), $database);
427
                $requestName = htmlentities($request->getName(), ENT_COMPAT, 'UTF-8');
428
429
                return "<a href=\"{$baseurl}/internal.php/editComment?id={$objectId}\">Comment {$objectId}</a> on request <a href=\"{$baseurl}/internal.php/viewRequest?id={$comment->getRequest()}#comment-{$objectId}\">#{$comment->getRequest()} ({$requestName})</a>";
430
            default:
431
                return '[' . $objectType . " " . $objectId . ']';
432
        }
433
    }
434
435
    /**
436
     * @param Log[] $logs
437
     * @throws Exception
438
     *
439
     * @returns User[]
440
     */
441
    private static function loadUsersFromLogs(array $logs, PdoDatabase $database): array
442
    {
443
        $userIds = array();
444
445
        foreach ($logs as $logEntry) {
446
            if (!$logEntry instanceof Log) {
447
                // if this happens, we've done something wrong with passing back the log data.
448
                throw new Exception('Log entry is not an instance of a Log, this should never happen.');
449
            }
450
451
            $user = $logEntry->getUser();
452
            if ($user === -1) {
453
                continue;
454
            }
455
456
            if (!array_search($user, $userIds)) {
457
                $userIds[] = $user;
458
            }
459
        }
460
461
        $users = UserSearchHelper::get($database)->inIds($userIds)->fetchMap('username');
462
        $users[-1] = User::getCommunity()->getUsername();
463
464
        return $users;
465
    }
466
467
    /**
468
     * @param Log[] $logs
469
     *
470
     * @throws Exception
471
     */
472
    public static function prepareLogsForTemplate(
473
        array $logs,
474
        PdoDatabase $database,
475
        SiteConfiguration $configuration,
476
        ISecurityManager $securityManager
477
    ): array {
478
        $users = self::loadUsersFromLogs($logs, $database);
479
        $currentUser = User::getCurrent($database);
480
481
        $allowAccountLogSelf = $securityManager->allows('UserData', 'accountLogSelf', $currentUser) === ISecurityManager::ALLOWED;
482
        $allowAccountLog = $securityManager->allows('UserData', 'accountLog', $currentUser) === ISecurityManager::ALLOWED;
483
484
        $protectedLogActions = [
485
            'RequestedReactivation',
486
            'DeactivatedUser',
487
        ];
488
489
        $logData = array();
490
        foreach ($logs as $logEntry) {
491
            $objectDescription = self::getObjectDescription($logEntry->getObjectId(), $logEntry->getObjectType(),
492
                $database, $configuration);
493
494
            // initialise to sane default
495
            $comment = null;
496
497
            switch ($logEntry->getAction()) {
498
                case 'Renamed':
499
                    $renameData = unserialize($logEntry->getComment());
0 ignored issues
show
It seems like $logEntry->getComment() can also be of type null; however, parameter $data of unserialize() 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

499
                    $renameData = unserialize(/** @scrutinizer ignore-type */ $logEntry->getComment());
Loading history...
500
                    $oldName = htmlentities($renameData['old'], ENT_COMPAT, 'UTF-8');
501
                    $newName = htmlentities($renameData['new'], ENT_COMPAT, 'UTF-8');
502
                    $comment = 'Renamed \'' . $oldName . '\' to \'' . $newName . '\'.';
503
                    break;
504
                case 'RoleChange':
505
                case 'GlobalRoleChange':
506
                    $roleChangeData = unserialize($logEntry->getComment());
507
508
                    $removed = array();
509
                    foreach ($roleChangeData['removed'] as $r) {
510
                        $removed[] = htmlentities($r, ENT_COMPAT, 'UTF-8');
511
                    }
512
513
                    $added = array();
514
                    foreach ($roleChangeData['added'] as $r) {
515
                        $added[] = htmlentities($r, ENT_COMPAT, 'UTF-8');
516
                    }
517
518
                    $reason = htmlentities($roleChangeData['reason'], ENT_COMPAT, 'UTF-8');
519
520
                    $roleDelta = 'Removed [' . implode(', ', $removed) . '], Added [' . implode(', ', $added) . ']';
521
                    $comment = $roleDelta . ' with comment: ' . $reason;
522
                    break;
523
                case 'JobIssue':
524
                    $jobIssueData = unserialize($logEntry->getComment());
525
                    $errorMessage = $jobIssueData['error'];
526
                    $status = $jobIssueData['status'];
527
528
                    $comment = 'Job ' . htmlentities($status, ENT_COMPAT, 'UTF-8') . ': ';
529
                    $comment .= htmlentities($errorMessage, ENT_COMPAT, 'UTF-8');
530
                    break;
531
                case 'JobIssueRequest':
532
                case 'JobCompletedRequest':
533
                    $jobData = unserialize($logEntry->getComment());
534
535
                    /** @var JobQueue $job */
536
                    $job = JobQueue::getById($jobData['job'], $database);
537
                    $descs = JobQueue::getTaskDescriptions();
538
                    $comment = htmlentities($descs[$job->getTask()], ENT_COMPAT, 'UTF-8');
539
                    break;
540
541
                case 'JobCompleted':
542
                    break;
543
544
                default:
545
                    $comment = $logEntry->getComment();
546
                    break;
547
            }
548
549
            if (in_array($logEntry->getAction(), $protectedLogActions) && $logEntry->getObjectType() === 'User') {
550
                if ($allowAccountLog) {
551
                    // do nothing, allowed to see all account logs
552
                }
553
                else if ($allowAccountLogSelf && $currentUser->getId() === $logEntry->getObjectId()) {
554
                    // do nothing, allowed to see own account log
555
                }
556
                else {
557
                    $comment = null;
558
                }
559
            }
560
561
            $logData[] = array(
562
                'timestamp'         => $logEntry->getTimestamp(),
563
                'userid'            => $logEntry->getUser(),
564
                'username'          => $users[$logEntry->getUser()],
565
                'description'       => self::getLogDescription($logEntry),
566
                'objectdescription' => $objectDescription,
567
                'comment'           => $comment,
568
            );
569
        }
570
571
        return array($users, $logData);
572
    }
573
}
574