Test Failed
Push — master ( 78ede5...a61c57 )
by MusikAnimal
07:20
created

Edit::getEditsFromRevs()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.125

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 3
dl 0
loc 9
ccs 1
cts 2
cp 0.5
crap 1.125
rs 10
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']);
0 ignored issues
show
Documentation Bug introduced by
It seems like DateTime::createFromForm...', $attrs['timestamp']) can also be of type false. However, the property $timestamp is declared as type DateTime. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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

427
        return $this->getRepository()->/** @scrutinizer ignore-call */ getDiffHtml($this);
Loading history...
428
    }
429
}
430