Edit::getSize()   A
last analyzed

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 = (int)($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 '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:SSZ
154
     * @return string
155
     */
156
    public function getUTCTimestamp(): string
157
    {
158
        return $this->getTimestamp()->format('Y-m-d\TH:i:s\Z');
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|null 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
     * Get the edit summary.
236
     * @return string
237
     */
238
    public function getComment(): string
239
    {
240
        return (string)$this->comment;
241
    }
242
243
    /**
244
     * Get the edit summary (alias of Edit::getComment()).
245
     * @return string
246
     */
247
    public function getSummary(): string
248
    {
249
        return $this->getComment();
250
    }
251
252
    /**
253
     * Get the SHA-1 of the revision.
254
     * @return string|null
255
     */
256
    public function getSha(): ?string
257
    {
258
        return $this->sha;
259
    }
260
261
    /**
262
     * Was this edit reported as having been reverted?
263
     * The value for this is merely passed in from precomputed data.
264
     * @return bool|null
265
     */
266
    public function isReverted(): ?bool
267
    {
268
        return $this->reverted;
269
    }
270
271
    /**
272
     * Set the reverted property.
273
     * @param bool $reverted
274
     */
275
    public function setReverted(bool $reverted): void
276
    {
277
        $this->reverted = $reverted;
278
    }
279
280
    /**
281
     * Get deletion status of the revision.
282
     * @return int
283
     */
284
    public function getDeleted(): int
285
    {
286
        return $this->deleted;
287
    }
288
289
    /**
290
     * Was the username deleted from public view?
291
     * @return bool
292
     */
293
    public function deletedUser(): bool
294
    {
295
        return ($this->deleted & self::DELETED_USER) > 0;
296
    }
297
298
    /**
299
     * Was the edit summary deleted from public view?
300
     * @return bool
301
     */
302
    public function deletedSummary(): bool
303
    {
304
        return ($this->deleted & self::DELETED_COMMENT) > 0;
305
    }
306
307
    /**
308
     * Get edit summary as 'wikified' HTML markup
309
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
310
     *   an API call. This should be used only if you fetched the page title via other
311
     *   means (SQL query), and is not from user input alone.
312
     * @return string Safe HTML
313
     */
314
    public function getWikifiedComment(bool $useUnnormalizedPageTitle = false): string
315
    {
316
        return self::wikifyString(
317
            $this->getSummary(),
318
            $this->getProject(),
319
            $this->page,
320
            $useUnnormalizedPageTitle
321
        );
322
    }
323
324
    /**
325
     * Public static method to wikify a summary, can be used on any arbitrary string.
326
     * Does NOT support section links unless you specify a page.
327
     * @param string $summary
328
     * @param Project $project
329
     * @param Page|null $page
330
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
331
     *   an API call. This should be used only if you fetched the page title via other
332
     *   means (SQL query), and is not from user input alone.
333
     * @static
334
     * @return string
335
     */
336
    public static function wikifyString(
337
        string $summary,
338
        Project $project,
339
        ?Page $page = null,
340
        bool $useUnnormalizedPageTitle = false
341
    ): string {
342
        $summary = htmlspecialchars(html_entity_decode($summary), ENT_NOQUOTES);
343
344
        // First link raw URLs. Courtesy of https://stackoverflow.com/a/11641499/604142
345
        $summary = preg_replace(
346
            '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s',
347
            '<a target="_blank" href="$1">$1</a>',
348
            $summary
349
        );
350
351
        $sectionMatch = null;
352
        $isSection = preg_match_all("/^\/\* (.*?) \*\//", $summary, $sectionMatch);
353
354
        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...
355
            $pageUrl = $project->getUrlForPage($page->getTitle($useUnnormalizedPageTitle));
356
            $sectionTitle = $sectionMatch[1][0];
357
358
            // Must have underscores for the link to properly go to the section.
359
            $sectionTitleLink = htmlspecialchars(str_replace(' ', '_', $sectionTitle));
360
361
            $sectionWikitext = "<a target='_blank' href='$pageUrl#$sectionTitleLink'>&rarr;</a>" .
362
                "<em class='text-muted'>" . htmlspecialchars($sectionTitle) . ":</em> ";
363
            $summary = str_replace($sectionMatch[0][0], $sectionWikitext, $summary);
364
        }
365
366
        $linkMatch = null;
367
368
        while (preg_match_all("/\[\[:?(.*?)]]/", $summary, $linkMatch)) {
369
            $wikiLinkParts = explode('|', $linkMatch[1][0]);
370
            $wikiLinkPath = htmlspecialchars($wikiLinkParts[0]);
371
            $wikiLinkText = htmlspecialchars(
372
                $wikiLinkParts[1] ?? $wikiLinkPath
373
            );
374
375
            // Use normalized page title (underscored, capitalized).
376
            $pageUrl = $project->getUrlForPage(ucfirst(str_replace(' ', '_', $wikiLinkPath)));
377
378
            $link = "<a target='_blank' href='$pageUrl'>$wikiLinkText</a>";
379
            $summary = str_replace($linkMatch[0][0], $link, $summary);
380
        }
381
382
        return $summary;
383
    }
384
385
    /**
386
     * Get edit summary as 'wikified' HTML markup (alias of Edit::getWikifiedComment()).
387
     * @return string
388
     */
389
    public function getWikifiedSummary(): string
390
    {
391
        return $this->getWikifiedComment();
392
    }
393
394
    /**
395
     * Get the project this edit was made on
396
     * @return Project
397
     */
398
    public function getProject(): Project
399
    {
400
        return $this->getPage()->getProject();
401
    }
402
403
    /**
404
     * Get the full URL to the diff of the edit
405
     * @return string
406
     */
407
    public function getDiffUrl(): string
408
    {
409
        return rtrim($this->getProject()->getUrlForPage('Special:Diff/' . $this->id), '/');
410
    }
411
412
    /**
413
     * Get the full permanent URL to the page at the time of the edit
414
     * @return string
415
     */
416
    public function getPermaUrl(): string
417
    {
418
        return rtrim($this->getProject()->getUrlForPage('Special:PermaLink/' . $this->id), '/');
419
    }
420
421
    /**
422
     * Was the edit a revert, based on the edit summary?
423
     * @return bool
424
     */
425
    public function isRevert(): bool
426
    {
427
        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

427
        return $this->repository->/** @scrutinizer ignore-call */ getAutoEditsHelper()->isRevert($this->comment, $this->getProject());
Loading history...
428
    }
429
430
    /**
431
     * Get the name of the tool that was used to make this edit.
432
     * @return array|null The name of the tool(s) that was used to make the edit.
433
     */
434
    public function getTool(): ?array
435
    {
436
        return $this->repository->getAutoEditsHelper()->getTool($this->comment, $this->getProject());
437
    }
438
439
    /**
440
     * Was the edit (semi-)automated, based on the edit summary?
441
     * @return bool
442
     */
443
    public function isAutomated(): bool
444
    {
445
        return (bool)$this->getTool();
446
    }
447
448
    /**
449
     * Was the edit made by a logged out user?
450
     * @return bool|null
451
     */
452
    public function isAnon(): ?bool
453
    {
454
        return $this->getUser() ? $this->getUser()->isAnon() : null;
455
    }
456
457
    /**
458
     * Get HTML for the diff of this Edit.
459
     * @return string|null Raw HTML, must be wrapped in a <table> tag. Null if no comparison could be made.
460
     */
461
    public function getDiffHtml(): ?string
462
    {
463
        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

463
        return $this->repository->/** @scrutinizer ignore-call */ getDiffHtml($this);
Loading history...
464
    }
465
466
    /**
467
     * Formats the data as an array for use in JSON APIs.
468
     * @param bool $includeProject
469
     * @return array
470
     * @internal This method assumes the Edit was constructed with data already filled in from a database query.
471
     */
472
    public function getForJson(bool $includeProject = false): array
473
    {
474
        $nsId = $this->getPage()->getNamespace();
475
        $pageTitle = $this->getPage()->getTitle(true);
476
477
        if ($nsId > 0) {
478
            $nsName = $this->getProject()->getNamespaces()[$nsId];
479
            $pageTitle = preg_replace("/^$nsName:/", '', $pageTitle);
480
        }
481
482
        $ret = [
483
            'page_title' => str_replace('_', ' ', $pageTitle),
484
            'namespace' => $nsId,
485
        ];
486
        if ($includeProject) {
487
            $ret += ['project' => $this->getProject()->getDomain()];
488
        }
489
        if ($this->getUser()) {
490
            $ret += ['username' => $this->getUser()->getUsername()];
491
        }
492
        $ret += [
493
            'rev_id' => $this->id,
494
            'timestamp' => $this->getUTCTimestamp(),
495
            'minor' => $this->minor,
496
            'length' => $this->length,
497
            'length_change' => $this->lengthChange,
498
            'comment' => $this->comment,
499
        ];
500
        if (null !== $this->reverted) {
501
            $ret['reverted'] = $this->reverted;
502
        }
503
504
        return $ret;
505
    }
506
}
507