Issues (196)

Security Analysis    6 potential vulnerabilities

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection (4)
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection (1)
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Model/Edit.php (3 issues)

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
use App\Repository\EditRepository;
8
use App\Repository\PageRepository;
9
use App\Repository\UserRepository;
10
use DateTime;
11
12
/**
13
 * An Edit is a single edit to a page on one project.
14
 */
15
class Edit extends Model
16
{
17
    public const DELETED_TEXT = 1;
18
    public const DELETED_COMMENT = 2;
19
    public const DELETED_USER = 4;
20
    public const DELETED_RESTRICTED = 8;
21
22
    protected UserRepository $userRepo;
23
24
    /** @var int ID of the revision */
25
    protected int $id;
26
27
    /** @var DateTime Timestamp of the revision */
28
    protected DateTime $timestamp;
29
30
    /** @var bool Whether or not this edit was a minor edit */
31
    protected bool $minor;
32
33
    /** @var int|null Length of the page as of this edit, in bytes */
34
    protected ?int $length;
35
36
    /** @var int|null The diff size of this edit */
37
    protected ?int $lengthChange;
38
39
    /** @var string The edit summary */
40
    protected string $comment;
41
42
    /** @var string|null The SHA-1 of the wikitext as of the revision. */
43
    protected ?string $sha = null;
44
45
    /** @var bool|null Whether this edit was later reverted. */
46
    protected ?bool $reverted;
47
48
    /** @var int Deletion status of the revision. */
49
    protected int $deleted;
50
51
    /**
52
     * Edit constructor.
53
     * @param EditRepository $repository
54
     * @param UserRepository $userRepo
55
     * @param Page $page
56
     * @param string[] $attrs Attributes, as retrieved by PageRepository::getRevisions()
57
     */
58
    public function __construct(EditRepository $repository, UserRepository $userRepo, Page $page, array $attrs = [])
59
    {
60
        $this->repository = $repository;
61
        $this->userRepo = $userRepo;
62
        $this->page = $page;
63
64
        // Copy over supported attributes
65
        $this->id = isset($attrs['id']) ? (int)$attrs['id'] : (int)$attrs['rev_id'];
66
67
        // Allow DateTime or string (latter assumed to be of format YmdHis)
68
        if ($attrs['timestamp'] instanceof DateTime) {
69
            $this->timestamp = $attrs['timestamp'];
70
        } else {
71
            $this->timestamp = DateTime::createFromFormat('YmdHis', $attrs['timestamp']);
72
        }
73
74
        $this->deleted = (int)($attrs['rev_deleted'] ?? 0);
75
76
        if (($this->deleted & self::DELETED_USER) || ($this->deleted & self::DELETED_RESTRICTED)) {
77
            $this->user = null;
78
        } else {
79
            $this->user = $attrs['user'] ?? ($attrs['username'] ? new User($this->userRepo, $attrs['username']) : null);
80
        }
81
82
        $this->minor = 1 === (int)$attrs['minor'];
83
        $this->length = isset($attrs['length']) ? (int)$attrs['length'] : null;
84
        $this->lengthChange = isset($attrs['length_change']) ? (int)$attrs['length_change'] : null;
85
        $this->comment = $attrs['comment'] ?? '';
86
87
        if (isset($attrs['rev_sha1']) || isset($attrs['sha'])) {
88
            $this->sha = $attrs['rev_sha1'] ?? $attrs['sha'];
89
        }
90
91
        // This can be passed in to save as a property on the Edit instance.
92
        // Note that the Edit class knows nothing about it's value, and
93
        // is not capable of detecting whether the given edit was actually reverted.
94
        $this->reverted = isset($attrs['reverted']) ? (bool)$attrs['reverted'] : null;
95
    }
96
97
    /**
98
     * Get Edits given revision rows (JOINed on the page table).
99
     * @param PageRepository $pageRepo
100
     * @param EditRepository $editRepo
101
     * @param UserRepository $userRepo
102
     * @param Project $project
103
     * @param User $user
104
     * @param array $revs Each must contain 'page_title' and 'namespace'.
105
     * @return Edit[]
106
     */
107
    public static function getEditsFromRevs(
108
        PageRepository $pageRepo,
109
        EditRepository $editRepo,
110
        UserRepository $userRepo,
111
        Project $project,
112
        User $user,
113
        array $revs
114
    ): array {
115
        return array_map(function ($rev) use ($pageRepo, $editRepo, $userRepo, $project, $user) {
116
            /** Page object to be passed to the Edit constructor. */
117
            $page = Page::newFromRow($pageRepo, $project, $rev);
118
            $rev['user'] = $user;
119
120
            return new self($editRepo, $userRepo, $page, $rev);
121
        }, $revs);
122
    }
123
124
    /**
125
     * Unique identifier for this Edit, to be used in cache keys.
126
     * @see Repository::getCacheKey()
127
     * @return string
128
     */
129
    public function getCacheKey(): string
130
    {
131
        return (string)$this->id;
132
    }
133
134
    /**
135
     * ID of the edit.
136
     * @return int
137
     */
138
    public function getId(): int
139
    {
140
        return $this->id;
141
    }
142
143
    /**
144
     * Get the edit's timestamp.
145
     * @return DateTime
146
     */
147
    public function getTimestamp(): DateTime
148
    {
149
        return $this->timestamp;
150
    }
151
152
    /**
153
     * Get the edit's timestamp as a UTC string, as with YYYY-MM-DDTHH:MM:SSZ
154
     * @return string
155
     */
156
    public function getUTCTimestamp(): string
157
    {
158
        return $this->getTimestamp()->format('Y-m-d\TH:i:s\Z');
159
    }
160
161
    /**
162
     * Year the revision was made.
163
     * @return string
164
     */
165
    public function getYear(): string
166
    {
167
        return $this->timestamp->format('Y');
168
    }
169
170
    /**
171
     * Get the numeric representation of the month the revision was made, with leading zeros.
172
     * @return string
173
     */
174
    public function getMonth(): string
175
    {
176
        return $this->timestamp->format('m');
177
    }
178
179
    /**
180
     * Whether or not this edit was a minor edit.
181
     * @return bool
182
     */
183
    public function getMinor(): bool
184
    {
185
        return $this->minor;
186
    }
187
188
    /**
189
     * Alias of getMinor()
190
     * @return bool Whether or not this edit was a minor edit
191
     */
192
    public function isMinor(): bool
193
    {
194
        return $this->getMinor();
195
    }
196
197
    /**
198
     * Length of the page as of this edit, in bytes.
199
     * @see Edit::getSize() Edit::getSize() for the size <em>change</em>.
200
     * @return int|null
201
     */
202
    public function getLength(): ?int
203
    {
204
        return $this->length;
205
    }
206
207
    /**
208
     * The diff size of this edit.
209
     * @return int|null Signed length change in bytes.
210
     */
211
    public function getSize(): ?int
212
    {
213
        return $this->lengthChange;
214
    }
215
216
    /**
217
     * Alias of getSize()
218
     * @return int|null The diff size of this edit
219
     */
220
    public function getLengthChange(): ?int
221
    {
222
        return $this->getSize();
223
    }
224
225
    /**
226
     * Get the user who made the edit.
227
     * @return User|null null can happen for instance if the username was suppressed.
228
     */
229
    public function getUser(): ?User
230
    {
231
        return $this->user;
232
    }
233
234
    /**
235
     * Get the edit summary.
236
     * @return string
237
     */
238
    public function getComment(): string
239
    {
240
        return (string)$this->comment;
241
    }
242
243
    /**
244
     * Get the edit summary (alias of Edit::getComment()).
245
     * @return string
246
     */
247
    public function getSummary(): string
248
    {
249
        return $this->getComment();
250
    }
251
252
    /**
253
     * Get the SHA-1 of the revision.
254
     * @return string|null
255
     */
256
    public function getSha(): ?string
257
    {
258
        return $this->sha;
259
    }
260
261
    /**
262
     * Was this edit reported as having been reverted?
263
     * The value for this is merely passed in from precomputed data.
264
     * @return bool|null
265
     */
266
    public function isReverted(): ?bool
267
    {
268
        return $this->reverted;
269
    }
270
271
    /**
272
     * Set the reverted property.
273
     * @param bool $reverted
274
     */
275
    public function setReverted(bool $reverted): void
276
    {
277
        $this->reverted = $reverted;
278
    }
279
280
    /**
281
     * Get deletion status of the revision.
282
     * @return int
283
     */
284
    public function getDeleted(): int
285
    {
286
        return $this->deleted;
287
    }
288
289
    /**
290
     * Was the username deleted from public view?
291
     * @return bool
292
     */
293
    public function deletedUser(): bool
294
    {
295
        return ($this->deleted & self::DELETED_USER) > 0;
296
    }
297
298
    /**
299
     * Was the edit summary deleted from public view?
300
     * @return bool
301
     */
302
    public function deletedSummary(): bool
303
    {
304
        return ($this->deleted & self::DELETED_COMMENT) > 0;
305
    }
306
307
    /**
308
     * Get edit summary as 'wikified' HTML markup
309
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
310
     *   an API call. This should be used only if you fetched the page title via other
311
     *   means (SQL query), and is not from user input alone.
312
     * @return string Safe HTML
313
     */
314
    public function getWikifiedComment(bool $useUnnormalizedPageTitle = false): string
315
    {
316
        return self::wikifyString(
317
            $this->getSummary(),
318
            $this->getProject(),
319
            $this->page,
320
            $useUnnormalizedPageTitle
321
        );
322
    }
323
324
    /**
325
     * Public static method to wikify a summary, can be used on any arbitrary string.
326
     * Does NOT support section links unless you specify a page.
327
     * @param string $summary
328
     * @param Project $project
329
     * @param Page|null $page
330
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
331
     *   an API call. This should be used only if you fetched the page title via other
332
     *   means (SQL query), and is not from user input alone.
333
     * @static
334
     * @return string
335
     */
336
    public static function wikifyString(
337
        string $summary,
338
        Project $project,
339
        ?Page $page = null,
340
        bool $useUnnormalizedPageTitle = false
341
    ): string {
342
        $summary = htmlspecialchars(html_entity_decode($summary), ENT_NOQUOTES);
343
344
        // First link raw URLs. Courtesy of https://stackoverflow.com/a/11641499/604142
345
        $summary = preg_replace(
346
            '%\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))%s',
347
            '<a target="_blank" href="$1">$1</a>',
348
            $summary
349
        );
350
351
        $sectionMatch = null;
352
        $isSection = preg_match_all("/^\/\* (.*?) \*\//", $summary, $sectionMatch);
353
354
        if ($isSection && isset($page)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isSection of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
355
            $pageUrl = $project->getUrlForPage($page->getTitle($useUnnormalizedPageTitle));
356
            $sectionTitle = $sectionMatch[1][0];
357
358
            // Must have underscores for the link to properly go to the section.
359
            $sectionTitleLink = htmlspecialchars(str_replace(' ', '_', $sectionTitle));
360
361
            $sectionWikitext = "<a target='_blank' href='$pageUrl#$sectionTitleLink'>&rarr;</a>" .
362
                "<em class='text-muted'>" . htmlspecialchars($sectionTitle) . ":</em> ";
363
            $summary = str_replace($sectionMatch[0][0], $sectionWikitext, $summary);
364
        }
365
366
        $linkMatch = null;
367
368
        while (preg_match_all("/\[\[:?(.*?)]]/", $summary, $linkMatch)) {
369
            $wikiLinkParts = explode('|', $linkMatch[1][0]);
370
            $wikiLinkPath = htmlspecialchars($wikiLinkParts[0]);
371
            $wikiLinkText = htmlspecialchars(
372
                $wikiLinkParts[1] ?? $wikiLinkPath
373
            );
374
375
            // Use normalized page title (underscored, capitalized).
376
            $pageUrl = $project->getUrlForPage(ucfirst(str_replace(' ', '_', $wikiLinkPath)));
377
378
            $link = "<a target='_blank' href='$pageUrl'>$wikiLinkText</a>";
379
            $summary = str_replace($linkMatch[0][0], $link, $summary);
380
        }
381
382
        return $summary;
383
    }
384
385
    /**
386
     * Get edit summary as 'wikified' HTML markup (alias of Edit::getWikifiedComment()).
387
     * @return string
388
     */
389
    public function getWikifiedSummary(): string
390
    {
391
        return $this->getWikifiedComment();
392
    }
393
394
    /**
395
     * Get the project this edit was made on
396
     * @return Project
397
     */
398
    public function getProject(): Project
399
    {
400
        return $this->getPage()->getProject();
401
    }
402
403
    /**
404
     * Get the full URL to the diff of the edit
405
     * @return string
406
     */
407
    public function getDiffUrl(): string
408
    {
409
        return rtrim($this->getProject()->getUrlForPage('Special:Diff/' . $this->id), '/');
410
    }
411
412
    /**
413
     * Get the full permanent URL to the page at the time of the edit
414
     * @return string
415
     */
416
    public function getPermaUrl(): string
417
    {
418
        return rtrim($this->getProject()->getUrlForPage('Special:PermaLink/' . $this->id), '/');
419
    }
420
421
    /**
422
     * Was the edit a revert, based on the edit summary?
423
     * @return bool
424
     */
425
    public function isRevert(): bool
426
    {
427
        return $this->repository->getAutoEditsHelper()->isRevert($this->comment, $this->getProject());
0 ignored issues
show
The method getAutoEditsHelper() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\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

427
        return $this->repository->/** @scrutinizer ignore-call */ getAutoEditsHelper()->isRevert($this->comment, $this->getProject());
Loading history...
428
    }
429
430
    /**
431
     * Get the name of the tool that was used to make this edit.
432
     * @return array|null The name of the tool(s) that was used to make the edit.
433
     */
434
    public function getTool(): ?array
435
    {
436
        return $this->repository->getAutoEditsHelper()->getTool($this->comment, $this->getProject());
437
    }
438
439
    /**
440
     * Was the edit (semi-)automated, based on the edit summary?
441
     * @return bool
442
     */
443
    public function isAutomated(): bool
444
    {
445
        return (bool)$this->getTool();
446
    }
447
448
    /**
449
     * Was the edit made by a logged out user?
450
     * @return bool|null
451
     */
452
    public function isAnon(): ?bool
453
    {
454
        return $this->getUser() ? $this->getUser()->isAnon() : null;
455
    }
456
457
    /**
458
     * Get HTML for the diff of this Edit.
459
     * @return string|null Raw HTML, must be wrapped in a <table> tag. Null if no comparison could be made.
460
     */
461
    public function getDiffHtml(): ?string
462
    {
463
        return $this->repository->getDiffHtml($this);
0 ignored issues
show
The method getDiffHtml() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\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

463
        return $this->repository->/** @scrutinizer ignore-call */ getDiffHtml($this);
Loading history...
464
    }
465
466
    /**
467
     * Formats the data as an array for use in JSON APIs.
468
     * @param bool $includeProject
469
     * @return array
470
     * @internal This method assumes the Edit was constructed with data already filled in from a database query.
471
     */
472
    public function getForJson(bool $includeProject = false): array
473
    {
474
        $nsId = $this->getPage()->getNamespace();
475
        $pageTitle = $this->getPage()->getTitle(true);
476
477
        if ($nsId > 0) {
478
            $nsName = $this->getProject()->getNamespaces()[$nsId];
479
            $pageTitle = preg_replace("/^$nsName:/", '', $pageTitle);
480
        }
481
482
        $ret = [
483
            'page_title' => str_replace('_', ' ', $pageTitle),
484
            'namespace' => $nsId,
485
        ];
486
        if ($includeProject) {
487
            $ret += ['project' => $this->getProject()->getDomain()];
488
        }
489
        if ($this->getUser()) {
490
            $ret += ['username' => $this->getUser()->getUsername()];
491
        }
492
        $ret += [
493
            'rev_id' => $this->id,
494
            'timestamp' => $this->getUTCTimestamp(),
495
            'minor' => $this->minor,
496
            'length' => $this->length,
497
            'length_change' => $this->lengthChange,
498
            'comment' => $this->comment,
499
        ];
500
        if (null !== $this->reverted) {
501
            $ret['reverted'] = $this->reverted;
502
        }
503
504
        return $ret;
505
    }
506
}
507