Passed
Push — master ( 77644d...1d6156 )
by MusikAnimal
06:06
created

Edit::getPermaUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
ccs 4
cts 4
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
212
     */
213 1
    public function isReverted(): bool
214
    {
215 1
        return $this->reverted;
216
    }
217
218
    /**
219
     * Get edit summary as 'wikified' HTML markup
220
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
221
     *   an API call. This should be used only if you fetched the page title via other
222
     *   means (SQL query), and is not from user input alone.
223
     * @return string Safe HTML
224
     */
225 1
    public function getWikifiedComment(bool $useUnnormalizedPageTitle = false): string
226
    {
227 1
        return self::wikifyString(
228 1
            $this->getSummary(),
229 1
            $this->getProject(),
230 1
            $this->page,
231 1
            $useUnnormalizedPageTitle
232
        );
233
    }
234
235
    /**
236
     * Public static method to wikify a summary, can be used on any arbitrary string.
237
     * Does NOT support section links unless you specify a page.
238
     * @param string $summary
239
     * @param Project $project
240
     * @param Page $page
241
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
242
     *   an API call. This should be used only if you fetched the page title via other
243
     *   means (SQL query), and is not from user input alone.
244
     * @static
245
     * @return string
246
     */
247 2
    public static function wikifyString(
248
        string $summary,
249
        Project $project,
250
        ?Page $page = null,
251
        bool $useUnnormalizedPageTitle = false
252
    ): string {
253 2
        $summary = htmlspecialchars(html_entity_decode($summary), ENT_NOQUOTES);
254
255
        // First link raw URLs. Courtesy of https://stackoverflow.com/a/11641499/604142
256 2
        $summary = preg_replace(
257 2
            '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s',
258 2
            '<a target="_blank" href="$1">$1</a>',
259 2
            $summary
260
        );
261
262 2
        $sectionMatch = null;
263 2
        $isSection = preg_match_all("/^\/\* (.*?) \*\//", $summary, $sectionMatch);
264
265 2
        if ($isSection && isset($page)) {
266
            $pageUrl = $project->getUrl(false) . str_replace(
267
                '$1',
268
                $page->getTitle($useUnnormalizedPageTitle),
269
                $project->getArticlePath()
270
            );
271
            $sectionTitle = $sectionMatch[1][0];
272
273
            // Must have underscores for the link to properly go to the section.
274
            $sectionTitleLink = htmlspecialchars(str_replace(' ', '_', $sectionTitle));
275
276
            $sectionWikitext = "<a target='_blank' href='$pageUrl#$sectionTitleLink'>&rarr;</a>" .
277
                "<em class='text-muted'>" . htmlspecialchars($sectionTitle) . ":</em> ";
278
            $summary = str_replace($sectionMatch[0][0], $sectionWikitext, $summary);
279
        }
280
281 2
        $linkMatch = null;
282
283 2
        while (preg_match_all("/\[\[:?(.*?)\]\]/", $summary, $linkMatch)) {
284 2
            $wikiLinkParts = explode('|', $linkMatch[1][0]);
285 2
            $wikiLinkPath = htmlspecialchars($wikiLinkParts[0]);
286 2
            $wikiLinkText = htmlspecialchars(
287 2
                $wikiLinkParts[1] ?? $wikiLinkPath
288
            );
289
290
            // Use normalized page title (underscored, capitalized).
291 2
            $pageUrl = $project->getUrl(false) . str_replace(
292 2
                '$1',
293 2
                ucfirst(str_replace(' ', '_', $wikiLinkPath)),
294 2
                $project->getArticlePath()
295
            );
296
297 2
            $link = "<a target='_blank' href='$pageUrl'>$wikiLinkText</a>";
298 2
            $summary = str_replace($linkMatch[0][0], $link, $summary);
299
        }
300
301 2
        return $summary;
302
    }
303
304
    /**
305
     * Get edit summary as 'wikified' HTML markup (alias of Edit::getWikifiedSummary()).
306
     * @return string
307
     */
308 1
    public function getWikifiedSummary(): string
309
    {
310 1
        return $this->getWikifiedComment();
311
    }
312
313
    /**
314
     * Get the project this edit was made on
315
     * @return Project
316
     */
317 12
    public function getProject(): Project
318
    {
319 12
        return $this->getPage()->getProject();
320
    }
321
322
    /**
323
     * Get the full URL to the diff of the edit
324
     * @return string
325
     */
326 1
    public function getDiffUrl(): string
327
    {
328 1
        $project = $this->getProject();
329 1
        $path = str_replace('$1', 'Special:Diff/' . $this->id, $project->getArticlePath());
330 1
        return rtrim($project->getUrl(), '/') . $path;
331
    }
332
333
    /**
334
     * Get the full permanent URL to the page at the time of the edit
335
     * @return string
336
     */
337 1
    public function getPermaUrl(): string
338
    {
339 1
        $project = $this->getProject();
340 1
        $path = str_replace('$1', 'Special:PermaLink/' . $this->id, $project->getArticlePath());
341 1
        return rtrim($project->getUrl(), '/') . $path;
342
    }
343
344
    /**
345
     * Was the edit a revert, based on the edit summary?
346
     * @param ContainerInterface $container The DI container.
347
     * @return bool
348
     */
349 5
    public function isRevert(ContainerInterface $container): bool
350
    {
351 5
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
352 5
        return $automatedEditsHelper->isRevert($this->comment, $this->getProject());
353
    }
354
355
    /**
356
     * Get the name of the tool that was used to make this edit.
357
     * @param ContainerInterface $container The DI container.
358
     * @return array|false The name of the tool that was used to make the edit
359
     */
360 7
    public function getTool(ContainerInterface $container)
361
    {
362 7
        $automatedEditsHelper = $container->get('app.automated_edits_helper');
363 7
        return $automatedEditsHelper->getTool((string)$this->comment, $this->getProject());
364
    }
365
366
    /**
367
     * Was the edit (semi-)automated, based on the edit summary?
368
     * @param ContainerInterface $container [description]
369
     * @return bool
370
     */
371 2
    public function isAutomated(ContainerInterface $container): bool
372
    {
373 2
        return (bool)$this->getTool($container);
374
    }
375
376
    /**
377
     * Was the edit made by a logged out user?
378
     * @return bool|null
379
     */
380 5
    public function isAnon(): ?bool
381
    {
382 5
        return $this->getUser() ? $this->getUser()->isAnon() : null;
383
    }
384
}
385