Passed
Branch master (1d2ffa)
by MusikAnimal
06:54
created

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