Passed
Push — master ( 4b4993...dc4c2b )
by MusikAnimal
05:55
created

ProjectRepository::getProject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the ProjectRepository class.
4
 */
5
6
namespace Xtools;
7
8
use Mediawiki\Api\MediawikiApi;
9
use Mediawiki\Api\SimpleRequest;
10
use Psr\Container\ContainerInterface;
11
12
/**
13
 * This class provides data to the Project class.
14
 * @codeCoverageIgnore
15
 */
16
class ProjectRepository extends Repository
17
{
18
    /** @var array Project's 'dbName', 'url' and 'lang'. */
19
    protected $basicInfo;
20
21
    /** @var string[] Basic metadata if XTools is in single-wiki mode. */
22
    protected $singleBasicInfo;
23
24
    /** @var array Full Project metadata, including $basicInfo. */
25
    protected $metadata;
26
27
    /** @var string The cache key for the 'all project' metadata. */
28
    protected $cacheKeyAllProjects = 'allprojects';
29
30
    /**
31
     * Convenience method to get a new Project object based on a given identification string.
32
     * @param string $projectIdent The domain name, database name, or URL of a project.
33
     * @param ContainerInterface $container Symfony's container.
34
     * @return Project
35
     */
36
    public static function getProject($projectIdent, ContainerInterface $container)
37
    {
38
        $project = new Project($projectIdent);
39
        $projectRepo = new ProjectRepository();
40
        $projectRepo->setContainer($container);
41
42
        // The associated PageAssessmentsRepository also needs the container.
43
        $paRepo = new PageAssessmentsRepository();
44
        $paRepo->setContainer($container);
45
        $project->getPageAssessments()->setRepository($paRepo);
46
47
        if ($container->getParameter('app.single_wiki')) {
0 ignored issues
show
Bug introduced by
The method getParameter() does not exist on Psr\Container\ContainerInterface. It seems like you code against a sub-type of Psr\Container\ContainerInterface such as Symfony\Component\Depend...tion\ContainerInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

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