Passed
Push — rev-deletion ( efd785 )
by MusikAnimal
06:38
created

Edit::deletedUser()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
use App\Repository\EditRepository;
8
use App\Repository\PageRepository;
9
use App\Repository\UserRepository;
10
use DateTime;
11
12
/**
13
 * An Edit is a single edit to a page on one project.
14
 */
15
class Edit extends Model
16
{
17
    public const DELETED_TEXT = 1;
18
    public const DELETED_COMMENT = 2;
19
    public const DELETED_USER = 4;
20
    public const DELETED_RESTRICTED = 8;
21
22
    protected UserRepository $userRepo;
23
24
    /** @var int ID of the revision */
25
    protected int $id;
26
27
    /** @var DateTime Timestamp of the revision */
28
    protected DateTime $timestamp;
29
30
    /** @var bool Whether or not this edit was a minor edit */
31
    protected bool $minor;
32
33
    /** @var int|null Length of the page as of this edit, in bytes */
34
    protected ?int $length;
35
36
    /** @var int|null The diff size of this edit */
37
    protected ?int $lengthChange;
38
39
    /** @var string The edit summary */
40
    protected string $comment;
41
42
    /** @var string|null The SHA-1 of the wikitext as of the revision. */
43
    protected ?string $sha = null;
44
45
    /** @var bool|null Whether this edit was later reverted. */
46
    protected ?bool $reverted;
47
48
    /** @var int Deletion status of the revision. */
49
    protected int $deleted;
50
51
    /**
52
     * Edit constructor.
53
     * @param EditRepository $repository
54
     * @param UserRepository $userRepo
55
     * @param Page $page
56
     * @param string[] $attrs Attributes, as retrieved by PageRepository::getRevisions()
57
     */
58
    public function __construct(EditRepository $repository, UserRepository $userRepo, Page $page, array $attrs = [])
59
    {
60
        $this->repository = $repository;
61
        $this->userRepo = $userRepo;
62
        $this->page = $page;
63
64
        // Copy over supported attributes
65
        $this->id = isset($attrs['id']) ? (int)$attrs['id'] : (int)$attrs['rev_id'];
66
67
        // Allow DateTime or string (latter assumed to be of format YmdHis)
68
        if ($attrs['timestamp'] instanceof DateTime) {
69
            $this->timestamp = $attrs['timestamp'];
70
        } else {
71
            $this->timestamp = DateTime::createFromFormat('YmdHis', $attrs['timestamp']);
72
        }
73
74
        $this->deleted = $attrs['rev_deleted'] ?? 0;
75
76
        if (($this->deleted & self::DELETED_USER) || ($this->deleted & self::DELETED_RESTRICTED)) {
77
            $this->user = null;
78
        } else {
79
            $this->user = $attrs['user'] ?? ($attrs['username'] ? new User($this->userRepo, $attrs['username']) : null);
80
        }
81
82
        $this->minor = 1 === (int)$attrs['minor'];
83
        $this->length = isset($attrs['length']) ? (int)$attrs['length'] : null;
84
        $this->lengthChange = isset($attrs['length_change']) ? (int)$attrs['length_change'] : null;
85
        $this->comment = $attrs['comment'] ?? '';
86
87
        if (isset($attrs['rev_sha1']) || isset($attrs['sha'])) {
88
            $this->sha = $attrs['rev_sha1'] ?? $attrs['sha'];
89
        }
90
91
        // This can be passed in to save as a property on the Edit instance.
92
        // Note that the Edit class knows nothing about it's value, and
93
        // is not capable of detecting whether the given edit was actually reverted.
94
        $this->reverted = isset($attrs['reverted']) ? (bool)$attrs['reverted'] : null;
95
    }
96
97
    /**
98
     * Get Edits given revision rows (JOINed on the page table).
99
     * @param PageRepository $pageRepo
100
     * @param EditRepository $editRepo
101
     * @param UserRepository $userRepo
102
     * @param Project $project
103
     * @param User $user
104
     * @param array $revs Each must contain 'page_title' and 'page_namespace'.
105
     * @return Edit[]
106
     */
107
    public static function getEditsFromRevs(
108
        PageRepository $pageRepo,
109
        EditRepository $editRepo,
110
        UserRepository $userRepo,
111
        Project $project,
112
        User $user,
113
        array $revs
114
    ): array {
115
        return array_map(function ($rev) use ($pageRepo, $editRepo, $userRepo, $project, $user) {
116
            /** Page object to be passed to the Edit constructor. */
117
            $page = Page::newFromRow($pageRepo, $project, $rev);
118
            $rev['user'] = $user;
119
120
            return new self($editRepo, $userRepo, $page, $rev);
121
        }, $revs);
122
    }
123
124
    /**
125
     * Unique identifier for this Edit, to be used in cache keys.
126
     * @see Repository::getCacheKey()
127
     * @return string
128
     */
129
    public function getCacheKey(): string
130
    {
131
        return (string)$this->id;
132
    }
133
134
    /**
135
     * ID of the edit.
136
     * @return int
137
     */
138
    public function getId(): int
139
    {
140
        return $this->id;
141
    }
142
143
    /**
144
     * Get the edit's timestamp.
145
     * @return DateTime
146
     */
147
    public function getTimestamp(): DateTime
148
    {
149
        return $this->timestamp;
150
    }
151
152
    /**
153
     * Get the edit's timestamp as a UTC string, as with YYYY-MM-DDTHH:MM:SS
154
     * @return string
155
     */
156
    public function getUTCTimestamp(): string
157
    {
158
        return $this->getTimestamp()->format('Y-m-d\TH:i:s');
159
    }
160
161
    /**
162
     * Year the revision was made.
163
     * @return string
164
     */
165
    public function getYear(): string
166
    {
167
        return $this->timestamp->format('Y');
168
    }
169
170
    /**
171
     * Get the numeric representation of the month the revision was made, with leading zeros.
172
     * @return string
173
     */
174
    public function getMonth(): string
175
    {
176
        return $this->timestamp->format('m');
177
    }
178
179
    /**
180
     * Whether or not this edit was a minor edit.
181
     * @return bool
182
     */
183
    public function getMinor(): bool
184
    {
185
        return $this->minor;
186
    }
187
188
    /**
189
     * Alias of getMinor()
190
     * @return bool Whether or not this edit was a minor edit
191
     */
192
    public function isMinor(): bool
193
    {
194
        return $this->getMinor();
195
    }
196
197
    /**
198
     * Length of the page as of this edit, in bytes.
199
     * @see Edit::getSize() Edit::getSize() for the size <em>change</em>.
200
     * @return int|null
201
     */
202
    public function getLength(): ?int
203
    {
204
        return $this->length;
205
    }
206
207
    /**
208
     * The diff size of this edit.
209
     * @return int|null Signed length change in bytes.
210
     */
211
    public function getSize(): ?int
212
    {
213
        return $this->lengthChange;
214
    }
215
216
    /**
217
     * Alias of getSize()
218
     * @return int The diff size of this edit
219
     */
220
    public function getLengthChange(): int
221
    {
222
        return $this->getSize();
223
    }
224
225
    /**
226
     * Get the user who made the edit.
227
     * @return User|null null can happen for instance if the username was suppressed.
228
     */
229
    public function getUser(): ?User
230
    {
231
        return $this->user;
232
    }
233
234
    /**
235
     * Set the User.
236
     * @param User $user
237
     */
238
    public function setUser(User $user): void
239
    {
240
        if (($this->deleted & self::DELETED_USER) || ($this->deleted & self::DELETED_RESTRICTED)) {
241
            $this->user = $user;
242
        }
243
    }
244
245
    /**
246
     * Get the edit summary.
247
     * @return string
248
     */
249
    public function getComment(): string
250
    {
251
        return (string)$this->comment;
252
    }
253
254
    /**
255
     * Get the edit summary (alias of Edit::getComment()).
256
     * @return string
257
     */
258
    public function getSummary(): string
259
    {
260
        return $this->getComment();
261
    }
262
263
    /**
264
     * Get the SHA-1 of the revision.
265
     * @return string|null
266
     */
267
    public function getSha(): ?string
268
    {
269
        return $this->sha;
270
    }
271
272
    /**
273
     * Was this edit reported as having been reverted?
274
     * The value for this is merely passed in from precomputed data.
275
     * @return bool|null
276
     */
277
    public function isReverted(): ?bool
278
    {
279
        return $this->reverted;
280
    }
281
282
    /**
283
     * Set the reverted property.
284
     * @param bool $reverted
285
     */
286
    public function setReverted(bool $reverted): void
287
    {
288
        $this->reverted = $reverted;
289
    }
290
291
    /**
292
     * Get deletion status of the revision.
293
     * @return int
294
     */
295
    public function getDeleted(): int
296
    {
297
        return $this->deleted;
298
    }
299
300
    /**
301
     * Was the username deleted from public view?
302
     * @return bool
303
     */
304
    public function deletedUser(): bool
305
    {
306
        return ($this->deleted & self::DELETED_USER) > 0;
307
    }
308
309
    /**
310
     * Was the edit summary deleted from public view?
311
     * @return bool
312
     */
313
    public function deletedSummary(): bool
314
    {
315
        return ($this->deleted & self::DELETED_COMMENT) > 0;
316
    }
317
318
    /**
319
     * Get edit summary as 'wikified' HTML markup
320
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
321
     *   an API call. This should be used only if you fetched the page title via other
322
     *   means (SQL query), and is not from user input alone.
323
     * @return string Safe HTML
324
     */
325
    public function getWikifiedComment(bool $useUnnormalizedPageTitle = false): string
326
    {
327
        return self::wikifyString(
328
            $this->getSummary(),
329
            $this->getProject(),
330
            $this->page,
331
            $useUnnormalizedPageTitle
332
        );
333
    }
334
335
    /**
336
     * Public static method to wikify a summary, can be used on any arbitrary string.
337
     * Does NOT support section links unless you specify a page.
338
     * @param string $summary
339
     * @param Project $project
340
     * @param Page $page
341
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
342
     *   an API call. This should be used only if you fetched the page title via other
343
     *   means (SQL query), and is not from user input alone.
344
     * @static
345
     * @return string
346
     */
347
    public static function wikifyString(
348
        string $summary,
349
        Project $project,
350
        ?Page $page = null,
351
        bool $useUnnormalizedPageTitle = false
352
    ): string {
353
        $summary = htmlspecialchars(html_entity_decode($summary), ENT_NOQUOTES);
354
355
        // First link raw URLs. Courtesy of https://stackoverflow.com/a/11641499/604142
356
        $summary = preg_replace(
357
            '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s',
358
            '<a target="_blank" href="$1">$1</a>',
359
            $summary
360
        );
361
362
        $sectionMatch = null;
363
        $isSection = preg_match_all("/^\/\* (.*?) \*\//", $summary, $sectionMatch);
364
365
        if ($isSection && isset($page)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isSection of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
366
            $pageUrl = $project->getUrl(false) . str_replace(
367
                '$1',
368
                $page->getTitle($useUnnormalizedPageTitle),
369
                $project->getArticlePath()
370
            );
371
            $sectionTitle = $sectionMatch[1][0];
372
373
            // Must have underscores for the link to properly go to the section.
374
            $sectionTitleLink = htmlspecialchars(str_replace(' ', '_', $sectionTitle));
375
376
            $sectionWikitext = "<a target='_blank' href='$pageUrl#$sectionTitleLink'>&rarr;</a>" .
377
                "<em class='text-muted'>" . htmlspecialchars($sectionTitle) . ":</em> ";
378
            $summary = str_replace($sectionMatch[0][0], $sectionWikitext, $summary);
379
        }
380
381
        $linkMatch = null;
382
383
        while (preg_match_all("/\[\[:?(.*?)]]/", $summary, $linkMatch)) {
384
            $wikiLinkParts = explode('|', $linkMatch[1][0]);
385
            $wikiLinkPath = htmlspecialchars($wikiLinkParts[0]);
386
            $wikiLinkText = htmlspecialchars(
387
                $wikiLinkParts[1] ?? $wikiLinkPath
388
            );
389
390
            // Use normalized page title (underscored, capitalized).
391
            $pageUrl = $project->getUrl(false) . str_replace(
392
                '$1',
393
                ucfirst(str_replace(' ', '_', $wikiLinkPath)),
394
                $project->getArticlePath()
395
            );
396
397
            $link = "<a target='_blank' href='$pageUrl'>$wikiLinkText</a>";
398
            $summary = str_replace($linkMatch[0][0], $link, $summary);
399
        }
400
401
        return $summary;
402
    }
403
404
    /**
405
     * Get edit summary as 'wikified' HTML markup (alias of Edit::getWikifiedComment()).
406
     * @return string
407
     */
408
    public function getWikifiedSummary(): string
409
    {
410
        return $this->getWikifiedComment();
411
    }
412
413
    /**
414
     * Get the project this edit was made on
415
     * @return Project
416
     */
417
    public function getProject(): Project
418
    {
419
        return $this->getPage()->getProject();
420
    }
421
422
    /**
423
     * Get the full URL to the diff of the edit
424
     * @return string
425
     */
426
    public function getDiffUrl(): string
427
    {
428
        $project = $this->getProject();
429
        $path = str_replace('$1', 'Special:Diff/' . $this->id, $project->getArticlePath());
430
        return rtrim($project->getUrl(), '/') . $path;
431
    }
432
433
    /**
434
     * Get the full permanent URL to the page at the time of the edit
435
     * @return string
436
     */
437
    public function getPermaUrl(): string
438
    {
439
        $project = $this->getProject();
440
        $path = str_replace('$1', 'Special:PermaLink/' . $this->id, $project->getArticlePath());
441
        return rtrim($project->getUrl(), '/') . $path;
442
    }
443
444
    /**
445
     * Was the edit a revert, based on the edit summary?
446
     * @return bool
447
     */
448
    public function isRevert(): bool
449
    {
450
        return $this->repository->getAutoEditsHelper()->isRevert($this->comment, $this->getProject());
0 ignored issues
show
Bug introduced by
The method getAutoEditsHelper() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\EditRepository. ( Ignorable by Annotation )

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

450
        return $this->repository->/** @scrutinizer ignore-call */ getAutoEditsHelper()->isRevert($this->comment, $this->getProject());
Loading history...
451
    }
452
453
    /**
454
     * Get the name of the tool that was used to make this edit.
455
     * @return array|null The name of the tool(s) that was used to make the edit.
456
     */
457
    public function getTool(): ?array
458
    {
459
        return $this->repository->getAutoEditsHelper()->getTool($this->comment, $this->getProject());
460
    }
461
462
    /**
463
     * Was the edit (semi-)automated, based on the edit summary?
464
     * @return bool
465
     */
466
    public function isAutomated(): bool
467
    {
468
        return (bool)$this->getTool();
469
    }
470
471
    /**
472
     * Was the edit made by a logged out user?
473
     * @return bool|null
474
     */
475
    public function isAnon(): ?bool
476
    {
477
        return $this->getUser() ? $this->getUser()->isAnon() : null;
478
    }
479
480
    /**
481
     * Get HTML for the diff of this Edit.
482
     * @return string|null Raw HTML, must be wrapped in a <table> tag. Null if no comparison could be made.
483
     */
484
    public function getDiffHtml(): ?string
485
    {
486
        return $this->repository->getDiffHtml($this);
0 ignored issues
show
Bug introduced by
The method getDiffHtml() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\EditRepository. ( Ignorable by Annotation )

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

486
        return $this->repository->/** @scrutinizer ignore-call */ getDiffHtml($this);
Loading history...
487
    }
488
489
    /**
490
     * Formats the data as an array for use in JSON APIs.
491
     * @param bool $includeUsername False for most tools such as Global Contribs, AutoEdits, etc.
492
     * @param bool $includeProject
493
     * @return array
494
     * @internal This method assumes the Edit was constructed with data already filled in from a database query.
495
     */
496
    public function getForJson(bool $includeUsername = false, bool $includeProject = false): array
497
    {
498
        $nsId = $this->getPage()->getNamespace();
499
        $pageTitle = $this->getPage()->getTitle(true);
500
501
        if ($nsId > 0) {
502
            $nsName = $this->getProject()->getNamespaces()[$nsId];
503
            $pageTitle = preg_replace("/^$nsName:/", '', $pageTitle);
504
        }
505
506
        $ret = [
507
            'page_title' => $pageTitle,
508
            'page_namespace' => $nsId,
509
            'rev_id' => $this->id,
510
            'timestamp' => $this->getUTCTimestamp(),
511
            'minor' => $this->minor,
512
            'length' => $this->length,
513
            'length_change' => $this->lengthChange,
514
            'comment' => $this->comment,
515
        ];
516
        if (null !== $this->reverted) {
517
            $ret['reverted'] = $this->reverted;
518
        }
519
        if ($includeUsername) {
520
            $ret = [ 'username' => $this->getUser()->getUsername() ] + $ret;
521
        }
522
        if ($includeProject) {
523
            $ret = [ 'project' => $this->getProject()->getDomain() ] + $ret;
524
        }
525
526
        return $ret;
527
    }
528
}
529