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/Project.php (8 issues)

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Model;
6
7
/**
8
 * A Project is a single wiki that XTools is querying.
9
 */
10
class Project extends Model
11
{
12
    protected PageAssessments $pageAssessments;
13
14
    /** @var string The project name as supplied by the user. */
15
    protected string $nameUnnormalized;
16
17
    /** @var string[]|null Basic metadata about the project */
18
    protected ?array $metadata;
19
20
    /** @var string[]|null Project's 'dbName', 'url' and 'lang'. */
21
    protected ?array $basicInfo;
22
23
    /**
24
     * Whether the user being queried for in this session has opted in to restricted statistics.
25
     * @var bool
26
     */
27
    protected bool $userOptedIn;
28
29
    /**
30
     * Create a new Project.
31
     * @param string $nameOrUrl The project's database name or URL.
32
     */
33
    public function __construct(string $nameOrUrl)
34
    {
35
        $this->nameUnnormalized = $nameOrUrl;
36
    }
37
38
    /**
39
     * Get the associated PageAssessments model.
40
     * @return PageAssessments
41
     */
42
    public function getPageAssessments(): PageAssessments
43
    {
44
        return $this->pageAssessments;
45
    }
46
47
    /**
48
     * @param PageAssessments $pageAssessments
49
     * @return Project
50
     */
51
    public function setPageAssessments(PageAssessments $pageAssessments): Project
52
    {
53
        $this->pageAssessments = $pageAssessments;
54
        return $this;
55
    }
56
57
    /**
58
     * Whether or not this project supports page assessments, or if they exist for the given namespace.
59
     * @param int|string|null $nsId Namespace ID, null if checking if project has page assessments for any namespace.
60
     * @return bool
61
     */
62
    public function hasPageAssessments($nsId = null): bool
63
    {
64
        if (null !== $nsId && (int)$nsId > 0) {
65
            return $this->pageAssessments->isSupportedNamespace((int)$nsId);
66
        } else {
67
            return $this->pageAssessments->isEnabled();
68
        }
69
    }
70
71
    /**
72
     * Unique identifier this Project, to be used in cache keys.
73
     * @see Repository::getCacheKey()
74
     * @return string
75
     */
76
    public function getCacheKey(): string
77
    {
78
        return $this->getDatabaseName();
79
    }
80
81
    /**
82
     * Get 'dbName', 'url' and 'lang' of the project, the relevant basic info we can get from the meta database.
83
     * This is all you need to make database queries. More comprehensive metadata can be fetched with getMetadata()
84
     * at the expense of an API call, which may be cached.
85
     * @return string[]|null null if not found.
86
     */
87
    protected function getBasicInfo(): ?array
88
    {
89
        if (!isset($this->basicInfo)) {
90
            $this->basicInfo = $this->repository->getOne($this->nameUnnormalized);
0 ignored issues
show
The method getOne() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ProjectRepository. ( Ignorable by Annotation )

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

90
            /** @scrutinizer ignore-call */ 
91
            $this->basicInfo = $this->repository->getOne($this->nameUnnormalized);
Loading history...
91
        }
92
        return $this->basicInfo;
93
    }
94
95
    /**
96
     * Get full metadata about the project. See ProjectRepository::getMetadata() for more information.
97
     * @return array|null null if project not found.
98
     */
99
    protected function getMetadata(): ?array
100
    {
101
        if (!isset($this->metadata)) {
102
            $info = $this->getBasicInfo();
103
            if (!isset($info['url'])) {
104
                // Project is probably not replicated.
105
                return null;
106
            }
107
            $url = $this->getBasicInfo()['url'];
108
            $this->metadata = $this->getRepository()->getMetadata($url);
0 ignored issues
show
The method getMetadata() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ProjectRepository. ( Ignorable by Annotation )

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

108
            $this->metadata = $this->getRepository()->/** @scrutinizer ignore-call */ getMetadata($url);
Loading history...
109
        }
110
        return $this->metadata;
111
    }
112
113
    /**
114
     * Does this project exist?
115
     * @return bool
116
     */
117
    public function exists(): bool
118
    {
119
        return !empty($this->getDomain());
120
    }
121
122
    /**
123
     * The unique domain name of this project, without protocol or path components.
124
     * This should be used as the canonical project identifier.
125
     * @return string|null null if nonexistent.
126
     */
127
    public function getDomain(): ?string
128
    {
129
        $url = $this->getBasicInfo()['url'] ?? '';
130
        return parse_url($url, PHP_URL_HOST);
131
    }
132
133
    /**
134
     * The name of the database for this project.
135
     * @return string
136
     */
137
    public function getDatabaseName(): string
138
    {
139
        return $this->getBasicInfo()['dbName'] ?? '';
140
    }
141
142
    /**
143
     * The language for this project.
144
     * @return string
145
     */
146
    public function getLang(): string
147
    {
148
        return $this->getBasicInfo()['lang'] ?? '';
149
    }
150
151
    /**
152
     * The project URL is the fully-qualified domain name, with protocol and trailing slash.
153
     * @param bool $withTrailingSlash Whether to append a slash.
154
     * @return string
155
     */
156
    public function getUrl(bool $withTrailingSlash = true): string
157
    {
158
        return rtrim($this->getBasicInfo()['url'], '/') . ($withTrailingSlash ? '/' : '');
159
    }
160
161
    /**
162
     * @param Page|string $page Full page title including namespace, or a Page object.
163
     * @param bool $useUnnormalizedPageTitle Use the unnormalized page title to avoid
164
     *    an API call. This should be used only if you fetched the page title via other
165
     *    means (SQL query), and is not from user input alone. Only applicable if $page
166
     *    is a Page object.
167
     * @return string
168
     */
169
    public function getUrlForPage($page, bool $useUnnormalizedPageTitle = false): string
170
    {
171
        if ($page instanceof Page) {
172
            $page = $page->getTitle($useUnnormalizedPageTitle);
173
        }
174
        return str_replace('$1', $page, $this->getUrl(false) . $this->getArticlePath());
175
    }
176
177
    /**
178
     * The base URL path of this project (that page titles are appended to).
179
     * For some wikis the title (apparently) may not be at the end.
180
     * Replace $1 with the article name.
181
     * @link https://www.mediawiki.org/wiki/Manual:$wgArticlePath
182
     * @return string
183
     */
184
    public function getArticlePath(): string
185
    {
186
        $metadata = $this->getMetadata();
187
        return $metadata['general']['articlePath'] ?? '/wiki/$1';
188
    }
189
190
    /**
191
     * The URL path of the directory that contains index.php, with no trailing slash.
192
     * Defaults to '/w' which is the same as the normal WMF set-up.
193
     * @link https://www.mediawiki.org/wiki/Manual:$wgScriptPath
194
     * @return string
195
     */
196
    public function getScriptPath(): string
197
    {
198
        $metadata = $this->getMetadata();
199
        return $metadata['general']['scriptPath'] ?? '/w';
200
    }
201
202
    /**
203
     * The URL path to index.php
204
     * Defaults to '/w/index.php' which is the same as the normal WMF set-up.
205
     * @return string
206
     */
207
    public function getScript(): string
208
    {
209
        $metadata = $this->getMetadata();
210
        return $metadata['general']['script'] ?? $this->getScriptPath() . '/index.php';
211
    }
212
213
    /**
214
     * The full URL to api.php.
215
     * @return string
216
     */
217
    public function getApiUrl(): string
218
    {
219
        return rtrim($this->getUrl(), '/') . $this->getRepository()->getApiPath();
0 ignored issues
show
The method getApiPath() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ProjectRepository. ( Ignorable by Annotation )

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

219
        return rtrim($this->getUrl(), '/') . $this->getRepository()->/** @scrutinizer ignore-call */ getApiPath();
Loading history...
220
    }
221
222
    /**
223
     * Get the project's title, the human-language full title of the wiki (e.g. "English Wikipedia (en.wikipedia.org)").
224
     */
225
    public function getTitle(): string
226
    {
227
        $metadata = $this->getMetadata();
228
        return $metadata['general']['wikiName'].' ('.$this->getDomain().')';
229
    }
230
231
    /**
232
     * Get an array of this project's namespaces and their IDs.
233
     * @return string[] Keys are IDs, values are names.
234
     */
235
    public function getNamespaces(): array
236
    {
237
        $metadata = $this->getMetadata();
238
        return $metadata['namespaces'];
239
    }
240
241
    /**
242
     * Get the title of the Main Page.
243
     * @return string
244
     */
245
    public function getMainPage(): string
246
    {
247
        $metadata = $this->getMetadata();
248
        return $metadata['general']['mainpage'] ?? '';
249
    }
250
251
    /**
252
     * List of extensions that are installed on the wiki.
253
     * @return string[]
254
     */
255
    public function getInstalledExtensions(): array
256
    {
257
        // Quick cache, valid only for the same request.
258
        static $installedExtensions = null;
259
        if (is_array($installedExtensions)) {
260
            return $installedExtensions;
261
        }
262
263
        return $installedExtensions = $this->getRepository()->getInstalledExtensions($this);
0 ignored issues
show
The method getInstalledExtensions() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ProjectRepository. ( Ignorable by Annotation )

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

263
        return $installedExtensions = $this->getRepository()->/** @scrutinizer ignore-call */ getInstalledExtensions($this);
Loading history...
264
    }
265
266
    /**
267
     * Get a list of users who are in one of the given user groups.
268
     * @param string[] $groups User groups to search for.
269
     * @param string[] $globalGroups Global groups to search for.
270
     * @return string[] User groups keyed by user name.
271
     */
272
    public function getUsersInGroups(array $groups, array $globalGroups): array
273
    {
274
        $users = [];
275
        $usersAndGroups = $this->getRepository()->getUsersInGroups($this, $groups, $globalGroups);
0 ignored issues
show
The method getUsersInGroups() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ProjectRepository. ( Ignorable by Annotation )

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

275
        $usersAndGroups = $this->getRepository()->/** @scrutinizer ignore-call */ getUsersInGroups($this, $groups, $globalGroups);
Loading history...
276
        foreach ($usersAndGroups as $userAndGroup) {
277
            $username = $userAndGroup['user_name'];
278
            if (isset($users[$username])) {
279
                $users[$username][] = $userAndGroup['user_group'];
280
            } else {
281
                $users[$username] = [$userAndGroup['user_group']];
282
            }
283
        }
284
        return $users;
285
    }
286
287
    /**
288
     * Get the name of the page on this project that the user must create in order to opt in for restricted statistics.
289
     * @param User $user
290
     * @return string
291
     */
292
    public function userOptInPage(User $user): string
293
    {
294
        return 'User:' . $user->getUsername() . '/EditCounterOptIn.js';
295
    }
296
297
    /**
298
     * Has a user opted in to having their restricted statistics displayed to anyone?
299
     * @param User $user
300
     * @return bool
301
     */
302
    public function userHasOptedIn(User $user): bool
303
    {
304
        // 1. First check to see if the whole project has opted in.
305
        if (!isset($this->userOptedIn)) {
306
            $optedInProjects = $this->getRepository()->optedIn();
0 ignored issues
show
The method optedIn() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ProjectRepository. ( Ignorable by Annotation )

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

306
            $optedInProjects = $this->getRepository()->/** @scrutinizer ignore-call */ optedIn();
Loading history...
307
            $this->userOptedIn = in_array($this->getDatabaseName(), $optedInProjects);
308
        }
309
        if ($this->userOptedIn) {
310
            return true;
311
        }
312
313
        // 2. Then see if the currently-logged-in user is requesting their own statistics.
314
        if ($user->isCurrentlyLoggedIn()) {
315
            return true;
316
        }
317
318
        // 3. Then see if the user has opted in on this project.
319
        $userNsId = 2;
320
        // Remove namespace since we're querying the database and supplying a namespace ID.
321
        $optInPage = preg_replace('/^User:/', '', $this->userOptInPage($user));
322
        $localExists = $this->getRepository()->pageHasContent($this, $userNsId, $optInPage);
0 ignored issues
show
The method pageHasContent() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ProjectRepository. ( Ignorable by Annotation )

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

322
        $localExists = $this->getRepository()->/** @scrutinizer ignore-call */ pageHasContent($this, $userNsId, $optInPage);
Loading history...
323
        if ($localExists) {
324
            return true;
325
        }
326
327
        // 4. Lastly, see if they've opted in globally on the default project or Meta.
328
        $globalPageName = $user->getUsername() . '/EditCounterGlobalOptIn.js';
329
        $globalProject = $this->getRepository()->getGlobalProject();
0 ignored issues
show
The method getGlobalProject() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\ProjectRepository. ( Ignorable by Annotation )

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

329
        $globalProject = $this->getRepository()->/** @scrutinizer ignore-call */ getGlobalProject();
Loading history...
330
        $globalExists = $globalProject->getRepository()
331
            ->pageHasContent($globalProject, $userNsId, $globalPageName);
332
        if ($globalExists) {
333
            return true;
334
        }
335
336
        return false;
337
    }
338
339
    /**
340
     * Normalize and quote a table name for use in SQL.
341
     * @param string $tableName
342
     * @param string|null $tableExtension Optional table extension, which will only get used if we're on Labs.
343
     * @return string Fully-qualified and quoted table name.
344
     */
345
    public function getTableName(string $tableName, ?string $tableExtension = null): string
346
    {
347
        return $this->getRepository()->getTableName($this->getDatabaseName(), $tableName, $tableExtension);
348
    }
349
}
350