|
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']); |
|
|
|
|
|
|
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'>→</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); |
|
|
|
|
|
|
428
|
|
|
} |
|
429
|
|
|
} |
|
430
|
|
|
|
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
$accountIdthat can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theidproperty of an instance of theAccountclass. 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.