ProjectRepository::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 12
dl 0
loc 20
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types = 1);
4
5
namespace App\Repository;
6
7
use App\Model\PageAssessments;
8
use App\Model\Project;
9
use Doctrine\DBAL\Connection;
10
use Doctrine\Persistence\ManagerRegistry;
11
use Exception;
12
use GuzzleHttp\Client;
13
use Psr\Cache\CacheItemPoolInterface;
14
use Psr\Log\LoggerInterface;
15
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
16
17
/**
18
 * This class provides data to the Project class.
19
 * @codeCoverageIgnore
20
 */
21
class ProjectRepository extends Repository
22
{
23
    protected PageAssessmentsRepository $assessmentsRepo;
24
25
    /** @var string[] Basic metadata if XTools is in single-wiki mode. */
26
    protected array $singleBasicInfo;
27
28
    /** @var string The cache key for the 'all project' metadata. */
29
    protected string $cacheKeyAllProjects = 'allprojects';
30
31
    /** @var string The configured default project. */
32
    protected string $defaultProject;
33
34
    /** @var bool Whether XTools is configured to run on a single wiki or not. */
35
    protected bool $singleWiki;
36
37
    /** @var array Projects that have opted into showing restricted stats to everyone. */
38
    protected array $optedIn;
39
40
    /** @var string The project's API path. */
41
    protected string $apiPath;
42
43
    /**
44
     * @param ManagerRegistry $managerRegistry
45
     * @param CacheItemPoolInterface $cache
46
     * @param Client $guzzle
47
     * @param LoggerInterface $logger
48
     * @param ParameterBagInterface $parameterBag
49
     * @param bool $isWMF
50
     * @param int $queryTimeout
51
     * @param PageAssessmentsRepository $assessmentsRepo
52
     * @param string $defaultProject
53
     * @param bool $singleWiki
54
     * @param array $optedIn
55
     * @param string $apiPath
56
     */
57
    public function __construct(
58
        ManagerRegistry $managerRegistry,
59
        CacheItemPoolInterface $cache,
60
        Client $guzzle,
61
        LoggerInterface $logger,
62
        ParameterBagInterface $parameterBag,
63
        bool $isWMF,
64
        int $queryTimeout,
65
        PageAssessmentsRepository $assessmentsRepo,
66
        string $defaultProject,
67
        bool $singleWiki,
68
        array $optedIn,
69
        string $apiPath
70
    ) {
71
        $this->assessmentsRepo = $assessmentsRepo;
72
        $this->defaultProject = $defaultProject;
73
        $this->singleWiki = $singleWiki;
74
        $this->optedIn = $optedIn;
75
        $this->apiPath = $apiPath;
76
        parent::__construct($managerRegistry, $cache, $guzzle, $logger, $parameterBag, $isWMF, $queryTimeout);
77
    }
78
79
    /**
80
     * Convenience method to get a new Project object based on a given identification string.
81
     * @param string $projectIdent The domain name, database name, or URL of a project.
82
     * @return Project
83
     */
84
    public function getProject(string $projectIdent): Project
85
    {
86
        $project = new Project($projectIdent);
87
        $project->setRepository($this);
88
        $project->setPageAssessments(new PageAssessments($this->assessmentsRepo, $project));
89
90
        if ($this->singleWiki) {
91
            $this->setSingleBasicInfo([
92
                'url' => $this->parameterBag->get('wiki_url'),
93
                'dbName' => '', // Just so this will pass in CI.
94
                // TODO: this will need to be restored for third party support; KEYWORD: isWMF
95
                // 'dbName' => $this->parameterBag->('database_replica_name'),
96
            ]);
97
        }
98
99
        return $project;
100
    }
101
102
    /**
103
     * Get the XTools default project.
104
     * @return Project
105
     */
106
    public function getDefaultProject(): Project
107
    {
108
        return $this->getProject($this->defaultProject);
109
    }
110
111
    /**
112
     * Get the global 'meta' project, which is either Meta (if this is Labs) or the default project.
113
     * @return Project
114
     */
115
    public function getGlobalProject(): Project
116
    {
117
        if ($this->isWMF) {
118
            return $this->getProject('metawiki');
119
        } else {
120
            return $this->getDefaultProject();
121
        }
122
    }
123
124
    /**
125
     * For single-wiki installations, you must manually set the wiki URL and database name
126
     * (because there's no meta.wiki database to query).
127
     * @param array $metadata
128
     * @throws Exception
129
     */
130
    public function setSingleBasicInfo(array $metadata): void
131
    {
132
        if (!array_key_exists('url', $metadata) || !array_key_exists('dbName', $metadata)) {
133
            $error = "Single-wiki metadata should contain 'url', 'dbName' and 'lang' keys.";
134
            throw new Exception($error);
135
        }
136
        $this->singleBasicInfo = array_intersect_key($metadata, [
137
            'url' => '',
138
            'dbName' => '',
139
            'lang' => '',
140
        ]);
141
    }
142
143
    /**
144
     * Get the 'dbName', 'url' and 'lang' of all projects.
145
     * @return string[][] Each item has 'dbName', 'url' and 'lang' keys.
146
     */
147
    public function getAll(): array
148
    {
149
        $this->logger->debug(__METHOD__." Getting all projects' metadata");
150
        // Single wiki mode?
151
        if (!empty($this->singleBasicInfo)) {
152
            return [$this->getOne('')];
153
        }
154
155
        // Maybe we've already fetched it.
156
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
157
            return $this->cache->getItem($this->cacheKeyAllProjects)->get();
158
        }
159
160
        if ($this->parameterBag->has("database_meta_table")) {
161
            $table = $this->parameterBag->get('database_meta_name') . '.' .
162
                $this->parameterBag->get('database_meta_table');
163
        } else {
164
            $table = "meta_p.wiki";
165
        }
166
167
        // Otherwise, fetch all from the database.
168
        $sql = "SELECT dbname AS dbName, url, lang FROM $table";
169
        $projects = $this->executeProjectsQuery('meta', $sql)
170
            ->fetchAllAssociative();
171
        $projectsMetadata = [];
172
        foreach ($projects as $project) {
173
            $projectsMetadata[$project['dbName']] = $project;
174
        }
175
176
        // Cache for one day and return.
177
        return $this->setCache(
178
            $this->cacheKeyAllProjects,
179
            $projectsMetadata,
180
            'P1D'
181
        );
182
    }
183
184
    /**
185
     * Get the 'dbName', 'url' and 'lang' of a project. This is all you need to make database queries.
186
     * More comprehensive metadata can be fetched with getMetadata() at the expense of an API call.
187
     * @param string $project A project URL, domain name, or database name.
188
     * @return string[]|null With 'dbName', 'url' and 'lang' keys; or null if not found.
189
     */
190
    public function getOne(string $project): ?array
191
    {
192
        $this->logger->debug(__METHOD__." Getting metadata about $project");
193
        // For single-wiki setups, every project is the same.
194
        if (isset($this->singleBasicInfo)) {
195
            return $this->singleBasicInfo;
196
        }
197
198
        // Remove _p suffix.
199
        $project = rtrim($project, '_p');
200
201
        // For multi-wiki setups, first check the cache.
202
        // First the all-projects cache, then the individual one.
203
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
204
            foreach ($this->cache->getItem($this->cacheKeyAllProjects)->get() as $projMetadata) {
205
                if ($projMetadata['dbName'] == "$project"
206
                    || $projMetadata['url'] == "$project"
207
                    || $projMetadata['url'] == "https://$project"
208
                    || $projMetadata['url'] == "https://$project.org"
209
                    || $projMetadata['url'] == "https://www.$project") {
210
                    $this->logger->debug(__METHOD__ . " Using cached data for $project");
211
                    return $projMetadata;
212
                }
213
            }
214
        }
215
        $cacheKey = $this->getCacheKey($project, 'project');
216
        if ($this->cache->hasItem($cacheKey)) {
217
            return $this->cache->getItem($cacheKey)->get();
218
        }
219
220
        // TODO: make this configurable if XTools is to work on 3rd party wiki farms
221
        $table = "meta_p.wiki";
222
223
        // Otherwise, fetch the project's metadata from the meta.wiki table.
224
        $sql = "SELECT dbname AS dbName, url, lang
225
                FROM $table
226
                WHERE dbname = :project
227
                    OR url LIKE :projectUrl
228
                    OR url LIKE :projectUrl2
229
                    OR url LIKE :projectUrl3
230
                    OR url LIKE :projectUrl4";
231
        $basicInfo = $this->executeProjectsQuery('meta', $sql, [
232
            'project' => $project,
233
            'projectUrl' => "https://$project",
234
            'projectUrl2' => "https://$project.org",
235
            'projectUrl3' => "https://www.$project",
236
            'projectUrl4' => "https://www.$project.org",
237
        ])->fetchAssociative();
238
        $basicInfo = false === $basicInfo ? null : $basicInfo;
239
240
        // Cache for one hour and return.
241
        return $this->setCache($cacheKey, $basicInfo, 'PT1H');
242
    }
243
244
    /**
245
     * Get metadata about a project, including the 'dbName', 'url' and 'lang'
246
     *
247
     * @param string $projectUrl The project's URL.
248
     * @return array|null With 'dbName', 'url', 'lang', 'general' and 'namespaces' keys.
249
     *   'general' contains: 'wikiName', 'articlePath', 'scriptPath', 'script',
250
     *   'timezone', and 'timezoneOffset'; 'namespaces' contains all namespace
251
     *   names, keyed by their IDs. If this function returns null, the API call
252
     *   failed.
253
     */
254
    public function getMetadata(string $projectUrl): ?array
255
    {
256
        $cacheKey = $this->getCacheKey(func_get_args(), "project_metadata");
257
        if ($this->cache->hasItem($cacheKey)) {
258
            return $this->cache->getItem($cacheKey)->get();
259
        }
260
261
        try {
262
            $res = json_decode($this->guzzle->request('GET', $projectUrl.$this->getApiPath(), [
263
                'query' => [
264
                    'action' => 'query',
265
                    'meta' => 'siteinfo',
266
                    'siprop' => 'general|namespaces',
267
                    'format' => 'json',
268
                ],
269
            ])->getBody()->getContents(), true);
270
        } catch (Exception $e) {
271
            return null;
272
        }
273
274
        $metadata = [
275
            'general' => [],
276
            'namespaces' => [],
277
        ];
278
279
        if (isset($res['query']['general'])) {
280
            $info = $res['query']['general'];
281
282
            $metadata['dbName'] = $info['wikiid'];
283
            $metadata['url'] = $info['server'];
284
            $metadata['lang'] = $info['lang'];
285
286
            $metadata['general'] = [
287
                'wikiName' => $info['sitename'],
288
                'articlePath' => $info['articlepath'],
289
                'scriptPath' => $info['scriptpath'],
290
                'script' => $info['script'],
291
                'timezone' => $info['timezone'],
292
                'timeOffset' => $info['timeoffset'],
293
                'mainpage' => $info['mainpage'],
294
            ];
295
        }
296
297
        $this->setNamespaces($res, $metadata);
298
299
        // Cache for one hour and return.
300
        return $this->setCache($cacheKey, $metadata, 'PT1H');
301
    }
302
303
    /**
304
     * Set the namespaces on the given $metadata.
305
     * @param array $res As produced by meta=siteinfo API.
306
     * @param array &$metadata The metadata array to modify.
307
     */
308
    private function setNamespaces(array $res, array &$metadata): void
309
    {
310
        if (!isset($res['query']['namespaces'])) {
311
            return;
312
        }
313
314
        foreach ($res['query']['namespaces'] as $namespace) {
315
            if ($namespace['id'] < 0) {
316
                continue;
317
            }
318
319
            if (isset($namespace['name'])) {
320
                $name = $namespace['name'];
321
            } elseif (isset($namespace['*'])) {
322
                $name = $namespace['*'];
323
            } else {
324
                continue;
325
            }
326
327
            $metadata['namespaces'][$namespace['id']] = $name;
328
        }
329
    }
330
331
    /**
332
     * Get a list of projects that have opted in to having all their users' restricted statistics available to anyone.
333
     * @return string[]
334
     */
335
    public function optedIn(): array
336
    {
337
        return $this->optedIn;
338
    }
339
340
    /**
341
     * The path to api.php.
342
     * @return string
343
     */
344
    public function getApiPath(): string
345
    {
346
        return $this->apiPath;
347
    }
348
349
    /**
350
     * Check to see if a page exists on this project and has some content.
351
     * @param Project $project The project.
352
     * @param int $namespaceId The page namespace ID.
353
     * @param string $pageTitle The page title, without namespace.
354
     * @return bool
355
     */
356
    public function pageHasContent(Project $project, int $namespaceId, string $pageTitle): bool
357
    {
358
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
359
        $query = "SELECT page_id "
360
             . " FROM $pageTable "
361
             . " WHERE page_namespace = :ns AND page_title = :title AND page_len > 0 "
362
             . " LIMIT 1";
363
        $params = [
364
            'ns' => $namespaceId,
365
            'title' => str_replace(' ', '_', $pageTitle),
366
        ];
367
        $pages = $this->executeProjectsQuery($project, $query, $params)
368
            ->fetchAllAssociative();
369
        return count($pages) > 0;
370
    }
371
372
    /**
373
     * Get a list of the extensions installed on the wiki.
374
     * @param Project $project
375
     * @return string[]
376
     */
377
    public function getInstalledExtensions(Project $project): array
378
    {
379
        $res = json_decode($this->guzzle->request('GET', $project->getApiUrl(), ['query' => [
380
            'action' => 'query',
381
            'meta' => 'siteinfo',
382
            'siprop' => 'extensions',
383
            'format' => 'json',
384
        ]])->getBody()->getContents(), true);
385
386
        $extensions = $res['query']['extensions'] ?? [];
387
        return array_map(function ($extension) {
388
            return $extension['name'];
389
        }, $extensions);
390
    }
391
392
    /**
393
     * Get a list of users who are in one of the given user groups.
394
     * @param Project $project
395
     * @param string[] $groups List of user groups to look for.
396
     * @param string[] $globalGroups List of global groups to look for.
397
     * @return string[] with keys 'user_name' and 'ug_group'
398
     */
399
    public function getUsersInGroups(Project $project, array $groups = [], array $globalGroups = []): array
400
    {
401
        $cacheKey = $this->getCacheKey(func_get_args(), 'project_useringroups');
402
        if ($this->cache->hasItem($cacheKey)) {
403
            return $this->cache->getItem($cacheKey)->get();
404
        }
405
406
        $userTable = $project->getTableName('user');
407
        $userGroupsTable = $project->getTableName('user_groups');
408
409
        $sql = "SELECT user_name, ug_group AS user_group
410
                FROM $userTable
411
                JOIN $userGroupsTable ON ug_user = user_id
412
                WHERE ug_group IN (?)
413
                GROUP BY user_name, ug_group";
414
        $users = $this->getProjectsConnection($project)
415
            ->executeQuery($sql, [$groups], [Connection::PARAM_STR_ARRAY])
416
            ->fetchAllAssociative();
417
418
        if (count($globalGroups) > 0 && $this->isWMF) {
419
            $sql = "SELECT gu_name AS user_name, gug_group AS user_group
420
                    FROM centralauth_p.global_user_groups
421
                    JOIN centralauth_p.globaluser ON gug_user = gu_id
422
                    WHERE gug_group IN (?)
423
                    GROUP BY user_name, user_group";
424
            $globalUsers = $this->getProjectsConnection('centralauth')
425
                ->executeQuery($sql, [$globalGroups], [Connection::PARAM_STR_ARRAY])
426
                ->fetchAllAssociative();
427
428
            $users = array_merge($users, $globalUsers);
429
        }
430
431
        // Cache for 12 hours and return.
432
        return $this->setCache($cacheKey, $users, 'PT12H');
433
    }
434
}
435