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/PageAssessments.php (2 issues)

Labels
Severity
1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
use App\Repository\PageAssessmentsRepository;
8
9
/**
10
 * A PageAssessments is responsible for handling logic around
11
 * processing page assessments of a given set of Pages on a Project.
12
 * @see https://www.mediawiki.org/wiki/Extension:PageAssessments
13
 */
14
class PageAssessments extends Model
15
{
16
    /**
17
     * Namespaces in which there may be page assessments.
18
     * @var int[]
19
     * @todo Always JOIN on page_assessments and only display the data if it exists.
20
     */
21
    public const SUPPORTED_NAMESPACES = [
22
        // Core namespaces
23
        ...[0, 4, 6, 10, 12, 14],
24
        // Custom namespaces
25
        ...[
26
            100, // Portal
27
            102, // WikiProject (T360774)
28
            108, // Book
29
            118, // Draft
30
            828, // Module
31
        ],
32
    ];
33
34
    /** @var array|null The assessments config. */
35
    protected ?array $config;
36
37
    /**
38
     * Create a new PageAssessments.
39
     * @param PageAssessmentsRepository $repository
40
     * @param Project $project
41
     */
42
    public function __construct(PageAssessmentsRepository $repository, Project $project)
43
    {
44
        $this->repository = $repository;
45
        $this->project = $project;
46
    }
47
48
    /**
49
     * Get page assessments configuration for the Project and cache in static variable.
50
     * @return string[][][]|null As defined in config/assessments.yaml, or false if none exists.
51
     */
52
    public function getConfig(): ?array
53
    {
54
        if (!isset($this->config)) {
55
            return $this->config = $this->repository->getConfig($this->project);
0 ignored issues
show
The method getConfig() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageAssessmentsRepository or App\Repository\AdminStatsRepository. ( Ignorable by Annotation )

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

55
            return $this->config = $this->repository->/** @scrutinizer ignore-call */ getConfig($this->project);
Loading history...
56
        }
57
58
        return $this->config;
59
    }
60
61
    /**
62
     * Is the given namespace supported in Page Assessments?
63
     * @param  int $nsId Namespace ID.
64
     * @return bool
65
     */
66
    public function isSupportedNamespace(int $nsId): bool
67
    {
68
        return $this->isEnabled() && in_array($nsId, self::SUPPORTED_NAMESPACES);
69
    }
70
71
    /**
72
     * Does this project support page assessments?
73
     * @return bool
74
     */
75
    public function isEnabled(): bool
76
    {
77
        return (bool)$this->getConfig();
78
    }
79
80
    /**
81
     * Does this project have importance ratings through Page Assessments?
82
     * @return bool
83
     */
84
    public function hasImportanceRatings(): bool
85
    {
86
        $config = $this->getConfig();
87
        return isset($config['importance']);
88
    }
89
90
    /**
91
     * Get the image URL of the badge for the given page assessment.
92
     * @param string|null $class Valid classification for project, such as 'Start', 'GA', etc. Null for unknown.
93
     * @param bool $filenameOnly Get only the filename, not the URL.
94
     * @return string URL to image.
95
     */
96
    public function getBadgeURL(?string $class, bool $filenameOnly = false): string
97
    {
98
        $config = $this->getConfig();
99
100
        if (isset($config['class'][$class])) {
101
            $url = 'https://upload.wikimedia.org/wikipedia/commons/'.$config['class'][$class]['badge'];
102
        } elseif (isset($config['class']['Unknown'])) {
103
            $url = 'https://upload.wikimedia.org/wikipedia/commons/'.$config['class']['Unknown']['badge'];
104
        } else {
105
            $url = "";
106
        }
107
108
        if ($filenameOnly) {
109
            $parts = explode('/', $url);
110
            return end($parts);
111
        }
112
113
        return $url;
114
    }
115
116
    /**
117
     * Get the single overall assessment of the given page.
118
     * @param Page $page
119
     * @return string[]|false With keys 'value' and 'badge', or false if assessments are unsupported.
120
     */
121
    public function getAssessment(Page $page)
122
    {
123
        if (!$this->isEnabled() || !$this->isSupportedNamespace($page->getNamespace())) {
124
            return false;
125
        }
126
127
        $data = $this->repository->getAssessments($page, true);
0 ignored issues
show
The method getAssessments() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageAssessmentsRepository. ( Ignorable by Annotation )

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

127
        /** @scrutinizer ignore-call */ 
128
        $data = $this->repository->getAssessments($page, true);
Loading history...
128
129
        if (isset($data[0])) {
130
            return $this->getClassFromAssessment($data[0]);
131
        }
132
133
        // 'Unknown' class.
134
        return $this->getClassFromAssessment(['class' => '']);
135
    }
136
137
    /**
138
     * Get assessments for the given Page.
139
     * @param Page $page
140
     * @return string[]|null null if unsupported, or array in the format of:
141
     *         [
142
     *             'assessment' => [
143
     *                 // overall assessment
144
     *                 'badge' => 'https://upload.wikimedia.org/wikipedia/commons/b/bc/Featured_article_star.svg',
145
     *                 'color' => '#9CBDFF',
146
     *                 'category' => 'Category:FA-Class articles',
147
     *                 'class' => 'FA',
148
     *             ]
149
     *             'wikiprojects' => [
150
     *                 'Biography' => [
151
     *                     'assessment' => 'C',
152
     *                     'badge' => 'url',
153
     *                 ],
154
     *                 ...
155
     *             ],
156
     *             'wikiproject_prefix' => 'Wikipedia:WikiProject_',
157
     *         ]
158
     * @todo Add option to get ORES prediction.
159
     */
160
    public function getAssessments(Page $page): ?array
161
    {
162
        if (!$this->isEnabled() || !$this->isSupportedNamespace($page->getNamespace())) {
163
            return null;
164
        }
165
166
        $config = $this->getConfig();
167
        $data = $this->repository->getAssessments($page);
168
169
        // Set the default decorations for the overall assessment.
170
        // This will be replaced with the first valid class defined for any WikiProject.
171
        $overallAssessment = array_merge(['class' => '???'], $config['class']['Unknown']);
172
        $overallAssessment['badge'] = $this->getBadgeURL($overallAssessment['badge']);
173
174
        $decoratedAssessments = [];
175
176
        // Go through each raw assessment data from the database, and decorate them
177
        // with the colours and badges as retrieved from the XTools assessments config.
178
        foreach ($data as $assessment) {
179
            $assessment['class'] = $this->getClassFromAssessment($assessment);
180
181
            // Replace the overall assessment with the first non-empty assessment.
182
            if ('???' === $overallAssessment['class'] && '???' !== $assessment['class']['value']) {
183
                $overallAssessment['class'] = $assessment['class']['value'];
184
                $overallAssessment['color'] = $assessment['class']['color'];
185
                $overallAssessment['category'] = $assessment['class']['category'];
186
                $overallAssessment['badge'] = $assessment['class']['badge'];
187
            }
188
189
            $assessment['importance'] = $this->getImportanceFromAssessment($assessment);
190
191
            $decoratedAssessments[$assessment['wikiproject']] = $assessment;
192
        }
193
194
        // Don't show 'Unknown' assessment outside of the mainspace.
195
        if (0 !== $page->getNamespace() && '???' === $overallAssessment['class']) {
196
            return [];
197
        }
198
199
        return [
200
            'assessment' => $overallAssessment,
201
            'wikiprojects' => $decoratedAssessments,
202
            'wikiproject_prefix' => $config['wikiproject_prefix'],
203
        ];
204
    }
205
206
    /**
207
     * Get the class attributes for the given class value, as fetched from the config.
208
     * @param string|null $classValue Such as 'FA', 'GA', 'Start', etc.
209
     * @return string[] Attributes as fetched from the XTools assessments config.
210
     */
211
    public function getClassAttrs(?string $classValue): array
212
    {
213
        $classValue = $classValue ?: 'Unknown';
214
        return $this->getConfig()['class'][$classValue] ?? $this->getConfig()['class']['Unknown'];
215
    }
216
217
    /**
218
     * Get the properties of the assessment class, including:
219
     *   'value' (class name in plain text),
220
     *   'color' (as hex RGB),
221
     *   'badge' (full URL to assessment badge),
222
     *   'category' (wiki path to related class category).
223
     * @param array $assessment
224
     * @return array Decorated class assessment.
225
     */
226
    private function getClassFromAssessment(array $assessment): array
227
    {
228
        $classValue = $assessment['class'];
229
230
        // Use ??? as the presented value when the class is unknown or is not defined in the config
231
        if ('Unknown' === $classValue || '' === $classValue || !isset($this->getConfig()['class'][$classValue])) {
232
            return array_merge($this->getClassAttrs('Unknown'), [
233
                'value' => '???',
234
                'badge' => $this->getBadgeURL('Unknown'),
235
            ]);
236
        }
237
238
        // Known class.
239
        $classAttrs = $this->getClassAttrs($classValue);
240
        $class = [
241
            'value' => $classValue,
242
            'color' => $classAttrs['color'],
243
            'category' => $classAttrs['category'],
244
        ];
245
246
        // add full URL to badge icon
247
        if ('' !== $classAttrs['badge']) {
248
            $class['badge'] = $this->getBadgeURL($classValue);
249
        }
250
251
        return $class;
252
    }
253
254
    /**
255
     * Get the properties of the assessment importance, including:
256
     *   'value' (importance in plain text),
257
     *   'color' (as hex RGB),
258
     *   'weight' (integer, 0 is lowest importance),
259
     *   'category' (wiki path to the related importance category).
260
     * @param  array $assessment
261
     * @return array|null Decorated importance assessment. Null if importance could not be determined.
262
     */
263
    private function getImportanceFromAssessment(array $assessment): ?array
264
    {
265
        $importanceValue = $assessment['importance'];
266
267
        if ('' == $importanceValue && !isset($this->getConfig()['importance'])) {
268
            return null;
269
        }
270
271
        // Known importance level.
272
        $importanceUnknown = 'Unknown' === $importanceValue || '' === $importanceValue;
273
274
        if ($importanceUnknown || !isset($this->getConfig()['importance'][$importanceValue])) {
275
            $importanceAttrs = $this->getConfig()['importance']['Unknown'];
276
277
            return array_merge($importanceAttrs, [
278
                'value' => '???',
279
                'category' => $importanceAttrs['category'],
280
            ]);
281
        } else {
282
            $importanceAttrs = $this->getConfig()['importance'][$importanceValue];
283
            return [
284
                'value' => $importanceValue,
285
                'color' => $importanceAttrs['color'],
286
                'weight' => $importanceAttrs['weight'], // numerical weight for sorting purposes
287
                'category' => $importanceAttrs['category'],
288
            ];
289
        }
290
    }
291
}
292