Passed
Push — master ( 1d6156...a73ad7 )
by MusikAnimal
05:41
created

Edit::getYear()   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 17
    public function __construct(Page $page, array $attrs = [])
51
    {
52 17
        $this->page = $page;
53
54
        // Copy over supported attributes
55 17
        $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 17
        if ($attrs['timestamp'] instanceof DateTime) {
59
            $this->timestamp = $attrs['timestamp'];
60
        } else {
61 17
            $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 17
        $this->minor = '1' === $attrs['minor'];
65 17
        $this->length = (int)$attrs['length'];
66 17
        $this->lengthChange = (int)$attrs['length_change'];
67 17
        $this->user = $attrs['user'] ?? ($attrs['username'] ? new User($attrs['username']) : null);
68 17
        $this->comment = $attrs['comment'];
69
70 17
        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 17
        $this->reverted = isset($attrs['reverted']) ? (bool)$attrs['reverted'] : null;
78 17
    }
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 6
    public function getId(): int
95
    {
96 6
        return $this->id;
97
    }
98
99
    /**
100
     * Get the edit's timestamp.
101
     * @return DateTime
102
     */
103 6
    public function getTimestamp(): DateTime
104
    {
105 6
        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
     * Get the edit summary.
183
     * @return string
184
     */
185 1
    public function getComment(): string
186
    {
187 1
        return (string)$this->comment;
188
    }
189
190
    /**
191
     * Get the edit summary (alias of Edit::getComment()).
192
     * @return string
193
     */
194 1
    public function getSummary(): string
195
    {
196 1
        return $this->getComment();
197
    }
198
199
    /**
200
     * Get the SHA-1 of the revision.
201
     * @return string|null
202
     */
203 4
    public function getSha(): ?string
204
    {
205 4
        return $this->sha;
206
    }
207
208
    /**
209
     * Was this edit reported as having been reverted?
210
     * The value for this is merely passed in from precomputed data.
211
     * @return bool|null
212
     */
213 5
    public function isReverted(): ?bool
214
    {
215 5
        return $this->reverted;
216
    }
217
218
    /**
219
     * Set the reverted property.
220
     * @param bool $revert
221
     */
222 4
    public function setReverted(bool $revert): void
223
    {
224 4
        $this->reverted = $revert;
225 4
    }
226
227
    /**
228
     * Get edit summary as 'wikified' HTML markup
229
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
230
     *   an API call. This should be used only if you fetched the page title via other
231
     *   means (SQL query), and is not from user input alone.
232
     * @return string Safe HTML
233
     */
234 1
    public function getWikifiedComment(bool $useUnnormalizedPageTitle = false): string
235
    {
236 1
        return self::wikifyString(
237 1
            $this->getSummary(),
238 1
            $this->getProject(),
239 1
            $this->page,
240 1
            $useUnnormalizedPageTitle
241
        );
242
    }
243
244
    /**
245
     * Public static method to wikify a summary, can be used on any arbitrary string.
246
     * Does NOT support section links unless you specify a page.
247
     * @param string $summary
248
     * @param Project $project
249
     * @param Page $page
250
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
251
     *   an API call. This should be used only if you fetched the page title via other
252
     *   means (SQL query), and is not from user input alone.
253
     * @static
254
     * @return string
255
     */
256 2
    public static function wikifyString(
257
        string $summary,
258
        Project $project,
259
        ?Page $page = null,
260
        bool $useUnnormalizedPageTitle = false
261
    ): string {
262 2
        $summary = htmlspecialchars(html_entity_decode($summary), ENT_NOQUOTES);
263
264
        // First link raw URLs. Courtesy of https://stackoverflow.com/a/11641499/604142
265 2
        $summary = preg_replace(
266 2
            '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s',
267 2
            '<a target="_blank" href="$1">$1</a>',
268 2
            $summary
269
        );
270
271 2
        $sectionMatch = null;
272 2
        $isSection = preg_match_all("/^\/\* (.*?) \*\//", $summary, $sectionMatch);
273
274 2
        if ($isSection && isset($page)) {
275
            $pageUrl = $project->getUrl(false) . str_replace(
276
                '$1',
277
                $page->getTitle($useUnnormalizedPageTitle),
278
                $project->getArticlePath()
279
            );
280
            $sectionTitle = $sectionMatch[1][0];
281
282
            // Must have underscores for the link to properly go to the section.
283
            $sectionTitleLink = htmlspecialchars(str_replace(' ', '_', $sectionTitle));
284
285
            $sectionWikitext = "<a target='_blank' href='$pageUrl#$sectionTitleLink'>&rarr;</a>" .
286
                "<em class='text-muted'>" . htmlspecialchars($sectionTitle) . ":</em> ";
287
            $summary = str_replace($sectionMatch[0][0], $sectionWikitext, $summary);
288
        }
289
290 2
        $linkMatch = null;
291
292 2
        while (preg_match_all("/\[\[:?(.*?)\]\]/", $summary, $linkMatch)) {
293 2
            $wikiLinkParts = explode('|', $linkMatch[1][0]);
294 2
            $wikiLinkPath = htmlspecialchars($wikiLinkParts[0]);
295 2
            $wikiLinkText = htmlspecialchars(
296 2
                $wikiLinkParts[1] ?? $wikiLinkPath
297
            );
298
299
            // Use normalized page title (underscored, capitalized).
300 2
            $pageUrl = $project->getUrl(false) . str_replace(
301 2
                '$1',
302 2
                ucfirst(str_replace(' ', '_', $wikiLinkPath)),
303 2
                $project->getArticlePath()
304
            );
305
306 2
            $link = "<a target='_blank' href='$pageUrl'>$wikiLinkText</a>";
307 2
            $summary = str_replace($linkMatch[0][0], $link, $summary);
308
        }
309
310 2
        return $summary;
311
    }
312
313
    /**
314
     * Get edit summary as 'wikified' HTML markup (alias of Edit::getWikifiedSummary()).
315
     * @return string
316
     */
317 1
    public function getWikifiedSummary(): string
318
    {
319 1
        return $this->getWikifiedComment();
320
    }
321
322
    /**
323
     * Get the project this edit was made on
324
     * @return Project
325
     */
326 12
    public function getProject(): Project
327
    {
328 12
        return $this->getPage()->getProject();
329
    }
330
331
    /**
332
     * Get the full URL to the diff of the edit
333
     * @return string
334
     */
335 1
    public function getDiffUrl(): string
336
    {
337 1
        $project = $this->getProject();
338 1
        $path = str_replace('$1', 'Special:Diff/' . $this->id, $project->getArticlePath());
339 1
        return rtrim($project->getUrl(), '/') . $path;
340
    }
341
342
    /**
343
     * Get the full permanent URL to the page at the time of the edit
344
     * @return string
345
     */
346 1
    public function getPermaUrl(): string
347
    {
348 1
        $project = $this->getProject();
349 1
        $path = str_replace('$1', 'Special:PermaLink/' . $this->id, $project->getArticlePath());
350 1
        return rtrim($project->getUrl(), '/') . $path;
351
    }
352
353
    /**
354
     * Was the edit a revert, based on the edit summary?
355
     * @param ContainerInterface $container The DI container.
356
     * @return bool
357
     */
358 5
    public function isRevert(ContainerInterface $container): bool
359
    {
360 5
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
361 5
        return $automatedEditsHelper->isRevert($this->comment, $this->getProject());
362
    }
363
364
    /**
365
     * Get the name of the tool that was used to make this edit.
366
     * @param ContainerInterface $container The DI container.
367
     * @return array|false The name of the tool that was used to make the edit
368
     */
369 7
    public function getTool(ContainerInterface $container)
370
    {
371 7
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
372 7
        return $automatedEditsHelper->getTool((string)$this->comment, $this->getProject());
373
    }
374
375
    /**
376
     * Was the edit (semi-)automated, based on the edit summary?
377
     * @param ContainerInterface $container [description]
378
     * @return bool
379
     */
380 2
    public function isAutomated(ContainerInterface $container): bool
381
    {
382 2
        return (bool)$this->getTool($container);
383
    }
384
385
    /**
386
     * Was the edit made by a logged out user?
387
     * @return bool|null
388
     */
389 5
    public function isAnon(): ?bool
390
    {
391 5
        return $this->getUser() ? $this->getUser()->isAnon() : null;
392
    }
393
}
394