Test Failed
Pull Request — main (#426)
by MusikAnimal
24:07 queued 16:58
created

ProjectRepository::getOne()   B

Complexity

Conditions 11
Paths 8

Size

Total Lines 55
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 31
nc 8
nop 1
dl 0
loc 55
rs 7.3166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * This file contains only the ProjectRepository class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace App\Repository;
9
10
use App\Model\Page;
11
use App\Model\Project;
12
use Doctrine\DBAL\Connection;
13
use Exception;
14
use GuzzleHttp\Client;
15
use Symfony\Component\DependencyInjection\ContainerInterface;
16
17
/**
18
 * This class provides data to the Project class.
19
 * @codeCoverageIgnore
20
 */
21
class ProjectRepository extends Repository
22
{
23
    /** @var array Project's 'dbName', 'url' and 'lang'. */
24
    protected $basicInfo;
25
26
    /** @var string[] Basic metadata if XTools is in single-wiki mode. */
27
    protected $singleBasicInfo;
28
29
    /** @var array Full Project metadata, including $basicInfo. */
30
    protected $metadata;
31
32
    /** @var string The cache key for the 'all project' metadata. */
33
    protected $cacheKeyAllProjects = 'allprojects';
34
35
    /**
36
     * Convenience method to get a new Project object based on a given identification string.
37
     * @param string $projectIdent The domain name, database name, or URL of a project.
38
     * @param ContainerInterface $container Symfony's container.
39
     * @return Project
40
     */
41
    public static function getProject(string $projectIdent, ContainerInterface $container): Project
42
    {
43
        $project = new Project($projectIdent);
44
        $projectRepo = new ProjectRepository();
45
        $projectRepo->setContainer($container);
46
47
        // The associated PageAssessmentsRepository also needs the container.
48
        $paRepo = new PageAssessmentsRepository();
49
        $paRepo->setContainer($container);
50
        $project->getPageAssessments()->setRepository($paRepo);
51
52
        if ($container->getParameter('app.single_wiki')) {
53
            $projectRepo->setSingleBasicInfo([
54
                'url' => $container->getParameter('wiki_url'),
55
                'dbName' => '', // Just so this will pass in CI.
56
                // TODO: this will need to be restored for third party support; KEYWORD: isLabs()
57
                // 'dbName' => $container->getParameter('database_replica_name'),
58
            ]);
59
        }
60
        $project->setRepository($projectRepo);
61
62
        return $project;
63
    }
64
65
    /**
66
     * Get the XTools default project.
67
     * @param ContainerInterface $container
68
     * @return Project
69
     */
70
    public static function getDefaultProject(ContainerInterface $container): Project
71
    {
72
        $defaultProjectName = $container->getParameter('default_project');
73
        return self::getProject($defaultProjectName, $container);
74
    }
75
76
    /**
77
     * Get the global 'meta' project, which is either Meta (if this is Labs) or the default project.
78
     * @return Project
79
     */
80
    public function getGlobalProject(): Project
81
    {
82
        if ($this->isLabs()) {
83
            return self::getProject('metawiki', $this->container);
84
        } else {
85
            return self::getDefaultProject($this->container);
86
        }
87
    }
88
89
    /**
90
     * For single-wiki installations, you must manually set the wiki URL and database name
91
     * (because there's no meta.wiki database to query).
92
     * @param array $metadata
93
     * @throws Exception
94
     */
95
    public function setSingleBasicInfo(array $metadata): void
96
    {
97
        if (!array_key_exists('url', $metadata) || !array_key_exists('dbName', $metadata)) {
98
            $error = "Single-wiki metadata should contain 'url', 'dbName' and 'lang' keys.";
99
            throw new Exception($error);
100
        }
101
        $this->singleBasicInfo = array_intersect_key($metadata, [
102
            'url' => '',
103
            'dbName' => '',
104
            'lang' => '',
105
        ]);
106
    }
107
108
    /**
109
     * Get the 'dbName', 'url' and 'lang' of all projects.
110
     * @return string[][] Each item has 'dbName', 'url' and 'lang' keys.
111
     */
112
    public function getAll(): array
113
    {
114
        $this->log->debug(__METHOD__." Getting all projects' metadata");
115
        // Single wiki mode?
116
        if (!empty($this->singleBasicInfo)) {
117
            return [$this->getOne('')];
118
        }
119
120
        // Maybe we've already fetched it.
121
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
122
            return $this->cache->getItem($this->cacheKeyAllProjects)->get();
123
        }
124
125
        if ($this->container->hasParameter("database_meta_table")) {
126
            $table = $this->container->getParameter('database_meta_name') . '.' .
127
                $this->container->getParameter('database_meta_table');
128
        } else {
129
            $table = "meta_p.wiki";
130
        }
131
132
        // Otherwise, fetch all from the database.
133
        $sql = "SELECT dbname AS dbName, url, lang FROM $table";
134
        $projects = $this->executeProjectsQuery('meta', $sql)->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCom...lity\Result::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

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

134
        $projects = /** @scrutinizer ignore-deprecated */ $this->executeProjectsQuery('meta', $sql)->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
135
        $projectsMetadata = [];
136
        foreach ($projects as $project) {
137
            $projectsMetadata[$project['dbName']] = $project;
138
        }
139
140
        // Cache for one day and return.
141
        return $this->setCache(
142
            $this->cacheKeyAllProjects,
143
            $projectsMetadata,
144
            'P1D'
145
        );
146
    }
147
148
    /**
149
     * Get the 'dbName', 'url' and 'lang' of a project. This is all you need to make database queries.
150
     * More comprehensive metadata can be fetched with getMetadata() at the expense of an API call.
151
     * @param string $project A project URL, domain name, or database name.
152
     * @return string[]|bool With 'dbName', 'url' and 'lang' keys; or false if not found.
153
     */
154
    public function getOne(string $project)
155
    {
156
        $this->log->debug(__METHOD__." Getting metadata about $project");
157
        // For single-wiki setups, every project is the same.
158
        if (!empty($this->singleBasicInfo)) {
159
            return $this->singleBasicInfo;
160
        }
161
162
        // Remove _p suffix.
163
        $project = rtrim($project, '_p');
164
165
        // For multi-wiki setups, first check the cache.
166
        // First the all-projects cache, then the individual one.
167
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
168
            foreach ($this->cache->getItem($this->cacheKeyAllProjects)->get() as $projMetadata) {
169
                if ($projMetadata['dbName'] == "$project"
170
                    || $projMetadata['url'] == "$project"
171
                    || $projMetadata['url'] == "https://$project"
172
                    || $projMetadata['url'] == "https://$project.org"
173
                    || $projMetadata['url'] == "https://www.$project") {
174
                    $this->log->debug(__METHOD__ . " Using cached data for $project");
175
                    return $projMetadata;
176
                }
177
            }
178
        }
179
        $cacheKey = $this->getCacheKey($project, 'project');
180
        if ($this->cache->hasItem($cacheKey)) {
181
            return $this->cache->getItem($cacheKey)->get();
182
        }
183
184
        if ($this->container->hasParameter("database_meta_table")) {
185
            $table = $this->container->getParameter('database_meta_name') . '.' .
186
                $this->container->getParameter('database_meta_table');
187
        } else {
188
            $table = "meta_p.wiki";
189
        }
190
191
        // Otherwise, fetch the project's metadata from the meta.wiki table.
192
        $sql = "SELECT dbname AS dbName, url, lang
193
                FROM $table
194
                WHERE dbname = :project
195
                    OR url LIKE :projectUrl
196
                    OR url LIKE :projectUrl2
197
                    OR url LIKE :projectUrl3
198
                    OR url LIKE :projectUrl4";
199
        $basicInfo = $this->executeProjectsQuery('meta', $sql, [
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCompatibility\Result::fetch() has been deprecated: Use fetchNumeric(), fetchAssociative() or fetchOne() instead. ( Ignorable by Annotation )

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

199
        $basicInfo = /** @scrutinizer ignore-deprecated */ $this->executeProjectsQuery('meta', $sql, [

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
200
            'project' => $project,
201
            'projectUrl' => "https://$project",
202
            'projectUrl2' => "https://$project.org",
203
            'projectUrl3' => "https://www.$project",
204
            'projectUrl4' => "https://www.$project.org",
205
        ])->fetch();
206
207
        // Cache for one hour and return.
208
        return $this->setCache($cacheKey, $basicInfo, 'PT1H');
209
    }
210
211
    /**
212
     * Get metadata about a project, including the 'dbName', 'url' and 'lang'
213
     *
214
     * @param string $projectUrl The project's URL.
215
     * @return array|null With 'dbName', 'url', 'lang', 'general' and 'namespaces' keys.
216
     *   'general' contains: 'wikiName', 'articlePath', 'scriptPath', 'script',
217
     *   'timezone', and 'timezoneOffset'; 'namespaces' contains all namespace
218
     *   names, keyed by their IDs. If this function returns null, the API call
219
     *   failed.
220
     */
221
    public function getMetadata(string $projectUrl): ?array
222
    {
223
        // First try variable cache
224
        if (!empty($this->metadata)) {
225
            return $this->metadata;
226
        }
227
228
        // Redis cache
229
        $cacheKey = $this->getCacheKey(
230
            // Removed non-alphanumeric characters
231
            preg_replace("/[^A-Za-z0-9]/", '', $projectUrl),
232
            'project_metadata'
233
        );
234
235
        if ($this->cache->hasItem($cacheKey)) {
236
            $this->metadata = $this->cache->getItem($cacheKey)->get();
237
            return $this->metadata;
238
        }
239
240
        /** @var Client $client */
241
        $client = $this->container->get('eight_points_guzzle.client.xtools');
242
243
        try {
244
            $res = json_decode($client->request('GET', $projectUrl.$this->getApiPath(), [
245
                'query' => [
246
                    'action' => 'query',
247
                    'meta' => 'siteinfo',
248
                    'siprop' => 'general|namespaces',
249
                    'format' => 'json',
250
                ],
251
            ])->getBody()->getContents(), true);
252
        } catch (Exception $e) {
253
            return null;
254
        }
255
256
        $this->metadata = [
257
            'general' => [],
258
            'namespaces' => [],
259
        ];
260
261
        // Even if general info could not be fetched,
262
        //   return dbName, url and lang if already known
263
        if (!empty($this->basicInfo)) {
264
            $this->metadata['dbName'] = $this->basicInfo['dbName'];
265
            $this->metadata['url'] = $this->basicInfo['url'];
266
            $this->metadata['lang'] = $this->basicInfo['lang'];
267
        }
268
269
        if (isset($res['query']['general'])) {
270
            $info = $res['query']['general'];
271
272
            $this->metadata['dbName'] = $info['wikiid'];
273
            $this->metadata['url'] = $info['server'];
274
            $this->metadata['lang'] = $info['lang'];
275
276
            $this->metadata['general'] = [
277
                'wikiName' => $info['sitename'],
278
                'articlePath' => $info['articlepath'],
279
                'scriptPath' => $info['scriptpath'],
280
                'script' => $info['script'],
281
                'timezone' => $info['timezone'],
282
                'timeOffset' => $info['timeoffset'],
283
                'mainpage' => $info['mainpage'],
284
            ];
285
        }
286
287
        $this->setNamespaces($res);
288
289
        // Cache for one hour and return.
290
        return $this->setCache($cacheKey, $this->metadata, 'PT1H');
291
    }
292
293
    /**
294
     * Set the namespaces on $this->metadata.
295
     * @param array $res As produced by meta=siteinfo API.
296
     */
297
    private function setNamespaces(array $res): void
298
    {
299
        if (!isset($res['query']['namespaces'])) {
300
            return;
301
        }
302
303
        foreach ($res['query']['namespaces'] as $namespace) {
304
            if ($namespace['id'] < 0) {
305
                continue;
306
            }
307
308
            if (isset($namespace['name'])) {
309
                $name = $namespace['name'];
310
            } elseif (isset($namespace['*'])) {
311
                $name = $namespace['*'];
312
            } else {
313
                continue;
314
            }
315
316
            $this->metadata['namespaces'][$namespace['id']] = $name;
317
        }
318
    }
319
320
    /**
321
     * Get a list of projects that have opted in to having all their users' restricted statistics available to anyone.
322
     * @return string[]
323
     */
324
    public function optedIn(): array
325
    {
326
        $optedIn = $this->container->getParameter('opted_in');
327
        // In case there's just one given.
328
        if (!is_array($optedIn)) {
329
            $optedIn = [ $optedIn ];
330
        }
331
        return $optedIn;
332
    }
333
334
    /**
335
     * The path to api.php.
336
     * @return string
337
     */
338
    public function getApiPath(): string
339
    {
340
        return $this->container->getParameter('api_path');
341
    }
342
343
    /**
344
     * Get a page from the given Project.
345
     * @param Project $project The project.
346
     * @param string $pageName The name of the page.
347
     * @return Page
348
     */
349
    public function getPage(Project $project, string $pageName): Page
350
    {
351
        $pageRepo = new PageRepository();
352
        $pageRepo->setContainer($this->container);
353
        $page = new Page($project, $pageName);
354
        $page->setRepository($pageRepo);
355
        return $page;
356
    }
357
358
    /**
359
     * Check to see if a page exists on this project and has some content.
360
     * @param Project $project The project.
361
     * @param int $namespaceId The page namespace ID.
362
     * @param string $pageTitle The page title, without namespace.
363
     * @return bool
364
     */
365
    public function pageHasContent(Project $project, int $namespaceId, string $pageTitle): bool
366
    {
367
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
368
        $query = "SELECT page_id "
369
             . " FROM $pageTable "
370
             . " WHERE page_namespace = :ns AND page_title = :title AND page_len > 0 "
371
             . " LIMIT 1";
372
        $params = [
373
            'ns' => $namespaceId,
374
            'title' => str_replace(' ', '_', $pageTitle),
375
        ];
376
        $pages = $this->executeProjectsQuery($project, $query, $params)->fetchAll();
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCom...lity\Result::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

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

376
        $pages = /** @scrutinizer ignore-deprecated */ $this->executeProjectsQuery($project, $query, $params)->fetchAll();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
377
        return count($pages) > 0;
378
    }
379
380
    /**
381
     * Get a list of the extensions installed on the wiki.
382
     * @param Project $project
383
     * @return string[]
384
     */
385
    public function getInstalledExtensions(Project $project): array
386
    {
387
        /** @var Client $client */
388
        $client = $this->container->get('eight_points_guzzle.client.xtools');
389
390
        $res = json_decode($client->request('GET', $project->getApiUrl(), ['query' => [
391
            'action' => 'query',
392
            'meta' => 'siteinfo',
393
            'siprop' => 'extensions',
394
            'format' => 'json',
395
        ]])->getBody()->getContents(), true);
396
397
        $extensions = $res['query']['extensions'] ?? [];
398
        return array_map(function ($extension) {
399
            return $extension['name'];
400
        }, $extensions);
401
    }
402
403
    /**
404
     * Get a list of users who are in one of the given user groups.
405
     * @param Project $project
406
     * @param string[] $groups List of user groups to look for.
407
     * @param string[] $globalGroups List of global groups to look for.
408
     * @return string[] with keys 'user_name' and 'ug_group'
409
     */
410
    public function getUsersInGroups(Project $project, array $groups = [], array $globalGroups = []): array
411
    {
412
        $cacheKey = $this->getCacheKey(func_get_args(), 'project_useringroups');
413
        if ($this->cache->hasItem($cacheKey)) {
414
            return $this->cache->getItem($cacheKey)->get();
415
        }
416
417
        $userTable = $project->getTableName('user');
418
        $userGroupsTable = $project->getTableName('user_groups');
419
420
        $sql = "SELECT user_name, ug_group AS user_group
421
                FROM $userTable
422
                JOIN $userGroupsTable ON ug_user = user_id
423
                WHERE ug_group IN (?)
424
                GROUP BY user_name, ug_group";
425
        $users = $this->getProjectsConnection($project)
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCom...lity\Result::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

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

425
        $users = /** @scrutinizer ignore-deprecated */ $this->getProjectsConnection($project)

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
426
            ->executeQuery($sql, [$groups], [Connection::PARAM_STR_ARRAY])
427
            ->fetchAll();
428
429
        if (count($globalGroups) > 0 && $this->isLabs()) {
430
            $sql = "SELECT gu_name AS user_name, gug_group AS user_group
431
                    FROM centralauth_p.global_user_groups
432
                    JOIN centralauth_p.globaluser ON gug_user = gu_id
433
                    WHERE gug_group IN (?)
434
                    GROUP BY user_name, user_group";
435
            $globalUsers = $this->getProjectsConnection('centralauth')
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCom...lity\Result::fetchAll() has been deprecated: Use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead. ( Ignorable by Annotation )

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

435
            $globalUsers = /** @scrutinizer ignore-deprecated */ $this->getProjectsConnection('centralauth')

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
436
                ->executeQuery($sql, [$globalGroups], [Connection::PARAM_STR_ARRAY])
437
                ->fetchAll();
438
439
            $users = array_merge($users, $globalUsers);
440
        }
441
442
        // Cache for 12 hours and return.
443
        return $this->setCache($cacheKey, $users, 'PT12H');
444
    }
445
}
446