Passed
Push — master ( 0c65d8...e43fc5 )
by MusikAnimal
18:43
created

ProjectRepository::getMetadata()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 71
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 42
nc 7
nop 1
dl 0
loc 71
rs 8.6257
c 0
b 0
f 0

How to fix   Long Method   

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 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
            'canonicalNamespaces' => [],
261
        ];
262
263
        // Even if general info could not be fetched,
264
        //   return dbName, url and lang if already known
265
        if (!empty($this->basicInfo)) {
266
            $this->metadata['dbName'] = $this->basicInfo['dbName'];
267
            $this->metadata['url'] = $this->basicInfo['url'];
268
            $this->metadata['lang'] = $this->basicInfo['lang'];
269
        }
270
271
        if (isset($res['query']['general'])) {
272
            $info = $res['query']['general'];
273
274
            $this->metadata['dbName'] = $info['wikiid'];
275
            $this->metadata['url'] = $info['server'];
276
            $this->metadata['lang'] = $info['lang'];
277
278
            $this->metadata['general'] = [
279
                'wikiName' => $info['sitename'],
280
                'articlePath' => $info['articlepath'],
281
                'scriptPath' => $info['scriptpath'],
282
                'script' => $info['script'],
283
                'timezone' => $info['timezone'],
284
                'timeOffset' => $info['timeoffset'],
285
                'mainpage' => $info['mainpage'],
286
            ];
287
        }
288
289
        $this->setNamespaces($res);
290
291
        // Cache for one hour and return.
292
        return $this->setCache($cacheKey, $this->metadata, 'PT1H');
293
    }
294
295
    /**
296
     * Set the namespaces on $this->metadata.
297
     * @param array $res As produced by meta=siteinfo API.
298
     */
299
    private function setNamespaces(array $res): void
300
    {
301
        if (!isset($res['query']['namespaces'])) {
302
            return;
303
        }
304
305
        foreach ($res['query']['namespaces'] as $namespace) {
306
            if ($namespace['id'] < 0) {
307
                continue;
308
            }
309
310
            if (isset($namespace['canonical'])) {
311
                $this->metadata['canonicalNamespaces'][$namespace['id']] = $namespace['canonical'];
312
            }
313
314
            if (isset($namespace['name'])) {
315
                $name = $namespace['name'];
316
            } elseif (isset($namespace['*'])) {
317
                $name = $namespace['*'];
318
            } else {
319
                continue;
320
            }
321
322
            $this->metadata['namespaces'][$namespace['id']] = $name;
323
        }
324
    }
325
326
    /**
327
     * Get a list of projects that have opted in to having all their users' restricted statistics available to anyone.
328
     * @return string[]
329
     */
330
    public function optedIn(): array
331
    {
332
        $optedIn = $this->container->getParameter('opted_in');
333
        // In case there's just one given.
334
        if (!is_array($optedIn)) {
335
            $optedIn = [ $optedIn ];
336
        }
337
        return $optedIn;
338
    }
339
340
    /**
341
     * The path to api.php.
342
     * @return string
343
     */
344
    public function getApiPath(): string
345
    {
346
        return $this->container->getParameter('api_path');
347
    }
348
349
    /**
350
     * Get a page from the given Project.
351
     * @param Project $project The project.
352
     * @param string $pageName The name of the page.
353
     * @return Page
354
     */
355
    public function getPage(Project $project, string $pageName): Page
356
    {
357
        $pageRepo = new PageRepository();
358
        $pageRepo->setContainer($this->container);
359
        $page = new Page($project, $pageName);
360
        $page->setRepository($pageRepo);
361
        return $page;
362
    }
363
364
    /**
365
     * Check to see if a page exists on this project and has some content.
366
     * @param Project $project The project.
367
     * @param int $namespaceId The page namespace ID.
368
     * @param string $pageTitle The page title, without namespace.
369
     * @return bool
370
     */
371
    public function pageHasContent(Project $project, int $namespaceId, string $pageTitle): bool
372
    {
373
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
374
        $query = "SELECT page_id "
375
             . " FROM $pageTable "
376
             . " WHERE page_namespace = :ns AND page_title = :title AND page_len > 0 "
377
             . " LIMIT 1";
378
        $params = [
379
            'ns' => $namespaceId,
380
            'title' => str_replace(' ', '_', $pageTitle),
381
        ];
382
        $pages = $this->executeProjectsQuery($query, $params)->fetchAll();
383
        return count($pages) > 0;
384
    }
385
386
    /**
387
     * Get a list of the extensions installed on the wiki.
388
     * @param Project $project
389
     * @return string[]
390
     */
391
    public function getInstalledExtensions(Project $project): array
392
    {
393
        /** @var Client $client */
394
        $client = $this->container->get('eight_points_guzzle.client.xtools');
395
396
        $res = json_decode($client->request('GET', $project->getApiUrl(), ['query' => [
397
            'action' => 'query',
398
            'meta' => 'siteinfo',
399
            'siprop' => 'extensions',
400
            'format' => 'json',
401
        ]])->getBody()->getContents(), true);
402
403
        $extensions = $res['query']['extensions'] ?? [];
404
        return array_map(function ($extension) {
405
            return $extension['name'];
406
        }, $extensions);
407
    }
408
409
    /**
410
     * Get a list of users who are in one of the given user groups.
411
     * @param Project $project
412
     * @param string[] $groups List of user groups to look for.
413
     * @param string[] $globalGroups List of global groups to look for.
414
     * @return string[] with keys 'user_name' and 'ug_group'
415
     */
416
    public function getUsersInGroups(Project $project, array $groups = [], array $globalGroups = []): array
417
    {
418
        $cacheKey = $this->getCacheKey(func_get_args(), 'project_useringroups');
419
        if ($this->cache->hasItem($cacheKey)) {
420
            return $this->cache->getItem($cacheKey)->get();
421
        }
422
423
        $userTable = $project->getTableName('user');
424
        $userGroupsTable = $project->getTableName('user_groups');
425
426
        $rqb = $this->getProjectsConnection()->createQueryBuilder();
427
        $rqb->select(['user_name', 'ug_group AS user_group'])
428
            ->from($userTable)
429
            ->join($userTable, $userGroupsTable, 'user_groups', 'ug_user = user_id')
430
            ->where($rqb->expr()->in('ug_group', ':groups'))
431
            ->groupBy('user_name, user_group')
432
            ->setParameter(
433
                'groups',
434
                $groups,
435
                \Doctrine\DBAL\Connection::PARAM_STR_ARRAY
436
            );
437
438
        $users = $this->executeQueryBuilder($rqb)->fetchAll();
439
440
        if (count($globalGroups) > 0 && $this->isLabs()) {
441
            $rqb = $this->getProjectsConnection()->createQueryBuilder();
442
            $rqb->select(['gu_name AS user_name', 'gug_group AS user_group'])
443
                ->from('centralauth_p.global_user_groups', 'gug')
444
                ->join('gug', 'centralauth_p.globaluser', null, 'gug_user = gu_id')
445
                ->where($rqb->expr()->in('gug_group', ':globalGroups'))
446
                ->groupBy('user_name', 'user_group')
447
                ->setParameter(
448
                    'globalGroups',
449
                    $globalGroups,
450
                    \Doctrine\DBAL\Connection::PARAM_STR_ARRAY
451
                );
452
            $ret = $this->executeQueryBuilder($rqb)->fetchAll();
453
            $users = array_merge($users, $ret);
454
        }
455
456
        // Cache for 12 hours and return.
457
        return $this->setCache($cacheKey, $users, 'PT12H');
458
    }
459
}
460