Test Setup Failed
Pull Request — main (#426)
by MusikAnimal
17:10 queued 11:44
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)
135
            ->fetchAllAssociative();
136
        $projectsMetadata = [];
137
        foreach ($projects as $project) {
138
            $projectsMetadata[$project['dbName']] = $project;
139
        }
140
141
        // Cache for one day and return.
142
        return $this->setCache(
143
            $this->cacheKeyAllProjects,
144
            $projectsMetadata,
145
            'P1D'
146
        );
147
    }
148
149
    /**
150
     * Get the 'dbName', 'url' and 'lang' of a project. This is all you need to make database queries.
151
     * More comprehensive metadata can be fetched with getMetadata() at the expense of an API call.
152
     * @param string $project A project URL, domain name, or database name.
153
     * @return string[]|bool With 'dbName', 'url' and 'lang' keys; or false if not found.
154
     */
155
    public function getOne(string $project)
156
    {
157
        $this->log->debug(__METHOD__." Getting metadata about $project");
158
        // For single-wiki setups, every project is the same.
159
        if (!empty($this->singleBasicInfo)) {
160
            return $this->singleBasicInfo;
161
        }
162
163
        // Remove _p suffix.
164
        $project = rtrim($project, '_p');
165
166
        // For multi-wiki setups, first check the cache.
167
        // First the all-projects cache, then the individual one.
168
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
169
            foreach ($this->cache->getItem($this->cacheKeyAllProjects)->get() as $projMetadata) {
170
                if ($projMetadata['dbName'] == "$project"
171
                    || $projMetadata['url'] == "$project"
172
                    || $projMetadata['url'] == "https://$project"
173
                    || $projMetadata['url'] == "https://$project.org"
174
                    || $projMetadata['url'] == "https://www.$project") {
175
                    $this->log->debug(__METHOD__ . " Using cached data for $project");
176
                    return $projMetadata;
177
                }
178
            }
179
        }
180
        $cacheKey = $this->getCacheKey($project, 'project');
181
        if ($this->cache->hasItem($cacheKey)) {
182
            return $this->cache->getItem($cacheKey)->get();
183
        }
184
185
        if ($this->container->hasParameter("database_meta_table")) {
186
            $table = $this->container->getParameter('database_meta_name') . '.' .
187
                $this->container->getParameter('database_meta_table');
188
        } else {
189
            $table = "meta_p.wiki";
190
        }
191
192
        // Otherwise, fetch the project's metadata from the meta.wiki table.
193
        $sql = "SELECT dbname AS dbName, url, lang
194
                FROM $table
195
                WHERE dbname = :project
196
                    OR url LIKE :projectUrl
197
                    OR url LIKE :projectUrl2
198
                    OR url LIKE :projectUrl3
199
                    OR url LIKE :projectUrl4";
200
        $basicInfo = $this->executeProjectsQuery('meta', $sql, [
201
            'project' => $project,
202
            'projectUrl' => "https://$project",
203
            'projectUrl2' => "https://$project.org",
204
            'projectUrl3' => "https://www.$project",
205
            'projectUrl4' => "https://www.$project.org",
206
        ])->fetchAssociative();
207
208
        // Cache for one hour and return.
209
        return $this->setCache($cacheKey, $basicInfo, 'PT1H');
210
    }
211
212
    /**
213
     * Get metadata about a project, including the 'dbName', 'url' and 'lang'
214
     *
215
     * @param string $projectUrl The project's URL.
216
     * @return array|null With 'dbName', 'url', 'lang', 'general' and 'namespaces' keys.
217
     *   'general' contains: 'wikiName', 'articlePath', 'scriptPath', 'script',
218
     *   'timezone', and 'timezoneOffset'; 'namespaces' contains all namespace
219
     *   names, keyed by their IDs. If this function returns null, the API call
220
     *   failed.
221
     */
222
    public function getMetadata(string $projectUrl): ?array
223
    {
224
        // First try variable cache
225
        if (!empty($this->metadata)) {
226
            return $this->metadata;
227
        }
228
229
        // Redis cache
230
        $cacheKey = $this->getCacheKey(
231
            // Removed non-alphanumeric characters
232
            preg_replace("/[^A-Za-z0-9]/", '', $projectUrl),
233
            'project_metadata'
234
        );
235
236
        if ($this->cache->hasItem($cacheKey)) {
237
            $this->metadata = $this->cache->getItem($cacheKey)->get();
238
            return $this->metadata;
239
        }
240
241
        /** @var Client $client */
242
        $client = $this->container->get('eight_points_guzzle.client.xtools');
243
244
        try {
245
            $res = json_decode($client->request('GET', $projectUrl.$this->getApiPath(), [
246
                'query' => [
247
                    'action' => 'query',
248
                    'meta' => 'siteinfo',
249
                    'siprop' => 'general|namespaces',
250
                    'format' => 'json',
251
                ],
252
            ])->getBody()->getContents(), true);
253
        } catch (Exception $e) {
254
            return null;
255
        }
256
257
        $this->metadata = [
258
            'general' => [],
259
            'namespaces' => [],
260
        ];
261
262
        // Even if general info could not be fetched,
263
        //   return dbName, url and lang if already known
264
        if (!empty($this->basicInfo)) {
265
            $this->metadata['dbName'] = $this->basicInfo['dbName'];
266
            $this->metadata['url'] = $this->basicInfo['url'];
267
            $this->metadata['lang'] = $this->basicInfo['lang'];
268
        }
269
270
        if (isset($res['query']['general'])) {
271
            $info = $res['query']['general'];
272
273
            $this->metadata['dbName'] = $info['wikiid'];
274
            $this->metadata['url'] = $info['server'];
275
            $this->metadata['lang'] = $info['lang'];
276
277
            $this->metadata['general'] = [
278
                'wikiName' => $info['sitename'],
279
                'articlePath' => $info['articlepath'],
280
                'scriptPath' => $info['scriptpath'],
281
                'script' => $info['script'],
282
                'timezone' => $info['timezone'],
283
                'timeOffset' => $info['timeoffset'],
284
                'mainpage' => $info['mainpage'],
285
            ];
286
        }
287
288
        $this->setNamespaces($res);
289
290
        // Cache for one hour and return.
291
        return $this->setCache($cacheKey, $this->metadata, 'PT1H');
292
    }
293
294
    /**
295
     * Set the namespaces on $this->metadata.
296
     * @param array $res As produced by meta=siteinfo API.
297
     */
298
    private function setNamespaces(array $res): void
299
    {
300
        if (!isset($res['query']['namespaces'])) {
301
            return;
302
        }
303
304
        foreach ($res['query']['namespaces'] as $namespace) {
305
            if ($namespace['id'] < 0) {
306
                continue;
307
            }
308
309
            if (isset($namespace['name'])) {
310
                $name = $namespace['name'];
311
            } elseif (isset($namespace['*'])) {
312
                $name = $namespace['*'];
313
            } else {
314
                continue;
315
            }
316
317
            $this->metadata['namespaces'][$namespace['id']] = $name;
318
        }
319
    }
320
321
    /**
322
     * Get a list of projects that have opted in to having all their users' restricted statistics available to anyone.
323
     * @return string[]
324
     */
325
    public function optedIn(): array
326
    {
327
        $optedIn = $this->container->getParameter('opted_in');
328
        // In case there's just one given.
329
        if (!is_array($optedIn)) {
330
            $optedIn = [ $optedIn ];
331
        }
332
        return $optedIn;
333
    }
334
335
    /**
336
     * The path to api.php.
337
     * @return string
338
     */
339
    public function getApiPath(): string
340
    {
341
        return $this->container->getParameter('api_path');
342
    }
343
344
    /**
345
     * Get a page from the given Project.
346
     * @param Project $project The project.
347
     * @param string $pageName The name of the page.
348
     * @return Page
349
     */
350
    public function getPage(Project $project, string $pageName): Page
351
    {
352
        $pageRepo = new PageRepository();
353
        $pageRepo->setContainer($this->container);
354
        $page = new Page($project, $pageName);
355
        $page->setRepository($pageRepo);
356
        return $page;
357
    }
358
359
    /**
360
     * Check to see if a page exists on this project and has some content.
361
     * @param Project $project The project.
362
     * @param int $namespaceId The page namespace ID.
363
     * @param string $pageTitle The page title, without namespace.
364
     * @return bool
365
     */
366
    public function pageHasContent(Project $project, int $namespaceId, string $pageTitle): bool
367
    {
368
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
369
        $query = "SELECT page_id "
370
             . " FROM $pageTable "
371
             . " WHERE page_namespace = :ns AND page_title = :title AND page_len > 0 "
372
             . " LIMIT 1";
373
        $params = [
374
            'ns' => $namespaceId,
375
            'title' => str_replace(' ', '_', $pageTitle),
376
        ];
377
        $pages = $this->executeProjectsQuery($project, $query, $params)
378
            ->fetchAllAssociative();
379
        return count($pages) > 0;
380
    }
381
382
    /**
383
     * Get a list of the extensions installed on the wiki.
384
     * @param Project $project
385
     * @return string[]
386
     */
387
    public function getInstalledExtensions(Project $project): array
388
    {
389
        /** @var Client $client */
390
        $client = $this->container->get('eight_points_guzzle.client.xtools');
391
392
        $res = json_decode($client->request('GET', $project->getApiUrl(), ['query' => [
393
            'action' => 'query',
394
            'meta' => 'siteinfo',
395
            'siprop' => 'extensions',
396
            'format' => 'json',
397
        ]])->getBody()->getContents(), true);
398
399
        $extensions = $res['query']['extensions'] ?? [];
400
        return array_map(function ($extension) {
401
            return $extension['name'];
402
        }, $extensions);
403
    }
404
405
    /**
406
     * Get a list of users who are in one of the given user groups.
407
     * @param Project $project
408
     * @param string[] $groups List of user groups to look for.
409
     * @param string[] $globalGroups List of global groups to look for.
410
     * @return string[] with keys 'user_name' and 'ug_group'
411
     */
412
    public function getUsersInGroups(Project $project, array $groups = [], array $globalGroups = []): array
413
    {
414
        $cacheKey = $this->getCacheKey(func_get_args(), 'project_useringroups');
415
        if ($this->cache->hasItem($cacheKey)) {
416
            return $this->cache->getItem($cacheKey)->get();
417
        }
418
419
        $userTable = $project->getTableName('user');
420
        $userGroupsTable = $project->getTableName('user_groups');
421
422
        $sql = "SELECT user_name, ug_group AS user_group
423
                FROM $userTable
424
                JOIN $userGroupsTable ON ug_user = user_id
425
                WHERE ug_group IN (?)
426
                GROUP BY user_name, ug_group";
427
        $users = $this->getProjectsConnection($project)
428
            ->executeQuery($sql, [$groups], [Connection::PARAM_STR_ARRAY])
429
            ->fetchAllAssociative();
430
431
        if (count($globalGroups) > 0 && $this->isLabs()) {
432
            $sql = "SELECT gu_name AS user_name, gug_group AS user_group
433
                    FROM centralauth_p.global_user_groups
434
                    JOIN centralauth_p.globaluser ON gug_user = gu_id
435
                    WHERE gug_group IN (?)
436
                    GROUP BY user_name, user_group";
437
            $globalUsers = $this->getProjectsConnection('centralauth')
438
                ->executeQuery($sql, [$globalGroups], [Connection::PARAM_STR_ARRAY])
439
                ->fetchAllAssociative();
440
441
            $users = array_merge($users, $globalUsers);
442
        }
443
444
        // Cache for 12 hours and return.
445
        return $this->setCache($cacheKey, $users, 'PT12H');
446
    }
447
}
448