Passed
Push — master ( 94d9d8...f8825d )
by MusikAnimal
07:28
created

ProjectRepository::setSingleBasicInfo()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 1
dl 0
loc 10
rs 10
c 0
b 0
f 0
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 Mediawiki\Api\MediawikiApi;
14
use Mediawiki\Api\SimpleRequest;
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' => $container->getParameter('database_replica_name'),
56
            ]);
57
        }
58
        $project->setRepository($projectRepo);
59
60
        return $project;
61
    }
62
63
    /**
64
     * Get the XTools default project.
65
     * @param ContainerInterface $container
66
     * @return Project
67
     */
68
    public static function getDefaultProject(ContainerInterface $container): Project
69
    {
70
        $defaultProjectName = $container->getParameter('default_project');
71
        return self::getProject($defaultProjectName, $container);
72
    }
73
74
    /**
75
     * Get the global 'meta' project, which is either Meta (if this is Labs) or the default project.
76
     * @return Project
77
     */
78
    public function getGlobalProject(): Project
79
    {
80
        if ($this->isLabs()) {
81
            return self::getProject('metawiki', $this->container);
82
        } else {
83
            return self::getDefaultProject($this->container);
84
        }
85
    }
86
87
    /**
88
     * For single-wiki installations, you must manually set the wiki URL and database name
89
     * (because there's no meta.wiki database to query).
90
     * @param array $metadata
91
     * @throws \Exception
92
     */
93
    public function setSingleBasicInfo(array $metadata): void
94
    {
95
        if (!array_key_exists('url', $metadata) || !array_key_exists('dbName', $metadata)) {
96
            $error = "Single-wiki metadata should contain 'url', 'dbName' and 'lang' keys.";
97
            throw new \Exception($error);
98
        }
99
        $this->singleBasicInfo = array_intersect_key($metadata, [
100
            'url' => '',
101
            'dbName' => '',
102
            'lang' => '',
103
        ]);
104
    }
105
106
    /**
107
     * Get the 'dbName', 'url' and 'lang' of all projects.
108
     * @return string[][] Each item has 'dbName', 'url' and 'lang' keys.
109
     */
110
    public function getAll(): array
111
    {
112
        $this->log->debug(__METHOD__." Getting all projects' metadata");
113
        // Single wiki mode?
114
        if (!empty($this->singleBasicInfo)) {
115
            return [$this->getOne('')];
116
        }
117
118
        // Maybe we've already fetched it.
119
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
120
            return $this->cache->getItem($this->cacheKeyAllProjects)->get();
121
        }
122
123
        if ($this->container->hasParameter("database_meta_table")) {
124
            $table = $this->container->getParameter("database_meta_table");
125
        } else {
126
            $table = "wiki";
127
        }
128
129
        // Otherwise, fetch all from the database.
130
        $wikiQuery = $this->getMetaConnection()->createQueryBuilder();
131
        $wikiQuery->select(['dbname AS dbName', 'url', 'lang'])->from($table);
132
        $projects = $this->executeQueryBuilder($wikiQuery)->fetchAll();
133
        $projectsMetadata = [];
134
        foreach ($projects as $project) {
135
            $projectsMetadata[$project['dbName']] = $project;
136
        }
137
138
        // Cache for one day and return.
139
        return $this->setCache(
140
            $this->cacheKeyAllProjects,
141
            $projectsMetadata,
142
            'P1D'
143
        );
144
    }
145
146
    /**
147
     * Get the 'dbName', 'url' and 'lang' of a project. This is all you need to make database queries.
148
     * More comprehensive metadata can be fetched with getMetadata() at the expense of an API call.
149
     * @param string $project A project URL, domain name, or database name.
150
     * @return string[]|bool With 'dbName', 'url' and 'lang' keys; or false if not found.
151
     */
152
    public function getOne(string $project)
153
    {
154
        $this->log->debug(__METHOD__." Getting metadata about $project");
155
        // For single-wiki setups, every project is the same.
156
        if (!empty($this->singleBasicInfo)) {
157
            return $this->singleBasicInfo;
158
        }
159
160
        // For multi-wiki setups, first check the cache.
161
        // First the all-projects cache, then the individual one.
162
        if ($this->cache->hasItem($this->cacheKeyAllProjects)) {
163
            foreach ($this->cache->getItem($this->cacheKeyAllProjects)->get() as $projMetadata) {
164
                if ($projMetadata['dbName'] == "$project"
165
                    || $projMetadata['url'] == "$project"
166
                    || $projMetadata['url'] == "https://$project"
167
                    || $projMetadata['url'] == "https://$project.org"
168
                    || $projMetadata['url'] == "https://www.$project") {
169
                    $this->log->debug(__METHOD__ . " Using cached data for $project");
170
                    return $projMetadata;
171
                }
172
            }
173
        }
174
        $cacheKey = $this->getCacheKey($project, 'project');
175
        if ($this->cache->hasItem($cacheKey)) {
176
            return $this->cache->getItem($cacheKey)->get();
177
        }
178
179
        if ($this->container->hasParameter("database_meta_table")) {
180
            $table = $this->container->getParameter("database_meta_table");
181
        } else {
182
            $table = "wiki";
183
        }
184
185
        // Otherwise, fetch the project's metadata from the meta.wiki table.
186
        $wikiQuery = $this->getMetaConnection()->createQueryBuilder();
187
        $wikiQuery->select(['dbname AS dbName', 'url', 'lang'])
188
            ->from($table)
189
            ->where($wikiQuery->expr()->eq('dbname', ':project'))
190
            // The meta database will have the project's URL stored as https://en.wikipedia.org
191
            // so we need to query for it accordingly, trying different variations the user
192
            // might have inputted.
193
            ->orWhere($wikiQuery->expr()->like('url', ':projectUrl'))
194
            ->orWhere($wikiQuery->expr()->like('url', ':projectUrl2'))
195
            ->orWhere($wikiQuery->expr()->like('url', ':projectUrl3'))
196
            ->orWhere($wikiQuery->expr()->like('url', ':projectUrl4'))
197
            ->setParameter('project', $project)
198
            ->setParameter('projectUrl', "https://$project")
199
            ->setParameter('projectUrl2', "https://$project.org")
200
            ->setParameter('projectUrl3', "https://www.$project")
201
            ->setParameter('projectUrl4', "https://www.$project.org");
202
        $wikiStatement = $this->executeQueryBuilder($wikiQuery);
203
204
        // Fetch and cache the wiki data.
205
        $basicInfo = $wikiStatement->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
        try {
241
            $api = MediawikiApi::newFromPage($projectUrl);
242
        } catch (\Exception $e) {
243
            return null;
244
        }
245
246
        $params = ['meta' => 'siteinfo', 'siprop' => 'general|namespaces'];
247
        $query = new SimpleRequest('query', $params);
248
249
        $this->metadata = [
250
            'general' => [],
251
            'namespaces' => [],
252
        ];
253
254
        // Even if general info could not be fetched,
255
        //   return dbName, url and lang if already known
256
        if (!empty($this->basicInfo)) {
257
            $this->metadata['dbName'] = $this->basicInfo['dbName'];
258
            $this->metadata['url'] = $this->basicInfo['url'];
259
            $this->metadata['lang'] = $this->basicInfo['lang'];
260
        }
261
262
        $res = $api->getRequest($query);
263
264
        if (isset($res['query']['general'])) {
265
            $info = $res['query']['general'];
266
267
            $this->metadata['dbName'] = $info['wikiid'];
268
            $this->metadata['url'] = $info['server'];
269
            $this->metadata['lang'] = $info['lang'];
270
271
            $this->metadata['general'] = [
272
                'wikiName' => $info['sitename'],
273
                'articlePath' => $info['articlepath'],
274
                'scriptPath' => $info['scriptpath'],
275
                'script' => $info['script'],
276
                'timezone' => $info['timezone'],
277
                'timeOffset' => $info['timeoffset'],
278
                'mainpage' => $info['mainpage'],
279
            ];
280
        }
281
282
        if (isset($res['query']['namespaces'])) {
283
            foreach ($res['query']['namespaces'] as $namespace) {
284
                if ($namespace['id'] < 0) {
285
                    continue;
286
                }
287
288
                if (isset($namespace['name'])) {
289
                    $name = $namespace['name'];
290
                } elseif (isset($namespace['*'])) {
291
                    $name = $namespace['*'];
292
                } else {
293
                    continue;
294
                }
295
296
                $this->metadata['namespaces'][$namespace['id']] = $name;
297
            }
298
        }
299
300
        // Cache for one hour and return.
301
        return $this->setCache($cacheKey, $this->metadata, 'PT1H');
302
    }
303
304
    /**
305
     * Get a list of projects that have opted in to having all their users' restricted statistics available to anyone.
306
     * @return string[]
307
     */
308
    public function optedIn(): array
309
    {
310
        $optedIn = $this->container->getParameter('opted_in');
311
        // In case there's just one given.
312
        if (!is_array($optedIn)) {
313
            $optedIn = [ $optedIn ];
314
        }
315
        return $optedIn;
316
    }
317
318
    /**
319
     * The path to api.php.
320
     * @return string
321
     */
322
    public function getApiPath(): string
323
    {
324
        return $this->container->getParameter('api_path');
325
    }
326
327
    /**
328
     * Get a page from the given Project.
329
     * @param Project $project The project.
330
     * @param string $pageName The name of the page.
331
     * @return Page
332
     */
333
    public function getPage(Project $project, string $pageName): Page
334
    {
335
        $pageRepo = new PageRepository();
336
        $pageRepo->setContainer($this->container);
337
        $page = new Page($project, $pageName);
338
        $page->setRepository($pageRepo);
339
        return $page;
340
    }
341
342
    /**
343
     * Check to see if a page exists on this project and has some content.
344
     * @param Project $project The project.
345
     * @param int $namespaceId The page namespace ID.
346
     * @param string $pageTitle The page title, without namespace.
347
     * @return bool
348
     */
349
    public function pageHasContent(Project $project, int $namespaceId, string $pageTitle): bool
350
    {
351
        $pageTable = $this->getTableName($project->getDatabaseName(), 'page');
352
        $query = "SELECT page_id "
353
             . " FROM $pageTable "
354
             . " WHERE page_namespace = :ns AND page_title = :title AND page_len > 0 "
355
             . " LIMIT 1";
356
        $params = [
357
            'ns' => $namespaceId,
358
            'title' => str_replace(' ', '_', $pageTitle),
359
        ];
360
        $pages = $this->executeProjectsQuery($query, $params)->fetchAll();
361
        return count($pages) > 0;
362
    }
363
364
    /**
365
     * Get a list of the extensions installed on the wiki.
366
     * @param Project $project
367
     * @return string[]
368
     */
369
    public function getInstalledExtensions(Project $project): array
370
    {
371
        $client = new Client();
372
373
        $res = json_decode($client->request('GET', $project->getApiUrl(), ['query' => [
374
            'action' => 'query',
375
            'meta' => 'siteinfo',
376
            'siprop' => 'extensions',
377
            'format' => 'json',
378
        ]])->getBody()->getContents(), true);
379
380
        $extensions = $res['query']['extensions'] ?? [];
381
        return array_map(function ($extension) {
382
            return $extension['name'];
383
        }, $extensions);
384
    }
385
386
    /**
387
     * Get a list of users who are in one of the given user groups.
388
     * @param Project $project
389
     * @param string[] List of user groups to look for.
0 ignored issues
show
Bug introduced by
The type AppBundle\Repository\List was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
390
     * @return string[] with keys 'user_name' and 'ug_group'
391
     */
392
    public function getUsersInGroups(Project $project, array $groups): array
393
    {
394
        $cacheKey = $this->getCacheKey(func_get_args(), 'project_useringroups');
395
        if ($this->cache->hasItem($cacheKey)) {
396
            return $this->cache->getItem($cacheKey)->get();
397
        }
398
399
        $userTable = $project->getTableName('user');
400
        $userGroupsTable = $project->getTableName('user_groups');
401
402
        $query = $this->getProjectsConnection()->createQueryBuilder();
403
        $query->select(['user_name', 'ug_group'])
404
            ->from($userTable)
405
            ->join($userTable, $userGroupsTable, null, 'ug_user = user_id')
406
            ->where($query->expr()->in('ug_group', ':groups'))
407
            ->groupBy('user_name, ug_group')
408
            ->setParameter(
409
                'groups',
410
                $groups,
411
                \Doctrine\DBAL\Connection::PARAM_STR_ARRAY
412
            );
413
        $admins = $this->executeQueryBuilder($query)->fetchAll();
414
415
        // Cache for one hour and return.
416
        return $this->setCache($cacheKey, $admins, 'PT1H');
417
    }
418
}
419