Test Failed
Push — globalcontribs-api ( 712af9 )
by MusikAnimal
07:26
created

Edit   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 471
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 119
dl 0
loc 471
rs 8.8
c 0
b 0
f 0
wmc 45

34 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 28 7
A getEditsFromRevs() 0 9 1
A isAnon() 0 3 2
A getSummary() 0 3 1
A getComment() 0 3 1
A getCacheKey() 0 3 1
A getId() 0 3 1
A setUser() 0 3 1
A isRevert() 0 4 1
A isMinor() 0 3 1
A getLength() 0 3 1
A getPermaUrl() 0 5 1
A wikifyString() 0 55 4
A getMonth() 0 3 1
A getDiffHtml() 0 3 1
A isReverted() 0 3 1
A getYear() 0 3 1
A isAutomated() 0 3 1
A getUTCTimestamp() 0 3 1
A getWikifiedSummary() 0 3 1
A getTool() 0 4 1
A addFullPageTitlesAndContinue() 0 8 1
A getProject() 0 3 1
A getForJson() 0 20 2
A getSha() 0 3 1
A getLengthChange() 0 3 1
A newFromRow() 0 4 1
A getMinor() 0 3 1
A setReverted() 0 3 1
A getDiffUrl() 0 5 1
A getWikifiedComment() 0 7 1
A getUser() 0 3 1
A getSize() 0 3 1
A getTimestamp() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Edit often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Edit, and based on these observations, apply Extract Interface, too.

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
    public function __construct(Page $page, array $attrs = [])
51
    {
52
        $this->page = $page;
53
54
        // Copy over supported attributes
55
        $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
        if ($attrs['timestamp'] instanceof DateTime) {
59
            $this->timestamp = $attrs['timestamp'];
60
        } else {
61
            $this->timestamp = DateTime::createFromFormat('YmdHis', $attrs['timestamp']);
62
        }
63
64
        $this->minor = '1' === $attrs['minor'];
65
        $this->length = (int)$attrs['length'];
66
        $this->lengthChange = (int)$attrs['length_change'];
67
        $this->user = $attrs['user'] ?? ($attrs['username'] ? new User($attrs['username']) : null);
68
        $this->comment = $attrs['comment'];
69
70
        if (isset($attrs['rev_sha1']) || isset($attrs['sha'])) {
71
            $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
        $this->reverted = isset($attrs['reverted']) ? (bool)$attrs['reverted'] : null;
78
    }
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
    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
            $page = Page::newFromRow($project, $rev);
92
            $rev['user'] = $user;
93
94
            return new self($page, $rev);
95
        }, $revs);
96
    }
97
98
    /**
99
     * Create an Edit object from a database row. Assume a revision JOIN on page with applicable fields.
100
     * @param Project $project
101
     * @param array $row
102
     * @return Edit
103
     */
104
    public static function newFromRow(Project $project, array $row): Edit
105
    {
106
        $page = Page::newFromRow($project, $row);
107
        return new self($page, $row);
108
    }
109
110
    /**
111
     * Unique identifier for this Edit, to be used in cache keys.
112
     * @see Repository::getCacheKey()
113
     * @return string
114
     */
115
    public function getCacheKey(): string
116
    {
117
        return (string)$this->id;
118
    }
119
120
    /**
121
     * ID of the edit.
122
     * @return int
123
     */
124
    public function getId(): int
125
    {
126
        return $this->id;
127
    }
128
129
    /**
130
     * Get the edit's timestamp.
131
     * @return DateTime
132
     */
133
    public function getTimestamp(): DateTime
134
    {
135
        return $this->timestamp;
136
    }
137
138
    /**
139
     * Get the edit's timestamp as a UTC string, as with YYYY-MM-DDTHH:MM:SS
140
     * @return string
141
     */
142
    public function getUTCTimestamp(): string
143
    {
144
        return $this->getTimestamp()->format('Y-m-d\TH:i:s');
145
    }
146
147
    /**
148
     * Year the revision was made.
149
     * @return string
150
     */
151
    public function getYear(): string
152
    {
153
        return $this->timestamp->format('Y');
154
    }
155
156
    /**
157
     * Get the numeric representation of the month the revision was made, with leading zeros.
158
     * @return string
159
     */
160
    public function getMonth(): string
161
    {
162
        return $this->timestamp->format('m');
163
    }
164
165
    /**
166
     * Whether or not this edit was a minor edit.
167
     * @return bool
168
     */
169
    public function getMinor(): bool
170
    {
171
        return $this->minor;
172
    }
173
174
    /**
175
     * Alias of getMinor()
176
     * @return bool Whether or not this edit was a minor edit
177
     */
178
    public function isMinor(): bool
179
    {
180
        return $this->getMinor();
181
    }
182
183
    /**
184
     * Length of the page as of this edit, in bytes.
185
     * @see Edit::getSize() Edit::getSize() for the size <em>change</em>.
186
     * @return int
187
     */
188
    public function getLength(): int
189
    {
190
        return $this->length;
191
    }
192
193
    /**
194
     * The diff size of this edit.
195
     * @return int Signed length change in bytes.
196
     */
197
    public function getSize(): int
198
    {
199
        return $this->lengthChange;
200
    }
201
202
    /**
203
     * Alias of getSize()
204
     * @return int The diff size of this edit
205
     */
206
    public function getLengthChange(): int
207
    {
208
        return $this->getSize();
209
    }
210
211
    /**
212
     * Get the user who made the edit.
213
     * @return User|null null can happen for instance if the username was suppressed.
214
     */
215
    public function getUser(): ?User
216
    {
217
        return $this->user;
218
    }
219
220
    /**
221
     * Set the User.
222
     * @param User $user
223
     */
224
    public function setUser(User $user): void
225
    {
226
        $this->user = $user;
227
    }
228
229
    /**
230
     * Get the edit summary.
231
     * @return string
232
     */
233
    public function getComment(): string
234
    {
235
        return (string)$this->comment;
236
    }
237
238
    /**
239
     * Get the edit summary (alias of Edit::getComment()).
240
     * @return string
241
     */
242
    public function getSummary(): string
243
    {
244
        return $this->getComment();
245
    }
246
247
    /**
248
     * Get the SHA-1 of the revision.
249
     * @return string|null
250
     */
251
    public function getSha(): ?string
252
    {
253
        return $this->sha;
254
    }
255
256
    /**
257
     * Was this edit reported as having been reverted?
258
     * The value for this is merely passed in from precomputed data.
259
     * @return bool|null
260
     */
261
    public function isReverted(): ?bool
262
    {
263
        return $this->reverted;
264
    }
265
266
    /**
267
     * Set the reverted property.
268
     * @param bool $revert
269
     */
270
    public function setReverted(bool $revert): void
271
    {
272
        $this->reverted = $revert;
273
    }
274
275
    /**
276
     * Get edit summary as 'wikified' HTML markup
277
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
278
     *   an API call. This should be used only if you fetched the page title via other
279
     *   means (SQL query), and is not from user input alone.
280
     * @return string Safe HTML
281
     */
282
    public function getWikifiedComment(bool $useUnnormalizedPageTitle = false): string
283
    {
284
        return self::wikifyString(
285
            $this->getSummary(),
286
            $this->getProject(),
287
            $this->page,
288
            $useUnnormalizedPageTitle
289
        );
290
    }
291
292
    /**
293
     * Public static method to wikify a summary, can be used on any arbitrary string.
294
     * Does NOT support section links unless you specify a page.
295
     * @param string $summary
296
     * @param Project $project
297
     * @param Page $page
298
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
299
     *   an API call. This should be used only if you fetched the page title via other
300
     *   means (SQL query), and is not from user input alone.
301
     * @static
302
     * @return string
303
     */
304
    public static function wikifyString(
305
        string $summary,
306
        Project $project,
307
        ?Page $page = null,
308
        bool $useUnnormalizedPageTitle = false
309
    ): string {
310
        $summary = htmlspecialchars(html_entity_decode($summary), ENT_NOQUOTES);
311
312
        // First link raw URLs. Courtesy of https://stackoverflow.com/a/11641499/604142
313
        $summary = preg_replace(
314
            '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s',
315
            '<a target="_blank" href="$1">$1</a>',
316
            $summary
317
        );
318
319
        $sectionMatch = null;
320
        $isSection = preg_match_all("/^\/\* (.*?) \*\//", $summary, $sectionMatch);
321
322
        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...
323
            $pageUrl = $project->getUrl(false) . str_replace(
324
                '$1',
325
                $page->getTitle($useUnnormalizedPageTitle),
326
                $project->getArticlePath()
327
            );
328
            $sectionTitle = $sectionMatch[1][0];
329
330
            // Must have underscores for the link to properly go to the section.
331
            $sectionTitleLink = htmlspecialchars(str_replace(' ', '_', $sectionTitle));
332
333
            $sectionWikitext = "<a target='_blank' href='$pageUrl#$sectionTitleLink'>&rarr;</a>" .
334
                "<em class='text-muted'>" . htmlspecialchars($sectionTitle) . ":</em> ";
335
            $summary = str_replace($sectionMatch[0][0], $sectionWikitext, $summary);
336
        }
337
338
        $linkMatch = null;
339
340
        while (preg_match_all("/\[\[:?(.*?)]]/", $summary, $linkMatch)) {
341
            $wikiLinkParts = explode('|', $linkMatch[1][0]);
342
            $wikiLinkPath = htmlspecialchars($wikiLinkParts[0]);
343
            $wikiLinkText = htmlspecialchars(
344
                $wikiLinkParts[1] ?? $wikiLinkPath
345
            );
346
347
            // Use normalized page title (underscored, capitalized).
348
            $pageUrl = $project->getUrl(false) . str_replace(
349
                '$1',
350
                ucfirst(str_replace(' ', '_', $wikiLinkPath)),
351
                $project->getArticlePath()
352
            );
353
354
            $link = "<a target='_blank' href='$pageUrl'>$wikiLinkText</a>";
355
            $summary = str_replace($linkMatch[0][0], $link, $summary);
356
        }
357
358
        return $summary;
359
    }
360
361
    /**
362
     * Get edit summary as 'wikified' HTML markup (alias of Edit::getWikifiedSummary()).
363
     * @return string
364
     */
365
    public function getWikifiedSummary(): string
366
    {
367
        return $this->getWikifiedComment();
368
    }
369
370
    /**
371
     * Get the project this edit was made on
372
     * @return Project
373
     */
374
    public function getProject(): Project
375
    {
376
        return $this->getPage()->getProject();
377
    }
378
379
    /**
380
     * Get the full URL to the diff of the edit
381
     * @return string
382
     */
383
    public function getDiffUrl(): string
384
    {
385
        $project = $this->getProject();
386
        $path = str_replace('$1', 'Special:Diff/' . $this->id, $project->getArticlePath());
387
        return rtrim($project->getUrl(), '/') . $path;
388
    }
389
390
    /**
391
     * Get the full permanent URL to the page at the time of the edit
392
     * @return string
393
     */
394
    public function getPermaUrl(): string
395
    {
396
        $project = $this->getProject();
397
        $path = str_replace('$1', 'Special:PermaLink/' . $this->id, $project->getArticlePath());
398
        return rtrim($project->getUrl(), '/') . $path;
399
    }
400
401
    /**
402
     * Was the edit a revert, based on the edit summary?
403
     * @param ContainerInterface $container The DI container.
404
     * @return bool
405
     */
406
    public function isRevert(ContainerInterface $container): bool
407
    {
408
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
409
        return $automatedEditsHelper->isRevert($this->comment, $this->getProject());
410
    }
411
412
    /**
413
     * Get the name of the tool that was used to make this edit.
414
     * @param ContainerInterface $container The DI container.
415
     * @return array|false The name of the tool that was used to make the edit
416
     */
417
    public function getTool(ContainerInterface $container)
418
    {
419
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
420
        return $automatedEditsHelper->getTool((string)$this->comment, $this->getProject());
421
    }
422
423
    /**
424
     * Was the edit (semi-)automated, based on the edit summary?
425
     * @param ContainerInterface $container
426
     * @return bool
427
     */
428
    public function isAutomated(ContainerInterface $container): bool
429
    {
430
        return (bool)$this->getTool($container);
431
    }
432
433
    /**
434
     * Was the edit made by a logged out user?
435
     * @return bool|null
436
     */
437
    public function isAnon(): ?bool
438
    {
439
        return $this->getUser() ? $this->getUser()->isAnon() : null;
440
    }
441
442
    /**
443
     * Get HTML for the diff of this Edit.
444
     * @return string|null Raw HTML, must be wrapped in a <table> tag. Null if no comparison could be made.
445
     */
446
    public function getDiffHtml(): ?string
447
    {
448
        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

448
        return $this->getRepository()->/** @scrutinizer ignore-call */ getDiffHtml($this);
Loading history...
449
    }
450
451
    /**
452
     * Formats the data as an array for use in JSON APIs.
453
     * @param bool $includeUsername False for most tools such as Global Contribs, AutoEdits, etc.
454
     * @return array
455
     * @internal This method assumes the Edit was constructed with data already filled in from a database query.
456
     */
457
    public function getForJson(bool $includeUsername = false): array
458
    {
459
        $nsName = $this->getProject()->getNamespaces()[$this->namespace];
460
        $ret = [
461
            'full_page_title' => $nsName.':'.$this->getPage()->getTitle(true),
462
            'page_title' => $this->getPage()->getTitle(true),
463
            'page_namespace' => $this->getPage()->getNamespace(),
464
            'rev_id' => $this->id,
465
            'timestamp' => $this->getUTCTimestamp(),
466
            'minor' => $this->minor,
467
            'length' => $this->length,
468
            'length_change' => $this->lengthChange,
469
            'comment' => $this->comment,
470
            'reverted' => $this->reverted,
471
        ];
472
        if ($includeUsername) {
473
            $ret = [ 'username' => $this->getUser()->getUsername() ] + $ret;
474
        }
475
476
        return $ret;
477
    }
478
479
    private function addFullPageTitlesAndContinue(array $data): array
0 ignored issues
show
Unused Code introduced by
The method addFullPageTitlesAndContinue() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
480
    {
481
        // Add full_page_title (in addition to the existing page_title and page_namespace keys).
482
        $data = array_map(function ($rev) {
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
483
            $nsName = $this->project->getNamespaces()[$rev['page_namespace']];
484
            $rev['full_page_title'] = $nsName.':'.$rev['page_title'];
485
            return $rev;
486
        }, $data);
487
    }
488
}
489