Passed
Push — master ( 892aee...f335b2 )
by MusikAnimal
05:53
created

ProjectRepository   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 434
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 199
dl 0
loc 434
rs 8.72
c 0
b 0
f 0
wmc 46

14 Methods

Rating   Name   Duplication   Size   Complexity  
B getOne() 0 60 11
A getDefaultProject() 0 4 1
A getGlobalProject() 0 6 2
A getProject() 0 20 2
A getAll() 0 33 5
A setSingleBasicInfo() 0 10 3
A getPage() 0 7 1
A setNamespaces() 0 20 6
A getApiPath() 0 3 1
A pageHasContent() 0 13 1
A getInstalledExtensions() 0 16 1
A optedIn() 0 8 2
A getUsersInGroups() 0 42 4
B getMetadata() 0 70 6

How to fix   Complexity   

Complex Class

Complex classes like ProjectRepository often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ProjectRepository, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file contains only the ProjectRepository class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace AppBundle\Repository;
9
10
use AppBundle\Model\Page;
11
use AppBundle\Model\Project;
12
use GuzzleHttp\Client;
13
use Symfony\Component\DependencyInjection\ContainerInterface;
14
15
/**
16
 * This class provides data to the Project class.
17
 * @codeCoverageIgnore
18
 */
19
class ProjectRepository extends Repository
20
{
21
    /** @var array Project's 'dbName', 'url' and 'lang'. */
22
    protected $basicInfo;
23
24
    /** @var string[] Basic metadata if XTools is in single-wiki mode. */
25
    protected $singleBasicInfo;
26
27
    /** @var array Full Project metadata, including $basicInfo. */
28
    protected $metadata;
29
30
    /** @var string The cache key for the 'all project' metadata. */
31
    protected $cacheKeyAllProjects = 'allprojects';
32
33
    /**
34
     * Convenience method to get a new Project object based on a given identification string.
35
     * @param string $projectIdent The domain name, database name, or URL of a project.
36
     * @param ContainerInterface $container Symfony's container.
37
     * @return Project
38
     */
39
    public static function getProject(string $projectIdent, ContainerInterface $container): Project
40
    {
41
        $project = new Project($projectIdent);
42
        $projectRepo = new ProjectRepository();
43
        $projectRepo->setContainer($container);
44
45
        // The associated PageAssessmentsRepository also needs the container.
46
        $paRepo = new PageAssessmentsRepository();
47
        $paRepo->setContainer($container);
48
        $project->getPageAssessments()->setRepository($paRepo);
49
50
        if ($container->getParameter('app.single_wiki')) {
51
            $projectRepo->setSingleBasicInfo([
52
                'url' => $container->getParameter('wiki_url'),
53
                'dbName' => $container->getParameter('database_replica_name'),
54
            ]);
55
        }
56
        $project->setRepository($projectRepo);
57
58
        return $project;
59
    }
60
61
    /**
62
     * Get the XTools default project.
63
     * @param ContainerInterface $container
64
     * @return Project
65
     */
66
    public static function getDefaultProject(ContainerInterface $container): Project
67
    {
68
        $defaultProjectName = $container->getParameter('default_project');
69
        return self::getProject($defaultProjectName, $container);
70
    }
71
72
    /**
73
     * Get the global 'meta' project, which is either Meta (if this is Labs) or the default project.
74
     * @return Project
75
     */
76
    public function getGlobalProject(): Project
77
    {
78
        if ($this->isLabs()) {
79
            return self::getProject('metawiki', $this->container);
80
        } else {
81
            return self::getDefaultProject($this->container);
82
        }
83
    }
84
85
    /**
86
     * For single-wiki installations, you must manually set the wiki URL and database name
87
     * (because there's no meta.wiki database to query).
88
     * @param array $metadata
89
     * @throws \Exception
90
     */
91
    public function setSingleBasicInfo(array $metadata): void
92
    {
93
        if (!array_key_exists('url', $metadata) || !array_key_exists('dbName', $metadata)) {
94
            $error = "Single-wiki metadata should contain 'url', 'dbName' and 'lang' keys.";
95
            throw new \Exception($error);
96
        }
97
        $this->singleBasicInfo = array_intersect_key($metadata, [
98
            'url' => '',
99
            'dbName' => '',
100
            'lang' => '',
101
        ]);
102
    }
103
104
    /**
105
     * Get the 'dbName', 'url' and 'lang' of all projects.
106
     * @return string[][] Each item has 'dbName', 'url' and 'lang' keys.
107
     */
108
    public function getAll(): array
109
    {
110
        $this->log->debug(__METHOD__." Getting all projects' metadata");
111
        // Single wiki mode?
112
        if (!empty($this->singleBasicInfo)) {
113
            return [$this->getOne('')];
114
        }
115
116
        // Maybe we've already fetched it.
117
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
118
            return $this->cache->getItem($this->cacheKeyAllProjects)->get();
119
        }
120
121
        if ($this->container->hasParameter("database_meta_table")) {
122
            $table = $this->container->getParameter("database_meta_table");
123
        } else {
124
            $table = "wiki";
125
        }
126
127
        // Otherwise, fetch all from the database.
128
        $wikiQuery = $this->getMetaConnection()->createQueryBuilder();
129
        $wikiQuery->select(['dbname AS dbName', 'url', 'lang'])->from($table);
130
        $projects = $this->executeQueryBuilder($wikiQuery)->fetchAll();
131
        $projectsMetadata = [];
132
        foreach ($projects as $project) {
133
            $projectsMetadata[$project['dbName']] = $project;
134
        }
135
136
        // Cache for one day and return.
137
        return $this->setCache(
138
            $this->cacheKeyAllProjects,
139
            $projectsMetadata,
140
            'P1D'
141
        );
142
    }
143
144
    /**
145
     * Get the 'dbName', 'url' and 'lang' of a project. This is all you need to make database queries.
146
     * More comprehensive metadata can be fetched with getMetadata() at the expense of an API call.
147
     * @param string $project A project URL, domain name, or database name.
148
     * @return string[]|bool With 'dbName', 'url' and 'lang' keys; or false if not found.
149
     */
150
    public function getOne(string $project)
151
    {
152
        $this->log->debug(__METHOD__." Getting metadata about $project");
153
        // For single-wiki setups, every project is the same.
154
        if (!empty($this->singleBasicInfo)) {
155
            return $this->singleBasicInfo;
156
        }
157
158
        // Remove _p suffix.
159
        $project = rtrim($project, '_p');
160
161
        // For multi-wiki setups, first check the cache.
162
        // First the all-projects cache, then the individual one.
163
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
164
            foreach ($this->cache->getItem($this->cacheKeyAllProjects)->get() as $projMetadata) {
165
                if ($projMetadata['dbName'] == "$project"
166
                    || $projMetadata['url'] == "$project"
167
                    || $projMetadata['url'] == "https://$project"
168
                    || $projMetadata['url'] == "https://$project.org"
169
                    || $projMetadata['url'] == "https://www.$project") {
170
                    $this->log->debug(__METHOD__ . " Using cached data for $project");
171
                    return $projMetadata;
172
                }
173
            }
174
        }
175
        $cacheKey = $this->getCacheKey($project, 'project');
176
        if ($this->cache->hasItem($cacheKey)) {
177
            return $this->cache->getItem($cacheKey)->get();
178
        }
179
180
        if ($this->container->hasParameter("database_meta_table")) {
181
            $table = $this->container->getParameter("database_meta_table");
182
        } else {
183
            $table = "wiki";
184
        }
185
186
        // Otherwise, fetch the project's metadata from the meta.wiki table.
187
        $wikiQuery = $this->getMetaConnection()->createQueryBuilder();
188
        $wikiQuery->select(['dbname AS dbName', 'url', 'lang'])
189
            ->from($table)
190
            ->where($wikiQuery->expr()->eq('dbname', ':project'))
191
            // The meta database will have the project's URL stored as https://en.wikipedia.org
192
            // so we need to query for it accordingly, trying different variations the user
193
            // might have inputted.
194
            ->orWhere($wikiQuery->expr()->like('url', ':projectUrl'))
195
            ->orWhere($wikiQuery->expr()->like('url', ':projectUrl2'))
196
            ->orWhere($wikiQuery->expr()->like('url', ':projectUrl3'))
197
            ->orWhere($wikiQuery->expr()->like('url', ':projectUrl4'))
198
            ->setParameter('project', $project)
199
            ->setParameter('projectUrl', "https://$project")
200
            ->setParameter('projectUrl2', "https://$project.org")
201
            ->setParameter('projectUrl3', "https://www.$project")
202
            ->setParameter('projectUrl4', "https://www.$project.org");
203
        $wikiStatement = $this->executeQueryBuilder($wikiQuery);
204
205
        // Fetch and cache the wiki data.
206
        $basicInfo = $wikiStatement->fetch();
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($query, $params)->fetchAll();
378
        return count($pages) > 0;
379
    }
380
381
    /**
382
     * Get a list of the extensions installed on the wiki.
383
     * @param Project $project
384
     * @return string[]
385
     */
386
    public function getInstalledExtensions(Project $project): array
387
    {
388
        /** @var Client $client */
389
        $client = $this->container->get('eight_points_guzzle.client.xtools');
390
391
        $res = json_decode($client->request('GET', $project->getApiUrl(), ['query' => [
392
            'action' => 'query',
393
            'meta' => 'siteinfo',
394
            'siprop' => 'extensions',
395
            'format' => 'json',
396
        ]])->getBody()->getContents(), true);
397
398
        $extensions = $res['query']['extensions'] ?? [];
399
        return array_map(function ($extension) {
400
            return $extension['name'];
401
        }, $extensions);
402
    }
403
404
    /**
405
     * Get a list of users who are in one of the given user groups.
406
     * @param Project $project
407
     * @param string[] $groups List of user groups to look for.
408
     * @param string[] $globalGroups List of global groups to look for.
409
     * @return string[] with keys 'user_name' and 'ug_group'
410
     */
411
    public function getUsersInGroups(Project $project, array $groups = [], array $globalGroups = []): array
412
    {
413
        $cacheKey = $this->getCacheKey(func_get_args(), 'project_useringroups');
414
        if ($this->cache->hasItem($cacheKey)) {
415
            return $this->cache->getItem($cacheKey)->get();
416
        }
417
418
        $userTable = $project->getTableName('user');
419
        $userGroupsTable = $project->getTableName('user_groups');
420
421
        $rqb = $this->getProjectsConnection()->createQueryBuilder();
422
        $rqb->select(['user_name', 'ug_group AS user_group'])
423
            ->from($userTable)
424
            ->join($userTable, $userGroupsTable, 'user_groups', 'ug_user = user_id')
425
            ->where($rqb->expr()->in('ug_group', ':groups'))
426
            ->groupBy('user_name, user_group')
427
            ->setParameter(
428
                'groups',
429
                $groups,
430
                \Doctrine\DBAL\Connection::PARAM_STR_ARRAY
431
            );
432
433
        $users = $this->executeQueryBuilder($rqb)->fetchAll();
434
435
        if (count($globalGroups) > 0 && $this->isLabs()) {
436
            $rqb = $this->getProjectsConnection()->createQueryBuilder();
437
            $rqb->select(['gu_name AS user_name', 'gug_group AS user_group'])
438
                ->from('centralauth_p.global_user_groups', 'gug')
439
                ->join('gug', 'centralauth_p.globaluser', null, 'gug_user = gu_id')
440
                ->where($rqb->expr()->in('gug_group', ':globalGroups'))
441
                ->groupBy('user_name', 'user_group')
442
                ->setParameter(
443
                    'globalGroups',
444
                    $globalGroups,
445
                    \Doctrine\DBAL\Connection::PARAM_STR_ARRAY
446
                );
447
            $ret = $this->executeQueryBuilder($rqb)->fetchAll();
448
            $users = array_merge($users, $ret);
449
        }
450
451
        // Cache for 12 hours and return.
452
        return $this->setCache($cacheKey, $users, 'PT12H');
453
    }
454
}
455