Passed
Pull Request — main (#425)
by MusikAnimal
04:29
created

Edit::getForJson()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 31
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 21
nc 16
nop 2
dl 0
loc 31
ccs 0
cts 0
cp 0
crap 30
rs 9.2728
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the Edit class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Model;
9
10
use DateTime;
11
use Symfony\Component\DependencyInjection\ContainerInterface;
12
13
/**
14
 * An Edit is a single edit to a page on one project.
15
 */
16
class Edit extends Model
17
{
18
    /** @var int ID of the revision */
19
    protected $id;
20
21
    /** @var DateTime Timestamp of the revision */
22
    protected $timestamp;
23
24
    /** @var bool Whether or not this edit was a minor edit */
25
    protected $minor;
26
27
    /** @var int|string|null Length of the page as of this edit, in bytes */
28
    protected $length;
29
30
    /** @var int|string|null The diff size of this edit */
31
    protected $lengthChange;
32
33
    /** @var User - User object of who made the edit */
34
    protected $user;
35
36
    /** @var string The edit summary */
37
    protected $comment;
38
39
    /** @var string The SHA-1 of the wikitext as of the revision. */
40
    protected $sha;
41
42
    /** @var bool Whether this edit was later reverted. */
43
    protected $reverted;
44
45
    /**
46
     * Edit constructor.
47
     * @param Page $page
48
     * @param string[] $attrs Attributes, as retrieved by PageRepository::getRevisions()
49
     */
50 18
    public function __construct(Page $page, array $attrs = [])
51
    {
52 18
        $this->page = $page;
53
54
        // Copy over supported attributes
55 18
        $this->id = isset($attrs['id']) ? (int)$attrs['id'] : (int)$attrs['rev_id'];
56
57
        // Allow DateTime or string (latter assumed to be of format YmdHis)
58 18
        if ($attrs['timestamp'] instanceof DateTime) {
59
            $this->timestamp = $attrs['timestamp'];
60
        } else {
61 18
            $this->timestamp = DateTime::createFromFormat('YmdHis', $attrs['timestamp']);
62
        }
63
64 18
        $this->minor = '1' === $attrs['minor'];
65 18
        $this->length = (int)$attrs['length'];
66 18
        $this->lengthChange = (int)$attrs['length_change'];
67 18
        $this->user = $attrs['user'] ?? ($attrs['username'] ? new User($attrs['username']) : null);
68 18
        $this->comment = $attrs['comment'];
69
70 18
        if (isset($attrs['rev_sha1']) || isset($attrs['sha'])) {
71 4
            $this->sha = $attrs['rev_sha1'] ?? $attrs['sha'];
72
        }
73
74
        // This can be passed in to save as a property on the Edit instance.
75
        // Note that the Edit class knows nothing about it's value, and
76
        // is not capable of detecting whether the given edit was reverted.
77 18
        $this->reverted = isset($attrs['reverted']) ? (bool)$attrs['reverted'] : null;
78 18
    }
79
80
    /**
81
     * Get Edits given revision rows (JOINed on the page table).
82
     * @param Project $project
83
     * @param User $user
84
     * @param array $revs Each must contain 'page_title' and 'page_namespace'.
85
     * @return Edit[]
86
     */
87 3
    public static function getEditsFromRevs(Project $project, User $user, array $revs): array
88
    {
89
        return array_map(function ($rev) use ($project, $user) {
90
            /** @var Page $page Page object to be passed to the Edit constructor. */
91 3
            $page = Page::newFromRow($project, $rev);
92 3
            $rev['user'] = $user;
93
94 3
            return new self($page, $rev);
95 3
        }, $revs);
96
    }
97
98
    /**
99
     * Unique identifier for this Edit, to be used in cache keys.
100
     * @see Repository::getCacheKey()
101
     * @return string
102
     */
103
    public function getCacheKey(): string
104
    {
105
        return (string)$this->id;
106
    }
107
108
    /**
109
     * ID of the edit.
110
     * @return int
111
     */
112 7
    public function getId(): int
113
    {
114 7
        return $this->id;
115
    }
116
117
    /**
118
     * Get the edit's timestamp.
119
     * @return DateTime
120
     */
121 7
    public function getTimestamp(): DateTime
122
    {
123 7
        return $this->timestamp;
124
    }
125
126
    /**
127
     * Get the edit's timestamp as a UTC string, as with YYYY-MM-DDTHH:MM:SS
128
     * @return string
129
     */
130
    public function getUTCTimestamp(): string
131
    {
132
        return $this->getTimestamp()->format('Y-m-d\TH:i:s');
133
    }
134
135
    /**
136
     * Year the revision was made.
137
     * @return string
138
     */
139 5
    public function getYear(): string
140
    {
141 5
        return $this->timestamp->format('Y');
142
    }
143
144
    /**
145
     * Get the numeric representation of the month the revision was made, with leading zeros.
146
     * @return string
147
     */
148 5
    public function getMonth(): string
149
    {
150 5
        return $this->timestamp->format('m');
151
    }
152
153
    /**
154
     * Whether or not this edit was a minor edit.
155
     * @return bool
156
     */
157 6
    public function getMinor(): bool
158
    {
159 6
        return $this->minor;
160
    }
161
162
    /**
163
     * Alias of getMinor()
164
     * @return bool Whether or not this edit was a minor edit
165
     */
166 6
    public function isMinor(): bool
167
    {
168 6
        return $this->getMinor();
169
    }
170
171
    /**
172
     * Length of the page as of this edit, in bytes.
173
     * @see Edit::getSize() Edit::getSize() for the size <em>change</em>.
174
     * @return int
175
     */
176 5
    public function getLength(): int
177
    {
178 5
        return $this->length;
179
    }
180
181
    /**
182
     * The diff size of this edit.
183
     * @return int Signed length change in bytes.
184
     */
185 5
    public function getSize(): int
186
    {
187 5
        return $this->lengthChange;
188
    }
189
190
    /**
191
     * Alias of getSize()
192
     * @return int The diff size of this edit
193
     */
194 1
    public function getLengthChange(): int
195
    {
196 1
        return $this->getSize();
197
    }
198
199
    /**
200
     * Get the user who made the edit.
201
     * @return User|null null can happen for instance if the username was suppressed.
202
     */
203 6
    public function getUser(): ?User
204
    {
205 6
        return $this->user;
206
    }
207
208
    /**
209
     * Set the User.
210
     * @param User $user
211
     */
212
    public function setUser(User $user): void
213
    {
214
        $this->user = $user;
215
    }
216
217
    /**
218
     * Get the edit summary.
219
     * @return string
220
     */
221 2
    public function getComment(): string
222
    {
223 2
        return (string)$this->comment;
224
    }
225
226
    /**
227
     * Get the edit summary (alias of Edit::getComment()).
228
     * @return string
229
     */
230 1
    public function getSummary(): string
231
    {
232 1
        return $this->getComment();
233
    }
234
235
    /**
236
     * Get the SHA-1 of the revision.
237
     * @return string|null
238
     */
239 4
    public function getSha(): ?string
240
    {
241 4
        return $this->sha;
242
    }
243
244
    /**
245
     * Was this edit reported as having been reverted?
246
     * The value for this is merely passed in from precomputed data.
247
     * @return bool|null
248
     */
249 5
    public function isReverted(): ?bool
250
    {
251 5
        return $this->reverted;
252
    }
253
254
    /**
255
     * Set the reverted property.
256
     * @param bool $revert
257
     */
258 4
    public function setReverted(bool $revert): void
259
    {
260 4
        $this->reverted = $revert;
261 4
    }
262
263
    /**
264
     * Get edit summary as 'wikified' HTML markup
265
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
266
     *   an API call. This should be used only if you fetched the page title via other
267
     *   means (SQL query), and is not from user input alone.
268
     * @return string Safe HTML
269
     */
270 1
    public function getWikifiedComment(bool $useUnnormalizedPageTitle = false): string
271
    {
272 1
        return self::wikifyString(
273 1
            $this->getSummary(),
274 1
            $this->getProject(),
275 1
            $this->page,
276 1
            $useUnnormalizedPageTitle
277
        );
278
    }
279
280
    /**
281
     * Public static method to wikify a summary, can be used on any arbitrary string.
282
     * Does NOT support section links unless you specify a page.
283
     * @param string $summary
284
     * @param Project $project
285
     * @param Page $page
286
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
287
     *   an API call. This should be used only if you fetched the page title via other
288
     *   means (SQL query), and is not from user input alone.
289
     * @static
290
     * @return string
291
     */
292 2
    public static function wikifyString(
293
        string $summary,
294
        Project $project,
295
        ?Page $page = null,
296
        bool $useUnnormalizedPageTitle = false
297
    ): string {
298 2
        $summary = htmlspecialchars(html_entity_decode($summary), ENT_NOQUOTES);
299
300
        // First link raw URLs. Courtesy of https://stackoverflow.com/a/11641499/604142
301 2
        $summary = preg_replace(
302 2
            '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s',
303 2
            '<a target="_blank" href="$1">$1</a>',
304 2
            $summary
305
        );
306
307 2
        $sectionMatch = null;
308 2
        $isSection = preg_match_all("/^\/\* (.*?) \*\//", $summary, $sectionMatch);
309
310 2
        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...
311
            $pageUrl = $project->getUrl(false) . str_replace(
312
                '$1',
313
                $page->getTitle($useUnnormalizedPageTitle),
314
                $project->getArticlePath()
315
            );
316
            $sectionTitle = $sectionMatch[1][0];
317
318
            // Must have underscores for the link to properly go to the section.
319
            $sectionTitleLink = htmlspecialchars(str_replace(' ', '_', $sectionTitle));
320
321
            $sectionWikitext = "<a target='_blank' href='$pageUrl#$sectionTitleLink'>&rarr;</a>" .
322
                "<em class='text-muted'>" . htmlspecialchars($sectionTitle) . ":</em> ";
323
            $summary = str_replace($sectionMatch[0][0], $sectionWikitext, $summary);
324
        }
325
326 2
        $linkMatch = null;
327
328 2
        while (preg_match_all("/\[\[:?(.*?)]]/", $summary, $linkMatch)) {
329 2
            $wikiLinkParts = explode('|', $linkMatch[1][0]);
330 2
            $wikiLinkPath = htmlspecialchars($wikiLinkParts[0]);
331 2
            $wikiLinkText = htmlspecialchars(
332 2
                $wikiLinkParts[1] ?? $wikiLinkPath
333
            );
334
335
            // Use normalized page title (underscored, capitalized).
336 2
            $pageUrl = $project->getUrl(false) . str_replace(
337 2
                '$1',
338 2
                ucfirst(str_replace(' ', '_', $wikiLinkPath)),
339 2
                $project->getArticlePath()
340
            );
341
342 2
            $link = "<a target='_blank' href='$pageUrl'>$wikiLinkText</a>";
343 2
            $summary = str_replace($linkMatch[0][0], $link, $summary);
344
        }
345
346 2
        return $summary;
347
    }
348
349
    /**
350
     * Get edit summary as 'wikified' HTML markup (alias of Edit::getWikifiedSummary()).
351
     * @return string
352
     */
353 1
    public function getWikifiedSummary(): string
354
    {
355 1
        return $this->getWikifiedComment();
356
    }
357
358
    /**
359
     * Get the project this edit was made on
360
     * @return Project
361
     */
362 12
    public function getProject(): Project
363
    {
364 12
        return $this->getPage()->getProject();
365
    }
366
367
    /**
368
     * Get the full URL to the diff of the edit
369
     * @return string
370
     */
371 1
    public function getDiffUrl(): string
372
    {
373 1
        $project = $this->getProject();
374 1
        $path = str_replace('$1', 'Special:Diff/' . $this->id, $project->getArticlePath());
375 1
        return rtrim($project->getUrl(), '/') . $path;
376
    }
377
378
    /**
379
     * Get the full permanent URL to the page at the time of the edit
380
     * @return string
381
     */
382 1
    public function getPermaUrl(): string
383
    {
384 1
        $project = $this->getProject();
385 1
        $path = str_replace('$1', 'Special:PermaLink/' . $this->id, $project->getArticlePath());
386 1
        return rtrim($project->getUrl(), '/') . $path;
387
    }
388
389
    /**
390
     * Was the edit a revert, based on the edit summary?
391
     * @param ContainerInterface $container The DI container.
392
     * @return bool
393
     */
394 5
    public function isRevert(ContainerInterface $container): bool
395
    {
396 5
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
397 5
        return $automatedEditsHelper->isRevert($this->comment, $this->getProject());
398
    }
399
400
    /**
401
     * Get the name of the tool that was used to make this edit.
402
     * @param ContainerInterface $container The DI container.
403
     * @return array|false The name of the tool that was used to make the edit
404
     */
405 7
    public function getTool(ContainerInterface $container)
406
    {
407 7
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
408 7
        return $automatedEditsHelper->getTool((string)$this->comment, $this->getProject());
409
    }
410
411
    /**
412
     * Was the edit (semi-)automated, based on the edit summary?
413
     * @param ContainerInterface $container
414
     * @return bool
415
     */
416 2
    public function isAutomated(ContainerInterface $container): bool
417
    {
418 2
        return (bool)$this->getTool($container);
419
    }
420
421
    /**
422
     * Was the edit made by a logged out user?
423
     * @return bool|null
424
     */
425 5
    public function isAnon(): ?bool
426
    {
427 5
        return $this->getUser() ? $this->getUser()->isAnon() : null;
428
    }
429
430
    /**
431
     * Get HTML for the diff of this Edit.
432
     * @return string|null Raw HTML, must be wrapped in a <table> tag. Null if no comparison could be made.
433
     */
434
    public function getDiffHtml(): ?string
435
    {
436
        return $this->getRepository()->getDiffHtml($this);
0 ignored issues
show
Bug introduced by
The method getDiffHtml() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

436
        return $this->getRepository()->/** @scrutinizer ignore-call */ getDiffHtml($this);
Loading history...
437
    }
438
439
    /**
440
     * Formats the data as an array for use in JSON APIs.
441
     * @param bool $includeUsername False for most tools such as Global Contribs, AutoEdits, etc.
442
     * @return array
443
     * @internal This method assumes the Edit was constructed with data already filled in from a database query.
444
     */
445
    public function getForJson(bool $includeUsername = false, bool $includeProject = false): array
446
    {
447
        $nsId = $this->getPage()->getNamespace();
448
        $pageTitle = $this->getPage()->getTitle(true);
449
450
        if ($nsId > 0) {
451
            $nsName = $this->getProject()->getNamespaces()[$nsId];
452
            $pageTitle = preg_replace("/^$nsName:/", '', $pageTitle);
453
        }
454
455
        $ret = [
456
            'page_title' => $pageTitle,
457
            'page_namespace' => $nsId,
458
            'rev_id' => $this->id,
459
            'timestamp' => $this->getUTCTimestamp(),
460
            'minor' => $this->minor,
461
            'length' => $this->length,
462
            'length_change' => $this->lengthChange,
463
            'comment' => $this->comment,
464
        ];
465
        if (null !== $this->reverted) {
466
            $ret['reverted'] = $this->reverted;
467
        }
468
        if ($includeUsername) {
469
            $ret = [ 'username' => $this->getUser()->getUsername() ] + $ret;
470
        }
471
        if ($includeProject) {
472
            $ret = [ 'project' => $this->getProject()->getDomain() ] + $ret;
473
        }
474
475
        return $ret;
476
    }
477
}
478