Passed
Push — master ( d0265c...1b1a87 )
by MusikAnimal
07:34
created

Edit::getComment()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

409
        return $this->getRepository()->/** @scrutinizer ignore-call */ getDiffHtml($this);
Loading history...
410
    }
411
}
412