Passed
Push — master ( f43d54...b6518a )
by MusikAnimal
01:39
created

Page::getProject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the Page class.
4
 */
5
6
namespace Xtools;
7
8
use DateTime;
9
10
/**
11
 * A Page is a single wiki page in one project.
12
 */
13
class Page extends Model
14
{
15
16
    /** @var Project The project that this page belongs to. */
17
    protected $project;
18
19
    /** @var string The page name as provided at instantiation. */
20
    protected $unnormalizedPageName;
21
22
    /** @var string[] Metadata about this page. */
23
    protected $pageInfo;
24
25
    /** @var string[] Revision history of this page. */
26
    protected $revisions;
27
28
    /** @var int Number of revisions for this page. */
29
    protected $numRevisions;
30
31
    /** @var string[] List of Wikidata sitelinks for this page. */
32
    protected $wikidataItems;
33
34
    /** @var int Number of Wikidata sitelinks for this page. */
35
    protected $numWikidataItems;
36
37
    /**
38
     * Page constructor.
39
     * @param Project $project
40
     * @param string $pageName
41
     */
42 35
    public function __construct(Project $project, $pageName)
43
    {
44 35
        $this->project = $project;
45 35
        $this->unnormalizedPageName = $pageName;
46 35
    }
47
48
    /**
49
     * Unique identifier for this Page, to be used in cache keys.
50
     * Use of md5 ensures the cache key does not contain reserved characters.
51
     * @see Repository::getCacheKey()
52
     * @return string
53
     * @codeCoverageIgnore
54
     */
55
    public function getCacheKey()
56
    {
57
        return md5($this->getId());
58
    }
59
60
    /**
61
     * Get basic information about this page from the repository.
62
     * @return \string[]
63
     */
64 9
    protected function getPageInfo()
65
    {
66 9
        if (empty($this->pageInfo)) {
67 9
            $this->pageInfo = $this->getRepository()
68 9
                    ->getPageInfo($this->project, $this->unnormalizedPageName);
69
        }
70 9
        return $this->pageInfo;
71
    }
72
73
    /**
74
     * Get the page's title.
75
     * @param bool $useUnnormalized Use the unnormalized page title to avoid an
76
     *    API call. This should be used only if you fetched the page title via
77
     *    other means (SQL query), and is not from user input alone.
78
     * @return string
79
     */
80 3
    public function getTitle($useUnnormalized = false)
81
    {
82 3
        if ($useUnnormalized) {
83 1
            return $this->unnormalizedPageName;
84
        }
85 3
        $info = $this->getPageInfo();
86 3
        return isset($info['title']) ? $info['title'] : $this->unnormalizedPageName;
87
    }
88
89
    /**
90
     * Get the page's title without the namespace.
91
     * @return string
92
     */
93 1
    public function getTitleWithoutNamespace()
94
    {
95 1
        $info = $this->getPageInfo();
96 1
        $title = isset($info['title']) ? $info['title'] : $this->unnormalizedPageName;
97 1
        $nsName = $this->getNamespaceName();
98 1
        return str_replace($nsName . ':', '', $title);
99
    }
100
101
    /**
102
     * Get this page's database ID.
103
     * @return int
104
     */
105 2
    public function getId()
106
    {
107 2
        $info = $this->getPageInfo();
108 2
        return isset($info['pageid']) ? $info['pageid'] : null;
109
    }
110
111
    /**
112
     * Get this page's length in bytes.
113
     * @return int
114
     */
115 1
    public function getLength()
116
    {
117 1
        $info = $this->getPageInfo();
118 1
        return isset($info['length']) ? $info['length'] : null;
119
    }
120
121
    /**
122
     * Get HTML for the stylized display of the title.
123
     * The text will be the same as Page::getTitle().
124
     * @return string
125
     */
126 1
    public function getDisplayTitle()
127
    {
128 1
        $info = $this->getPageInfo();
129 1
        if (isset($info['displaytitle'])) {
130 1
            return $info['displaytitle'];
131
        }
132 1
        return $this->getTitle();
133
    }
134
135
    /**
136
     * Get the full URL of this page.
137
     * @return string
138
     */
139 1
    public function getUrl()
140
    {
141 1
        $info = $this->getPageInfo();
142 1
        return isset($info['fullurl']) ? $info['fullurl'] : null;
143
    }
144
145
    /**
146
     * Get the numerical ID of the namespace of this page.
147
     * @return int
148
     */
149 2
    public function getNamespace()
150
    {
151 2
        $info = $this->getPageInfo();
152 2
        return isset($info['ns']) ? $info['ns'] : null;
153
    }
154
155
    /**
156
     * Get the name of the namespace of this page.
157
     * @return string
158
     */
159 1
    public function getNamespaceName()
160
    {
161 1
        $info = $this->getPageInfo();
162 1
        return isset($info['ns'])
163 1
            ? $this->getProject()->getNamespaces()[$info['ns']]
164 1
            : null;
165
    }
166
167
    /**
168
     * Get the number of page watchers.
169
     * @return int
170
     */
171 1
    public function getWatchers()
172
    {
173 1
        $info = $this->getPageInfo();
174 1
        return isset($info['watchers']) ? $info['watchers'] : null;
175
    }
176
177
    /**
178
     * Get the HTML content of the body of the page.
179
     * @param DateTime|int $target If a DateTime object, the
180
     *   revision at that time will be returned. If an integer, it is
181
     *   assumed to be the actual revision ID.
182
     * @return string
183
     */
184 1
    public function getHTMLContent($target = null)
185
    {
186 1
        if (is_a($target, 'DateTime')) {
187
            $target = $this->getRepository()->getRevisionIdAtDate($this, $target);
188
        }
189 1
        return $this->getRepository()->getHTMLContent($this, $target);
190
    }
191
192
    /**
193
     * Whether or not this page exists.
194
     * @return bool
195
     */
196 1
    public function exists()
197
    {
198 1
        $info = $this->getPageInfo();
199 1
        return !isset($info['missing']) && !isset($info['invalid']);
200
    }
201
202
    /**
203
     * Get the Project to which this page belongs.
204
     * @return Project
205
     */
206 15
    public function getProject()
207
    {
208 15
        return $this->project;
209
    }
210
211
    /**
212
     * Get the language code for this page.
213
     * If not set, the language code for the project is returned.
214
     * @return string
215
     */
216 2
    public function getLang()
217
    {
218 2
        $info = $this->getPageInfo();
219 2
        if (isset($info['pagelanguage'])) {
220 2
            return $info['pagelanguage'];
221
        } else {
222
            return $this->getProject()->getLang();
223
        }
224
    }
225
226
    /**
227
     * Get the Wikidata ID of this page.
228
     * @return string
229
     */
230 4
    public function getWikidataId()
231
    {
232 4
        $info = $this->getPageInfo();
233 4
        if (isset($info['pageprops']['wikibase_item'])) {
234 3
            return $info['pageprops']['wikibase_item'];
235
        } else {
236 1
            return null;
237
        }
238
    }
239
240
    /**
241
     * Get the number of revisions the page has.
242
     * @param User $user Optionally limit to those of this user.
243
     * @param false|int $start
244
     * @param false|int $end
245
     * @return int
246
     */
247 4
    public function getNumRevisions(User $user = null, $start = false, $end = false)
248
    {
249
        // If a user is given, we will not cache the result via instance variable.
250 4
        if ($user !== null) {
251
            return (int) $this->getRepository()->getNumRevisions($this, $user, $start, $end);
252
        }
253
254
        // Return cached value, if present.
255 4
        if ($this->numRevisions !== null) {
0 ignored issues
show
introduced by
The condition $this->numRevisions !== null can never be false.
Loading history...
256
            return $this->numRevisions;
257
        }
258
259
        // Otherwise, return the count of all revisions if already present.
260 4
        if ($this->revisions !== null) {
261
            $this->numRevisions = count($this->revisions);
262
        } else {
263
            // Otherwise do a COUNT in the event fetching all revisions is not desired.
264 4
            $this->numRevisions = (int) $this->getRepository()->getNumRevisions($this, null, $start, $end);
265
        }
266
267 4
        return $this->numRevisions;
268
    }
269
270
    /**
271
     * Get all edits made to this page.
272
     * @param User|null $user Specify to get only revisions by the given user.
273
     * @param false|int $start
274
     * @param false|int $end
275
     * @return array
276
     */
277
    public function getRevisions(User $user = null, $start = false, $end = false)
278
    {
279
        if ($this->revisions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->revisions of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
280
            return $this->revisions;
281
        }
282
283
        $this->revisions = $this->getRepository()->getRevisions($this, $user, $start, $end);
284
285
        return $this->revisions;
286
    }
287
288
    /**
289
     * Get the full page wikitext.
290
     * @return string|null Null if nothing was found.
291
     */
292 1
    public function getWikitext()
293
    {
294 1
        $content = $this->getRepository()->getPagesWikitext(
295 1
            $this->getProject(),
296 1
            [ $this->getTitle() ]
297
        );
298
299 1
        return isset($content[$this->getTitle()])
300 1
            ? $content[$this->getTitle()]
301 1
            : null;
302
    }
303
304
    /**
305
     * Get the statement for a single revision, so that you can iterate row by row.
306
     * @see PageRepository::getRevisionsStmt()
307
     * @param User|null $user Specify to get only revisions by the given user.
308
     * @param int $limit Max number of revisions to process.
309
     * @param int $numRevisions Number of revisions, if known. This is used solely to determine the
310
     *   OFFSET if we are given a $limit. If $limit is set and $numRevisions is not set, a
311
     *   separate query is ran to get the nuber of revisions.
312
     * @param false|int $start
313
     * @param false|int $end
314
     * @return Doctrine\DBAL\Driver\PDOStatement
0 ignored issues
show
Bug introduced by
The type Xtools\Doctrine\DBAL\Driver\PDOStatement was not found. Did you mean Doctrine\DBAL\Driver\PDOStatement? If so, make sure to prefix the type with \.
Loading history...
315
     */
316
    public function getRevisionsStmt(
317
        User $user = null,
318
        $limit = null,
319
        $numRevisions = null,
320
        $start = false,
321
        $end = false
322
    ) {
323
        // If we have a limit, we need to know the total number of revisions so that PageRepo
324
        // will properly set the OFFSET. See PageRepository::getRevisionsStmt() for more info.
325
        if (isset($limit) && $numRevisions === null) {
326
            $numRevisions = $this->getNumRevisions($user, $start, $end);
327
        }
328
        return $this->getRepository()->getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
329
    }
330
331
    /**
332
     * Get various basic info used in the API, including the
333
     *   number of revisions, unique authors, initial author
334
     *   and edit count of the initial author.
335
     * This is combined into one query for better performance.
336
     * Caching is intentionally disabled, because using the gadget,
337
     *   this will get hit for a different page constantly, where
338
     *   the likelihood of cache benefiting us is slim.
339
     * @return string[]
340
     */
341
    public function getBasicEditingInfo()
342
    {
343
        return $this->getRepository()->getBasicEditingInfo($this);
344
    }
345
346
    /**
347
     * Get assessments of this page
348
     * @see https://www.mediawiki.org/wiki/Extension:PageAssessments
349
     * @return string[]|false `false` if unsupported, or array in the format of:
350
     *         [
351
     *             'assessment' => 'C', // overall assessment
352
     *             'wikiprojects' => [
353
     *                 'Biography' => [
354
     *                     'assessment' => 'C',
355
     *                     'badge' => 'url',
356
     *                 ],
357
     *                 ...
358
     *             ],
359
     *             'wikiproject_prefix' => 'Wikipedia:WikiProject_',
360
     *         ]
361
     */
362 1
    public function getAssessments()
363
    {
364 1
        if (!$this->project->hasPageAssessments() || $this->getNamespace() !== 0) {
365
            return false;
366
        }
367
368 1
        $projectDomain = $this->project->getDomain();
369 1
        $config = $this->project->getRepository()->getAssessmentsConfig($projectDomain);
370 1
        $data = $this->getRepository()->getAssessments($this->project, [$this->getId()]);
371
372
        // Set the default decorations for the overall quality assessment
373
        // This will be replaced with the first valid class defined for any WikiProject
374 1
        $overallQuality = $config['class']['Unknown'];
375 1
        $overallQuality['value'] = '???';
376 1
        $overallQuality['badge'] = $this->project->getAssessmentBadgeURL($overallQuality['badge']);
377
378 1
        $decoratedAssessments = [];
379
380 1
        foreach ($data as $assessment) {
381 1
            $classValue = $assessment['class'];
382
383
            // Use ??? as the presented value when the class is unknown or is not defined in the config
384 1
            if ($classValue === 'Unknown' || $classValue === '' || !isset($config['class'][$classValue])) {
385
                $classAttrs = $config['class']['Unknown'];
386
                $assessment['class']['value'] = '???';
387
                $assessment['class']['category'] = $classAttrs['category'];
388
                $assessment['class']['color'] = $classAttrs['color'];
389
                $assessment['class']['badge'] = "https://upload.wikimedia.org/wikipedia/commons/"
390
                    . $classAttrs['badge'];
391
            } else {
392 1
                $classAttrs = $config['class'][$classValue];
393 1
                $assessment['class'] = [
394 1
                    'value' => $classValue,
395 1
                    'color' => $classAttrs['color'],
396 1
                    'category' => $classAttrs['category'],
397
                ];
398
399
                // add full URL to badge icon
400 1
                if ($classAttrs['badge'] !== '') {
401 1
                    $assessment['class']['badge'] = $this->project->getAssessmentBadgeURL($classValue);
402
                }
403
            }
404
405 1
            if ($overallQuality['value'] === '???') {
406 1
                $overallQuality = $assessment['class'];
407 1
                $overallQuality['category'] = $classAttrs['category'];
408
            }
409
410 1
            $importanceValue = $assessment['importance'];
411 1
            $importanceUnknown = $importanceValue === 'Unknown' || $importanceValue === '';
412
413 1
            if ($importanceUnknown || !isset($config['importance'][$importanceValue])) {
414
                $importanceAttrs = $config['importance']['Unknown'];
415
                $assessment['importance'] = $importanceAttrs;
416
                $assessment['importance']['value'] = '???';
417
                $assessment['importance']['category'] = $importanceAttrs['category'];
418
            } else {
419 1
                $importanceAttrs = $config['importance'][$importanceValue];
420 1
                $assessment['importance'] = [
421 1
                    'value' => $importanceValue,
422 1
                    'color' => $importanceAttrs['color'],
423 1
                    'weight' => $importanceAttrs['weight'], // numerical weight for sorting purposes
424 1
                    'category' => $importanceAttrs['category'],
425
                ];
426
            }
427
428 1
            $decoratedAssessments[$assessment['wikiproject']] = $assessment;
429
        }
430
431
        return [
432 1
            'assessment' => $overallQuality,
433 1
            'wikiprojects' => $decoratedAssessments,
434 1
            'wikiproject_prefix' => $config['wikiproject_prefix']
435
        ];
436
    }
437
438
    /**
439
     * Get CheckWiki errors for this page
440
     * @return string[] See getErrors() for format
441
     */
442 1
    public function getCheckWikiErrors()
443
    {
444 1
        return $this->getRepository()->getCheckWikiErrors($this);
445
    }
446
447
    /**
448
     * Get Wikidata errors for this page
449
     * @return string[] See getErrors() for format
450
     */
451 2
    public function getWikidataErrors()
452
    {
453 2
        $errors = [];
454
455 2
        if (empty($this->getWikidataId())) {
456
            return [];
457
        }
458
459 2
        $wikidataInfo = $this->getRepository()->getWikidataInfo($this);
460
461
        $terms = array_map(function ($entry) {
462 2
            return $entry['term'];
463 2
        }, $wikidataInfo);
464
465 2
        $lang = $this->getLang();
466
467 2
        if (!in_array('label', $terms)) {
468
            $errors[] = [
469
                'prio' => 2,
470
                'name' => 'Wikidata',
471
                'notice' => "Label for language <em>$lang</em> is missing", // FIXME: i18n
472
                'explanation' => "See: <a target='_blank' " .
473
                    "href='//www.wikidata.org/wiki/Help:Label'>Help:Label</a>",
474
            ];
475
        }
476
477 2
        if (!in_array('description', $terms)) {
478 2
            $errors[] = [
479 2
                'prio' => 3,
480 2
                'name' => 'Wikidata',
481 2
                'notice' => "Description for language <em>$lang</em> is missing", // FIXME: i18n
482
                'explanation' => "See: <a target='_blank' " .
483
                    "href='//www.wikidata.org/wiki/Help:Description'>Help:Description</a>",
484
            ];
485
        }
486
487 2
        return $errors;
488
    }
489
490
    /**
491
     * Get Wikidata and CheckWiki errors, if present
492
     * @return string[] List of errors in the format:
493
     *    [[
494
     *         'prio' => int,
495
     *         'name' => string,
496
     *         'notice' => string (HTML),
497
     *         'explanation' => string (HTML)
498
     *     ], ... ]
499
     */
500 1
    public function getErrors()
501
    {
502
        // Includes label and description
503 1
        $wikidataErrors = $this->getWikidataErrors();
504
505 1
        $checkWikiErrors = $this->getCheckWikiErrors();
506
507 1
        return array_merge($wikidataErrors, $checkWikiErrors);
508
    }
509
510
    /**
511
     * Get all wikidata items for the page, not just languages of sister projects
512
     * @return int Number of records.
513
     */
514 1
    public function getWikidataItems()
515
    {
516 1
        if (!is_array($this->wikidataItems)) {
0 ignored issues
show
introduced by
The condition ! is_array($this->wikidataItems) can never be true.
Loading history...
517 1
            $this->wikidataItems = $this->getRepository()->getWikidataItems($this);
518
        }
519 1
        return $this->wikidataItems;
520
    }
521
522
    /**
523
     * Count wikidata items for the page, not just languages of sister projects
524
     * @return int Number of records.
525
     */
526 2
    public function countWikidataItems()
527
    {
528 2
        if (is_array($this->wikidataItems)) {
0 ignored issues
show
introduced by
The condition is_array($this->wikidataItems) can never be false.
Loading history...
529
            $this->numWikidataItems = count($this->wikidataItems);
530 2
        } elseif ($this->numWikidataItems === null) {
531 2
            $this->numWikidataItems = $this->getRepository()->countWikidataItems($this);
532
        }
533 2
        return $this->numWikidataItems;
534
    }
535
536
    /**
537
     * Get number of in and outgoing links and redirects to this page.
538
     * @return string[] Counts with the keys 'links_ext_count', 'links_out_count',
539
     *                  'links_in_count' and 'redirects_count'
540
     */
541 2
    public function countLinksAndRedirects()
542
    {
543 2
        return $this->getRepository()->countLinksAndRedirects($this);
544
    }
545
546
    /**
547
     * Get the sum of pageviews for the given page and timeframe.
548
     * @param string|DateTime $start In the format YYYYMMDD
549
     * @param string|DateTime $end In the format YYYYMMDD
550
     * @return int
551
     */
552 1
    public function getPageviews($start, $end)
553
    {
554
        try {
555 1
            $pageviews = $this->getRepository()->getPageviews($this, $start, $end);
556
        } catch (\GuzzleHttp\Exception\ClientException $e) {
557
            // 404 means zero pageviews
558
            return 0;
559
        }
560
561 1
        return array_sum(array_map(function ($item) {
562 1
            return (int) $item['views'];
563 1
        }, $pageviews['items']));
564
    }
565
566
    /**
567
     * Get the sum of pageviews over the last N days
568
     * @param int $days Default 30
569
     * @return int Number of pageviews
570
     */
571 1
    public function getLastPageviews($days = 30)
572
    {
573 1
        $start = date('Ymd', strtotime("-$days days"));
574 1
        $end = date('Ymd');
575 1
        return $this->getPageviews($start, $end);
576
    }
577
578
    /**
579
     * Is the page the project's Main Page?
580
     * @return bool
581
     */
582 1
    public function isMainPage()
583
    {
584 1
        return $this->getProject()->getMainPage() === $this->getTitle();
585
    }
586
}
587