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/TopEdits.php (5 issues)

Labels
Severity
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
use App\Helper\AutomatedEditsHelper;
8
use App\Repository\TopEditsRepository;
9
10
/**
11
 * TopEdits returns the top-edited pages by a user.
12
 */
13
class TopEdits extends Model
14
{
15
    protected AutomatedEditsHelper $autoEditsHelper;
16
17
    /** @var string[]|Edit[] Top edits, either to a page or across namespaces. */
18
    protected array $topEdits = [];
19
20
    /** @var int Number of bytes added across all top edits. */
21
    protected int $totalAdded = 0;
22
23
    /** @var int Number of bytes removed across all top edits. */
24
    protected int $totalRemoved = 0;
25
26
    /** @var int Number of top edits marked as minor. */
27
    protected int $totalMinor = 0;
28
29
    /** @var int Number of automated top edits. */
30
    protected int $totalAutomated = 0;
31
32
    /** @var int Number of reverted top edits. */
33
    protected int $totalReverted = 0;
34
35
    /** @var int Which page of results to show. */
36
    protected int $pagination = 0;
37
38
    private const DEFAULT_LIMIT_SINGLE_NAMESPACE = 1000;
39
    private const DEFAULT_LIMIT_ALL_NAMESPACES = 20;
40
41
    /**
42
     * TopEdits constructor.
43
     * @param TopEditsRepository $repository
44
     * @param AutomatedEditsHelper $autoEditsHelper
45
     * @param Project $project
46
     * @param User $user
47
     * @param Page|null $page
48
     * @param string|int $namespace Namespace ID or 'all'.
49
     * @param int|false $start Start date as Unix timestamp.
50
     * @param int|false $end End date as Unix timestamp.
51
     * @param int|null $limit Number of rows to fetch. This defaults to DEFAULT_LIMIT_SINGLE_NAMESPACE if
52
     *   $this->namespace is a single namespace (int), and DEFAULT_LIMIT_ALL_NAMESPACES if $this->namespace is 'all'.
53
     * @param int $pagination Which page of results to show.
54
     */
55
    public function __construct(
56
        TopEditsRepository $repository,
57
        AutomatedEditsHelper $autoEditsHelper,
58
        Project $project,
59
        User $user,
60
        ?Page $page = null,
61
        $namespace = 0,
62
        $start = false,
63
        $end = false,
64
        ?int $limit = null,
65
        int $pagination = 0
66
    ) {
67
        $this->repository = $repository;
68
        $this->autoEditsHelper = $autoEditsHelper;
69
        $this->project = $project;
70
        $this->user = $user;
71
        $this->page = $page;
72
        $this->namespace = 'all' === $namespace ? 'all' : (int)$namespace;
73
        $this->start = $start;
74
        $this->end = $end;
75
        $this->pagination = $pagination;
76
77
        if (null !== $limit) {
78
            $this->limit = $limit;
79
        } else {
80
            $this->limit = 'all' === $this->namespace
81
                ? self::DEFAULT_LIMIT_ALL_NAMESPACES
82
                : self::DEFAULT_LIMIT_SINGLE_NAMESPACE;
83
        }
84
    }
85
86
    /**
87
     * Which page of results we're showing.
88
     * @return int
89
     */
90
    public function getPagination(): int
91
    {
92
        return $this->pagination;
93
    }
94
95
    /**
96
     * Get total number of bytes added.
97
     * @return int
98
     */
99
    public function getTotalAdded(): int
100
    {
101
        return $this->totalAdded;
102
    }
103
104
    /**
105
     * Get total number of bytes removed.
106
     * @return int
107
     */
108
    public function getTotalRemoved(): int
109
    {
110
        return $this->totalRemoved;
111
    }
112
113
    /**
114
     * Get total number of edits marked as minor.
115
     * @return int
116
     */
117
    public function getTotalMinor(): int
118
    {
119
        return $this->totalMinor;
120
    }
121
122
    /**
123
     * Get total number of automated edits.
124
     * @return int
125
     */
126
    public function getTotalAutomated(): int
127
    {
128
        return $this->totalAutomated;
129
    }
130
131
    /**
132
     * Get total number of edits that were reverted.
133
     * @return int
134
     */
135
    public function getTotalReverted(): int
136
    {
137
        return $this->totalReverted;
138
    }
139
140
    /**
141
     * Get the top edits data.
142
     * @return Edit[]
143
     */
144
    public function getTopEdits(): array
145
    {
146
        return $this->topEdits;
147
    }
148
149
    /**
150
     * Get the total number of top edits.
151
     * @return int
152
     */
153
    public function getNumTopEdits(): int
154
    {
155
        return count($this->topEdits);
156
    }
157
158
    /**
159
     * Get the average time between edits (in days).
160
     * @return float
161
     */
162
    public function getAtbe(): float
163
    {
164
        $firstDateTime = $this->topEdits[0]->getTimestamp();
165
        $lastDateTime = end($this->topEdits)->getTimestamp();
166
        $secs = $firstDateTime->getTimestamp() - $lastDateTime->getTimestamp();
167
        $days = $secs / (60 * 60 * 24);
168
        return $days / count($this->topEdits);
169
    }
170
171
    /**
172
     * Set the Page on the TopEdits instance.
173
     * @param Page $page
174
     */
175
    public function setPage(Page $page): void
176
    {
177
        $this->page = $page;
178
    }
179
180
    /**
181
     * Fetch and store all the data we need to show the TopEdits view.
182
     * This is the public method that should be called before using
183
     * the getter methods.
184
     */
185
    public function prepareData(): void
186
    {
187
        if (isset($this->page)) {
188
            $this->topEdits = $this->getTopEditsPage();
189
        } else {
190
            $this->topEdits = $this->getTopEditsNamespace();
191
        }
192
    }
193
194
    /**
195
     * Get the top edits by a user in the given namespace, or 'all' namespaces.
196
     * @return string[] Results keyed by namespace.
197
     */
198
    private function getTopEditsNamespace(): array
199
    {
200
        if ('all' === $this->namespace) {
201
            $pages = $this->repository->getTopEditsAllNamespaces(
0 ignored issues
show
The method getTopEditsAllNamespaces() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\TopEditsRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

201
            /** @scrutinizer ignore-call */ 
202
            $pages = $this->repository->getTopEditsAllNamespaces(
Loading history...
202
                $this->project,
203
                $this->user,
204
                $this->start,
205
                $this->end,
206
                $this->limit
207
            );
208
        } else {
209
            $pages = $this->repository->getTopEditsNamespace(
0 ignored issues
show
The method getTopEditsNamespace() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\TopEditsRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

209
            /** @scrutinizer ignore-call */ 
210
            $pages = $this->repository->getTopEditsNamespace(
Loading history...
210
                $this->project,
211
                $this->user,
212
                $this->namespace,
213
                $this->start,
214
                $this->end,
215
                $this->limit,
216
                $this->pagination
217
            );
218
        }
219
220
        return $this->formatTopPagesNamespace($pages);
221
    }
222
223
    /**
224
     * Get the total number of pages edited in the namespace.
225
     * @return int|null
226
     */
227
    public function getNumPagesNamespace(): ?int
228
    {
229
        if ('all' === $this->namespace) {
230
            return null;
231
        }
232
233
        return (int)$this->repository->countEditsNamespace(
0 ignored issues
show
The method countEditsNamespace() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\TopEditsRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

233
        return (int)$this->repository->/** @scrutinizer ignore-call */ countEditsNamespace(
Loading history...
234
            $this->project,
235
            $this->user,
236
            $this->namespace,
237
            $this->start,
238
            $this->end
239
        );
240
    }
241
242
    /**
243
     * Get the top edits to the given page.
244
     * @return Edit[]
245
     */
246
    private function getTopEditsPage(): array
247
    {
248
        $revs = $this->repository->getTopEditsPage(
0 ignored issues
show
The method getTopEditsPage() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\TopEditsRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

248
        /** @scrutinizer ignore-call */ 
249
        $revs = $this->repository->getTopEditsPage(
Loading history...
249
            $this->page,
250
            $this->user,
251
            $this->start,
252
            $this->end
253
        );
254
255
        return $this->formatTopEditsPage($revs);
256
    }
257
258
    /**
259
     * Format the results for top edits to a single page. This method also computes
260
     * totals for added/removed text, automated and reverted edits.
261
     * @param array[] $revs As returned by TopEditsRepository::getTopEditsPage.
262
     * @return Edit[]
263
     */
264
    private function formatTopEditsPage(array $revs): array
265
    {
266
        $edits = [];
267
268
        foreach ($revs as $revision) {
269
            // Check if the edit was reverted based on the edit summary of the following edit.
270
            // If so, update $revision so that when an Edit is instantiated, it will have the 'reverted' option set.
271
            if ($this->autoEditsHelper->isRevert($revision['parent_comment'], $this->project)) {
272
                $revision['reverted'] = 1;
273
            }
274
275
            $edits[] = $this->getEditAndIncrementCounts($revision);
276
        }
277
278
        return $edits;
279
    }
280
281
    /**
282
     * Create an Edit instance for the given revision, and increment running totals.
283
     * This is used by self::formatTopEditsPage().
284
     * @param string[] $revision Revision row as retrieved from the database.
285
     * @return Edit
286
     */
287
    private function getEditAndIncrementCounts(array $revision): Edit
288
    {
289
        $edit = $this->repository->getEdit($this->page, $revision);
0 ignored issues
show
The method getEdit() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\TopEditsRepository or App\Repository\PageInfoRepository. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

289
        /** @scrutinizer ignore-call */ 
290
        $edit = $this->repository->getEdit($this->page, $revision);
Loading history...
290
291
        if ($edit->isAutomated()) {
292
            $this->totalAutomated++;
293
        }
294
295
        if ($edit->isMinor()) {
296
            $this->totalMinor++;
297
        }
298
299
        if ($edit->isReverted()) {
300
            $this->totalReverted++;
301
        } else {
302
            // Length changes don't count if they were reverted.
303
            if ($revision['length_change'] > 0) {
304
                $this->totalAdded += (int)$revision['length_change'];
305
            } else {
306
                $this->totalRemoved += (int)$revision['length_change'];
307
            }
308
        }
309
310
        return $edit;
311
    }
312
313
    /**
314
     * Format the results to be keyed by namespace.
315
     * @param array $pages As returned by TopEditsRepository::getTopEditsNamespace()
316
     *   or TopEditsRepository::getTopEditsAllNamespaces().
317
     * @return array Same results but keyed by namespace.
318
     */
319
    private function formatTopPagesNamespace(array $pages): array
320
    {
321
        /** @var string[] $topEditedPages The top edited pages, keyed by namespace ID. */
322
        $topEditedPages = [];
323
324
        foreach ($pages as $page) {
325
            $nsId = (int)$page['namespace'];
326
            $page['page_title'] = str_replace('_', ' ', $page['page_title']);
327
328
            // FIXME: needs refactoring, done in PagesController::getPagepileResult() and AppExtension::titleWithNs().
329
            if (0 === $nsId) {
330
                $page['full_page_title'] = $page['page_title'];
331
            } else {
332
                $page['full_page_title'] = str_replace('_', ' ', (
333
                    $this->project->getNamespaces()[$page['namespace']] ?? ''
334
                ).':'.$page['page_title']);
335
            }
336
337
            if (array_key_exists('pa_class', $page)) {
338
                $page['assessment'] = array_merge(
339
                    ['class' => $page['pa_class']],
340
                    $this->project->getPageAssessments()->getClassAttrs($page['pa_class'])
341
                );
342
                unset($page['pa_class']);
343
            }
344
345
            if (isset($topEditedPages[$nsId])) {
346
                $topEditedPages[$nsId][] = $page;
347
            } else {
348
                $topEditedPages[$nsId] = [$page];
349
            }
350
        }
351
352
        return $topEditedPages;
353
    }
354
}
355