Test Setup Failed
Pull Request — main (#426)
by MusikAnimal
17:10 queued 11:44
created

Page::getDisplayTitle()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * This file contains only the Page class.
4
 */
5
6
declare(strict_types = 1);
7
8
namespace App\Model;
9
10
use DateTime;
11
use Doctrine\DBAL\Driver\ResultStatement;
12
13
/**
14
 * A Page is a single wiki page in one project.
15
 */
16
class Page extends Model
17
{
18
    /** @var string The page name as provided at instantiation. */
19
    protected $unnormalizedPageName;
20
21
    /** @var string[] Metadata about this page. */
22
    protected $pageInfo = [];
23
24
    /** @var string[] Revision history of this page. */
25
    protected $revisions;
26
27
    /** @var int Number of revisions for this page. */
28
    protected $numRevisions;
29
30
    /** @var string[] List of Wikidata sitelinks for this page. */
31
    protected $wikidataItems;
32
33
    /** @var int Number of Wikidata sitelinks for this page. */
34
    protected $numWikidataItems;
35
36
    /** @var string Title of the page. */
37
    protected $pageTitle;
38
39
    /** @var int Length of the page in bytes. */
40
    protected $length;
41
42
    /**
43
     * Page constructor.
44
     * @param Project $project
45
     * @param string $pageName
46
     */
47
    public function __construct(Project $project, string $pageName)
48
    {
49
        $this->project = $project;
50
        $this->unnormalizedPageName = $pageName;
51
    }
52
53
    /**
54
     * Get a Page instance given a database row (either from or JOINed on the page table).
55
     * @param Project $project
56
     * @param array $row Must contain 'page_title' and 'page_namespace'. May contain 'page_len'.
57
     * @return static
58
     */
59
    public static function newFromRow(Project $project, array $row): self
60
    {
61
        $pageTitle = $row['page_title'];
62
63
        if (0 === (int)$row['page_namespace']) {
64
            $fullPageTitle = $pageTitle;
65
        } else {
66
            $namespaces = $project->getNamespaces();
67
            $fullPageTitle = $namespaces[$row['page_namespace']].":$pageTitle";
68
        }
69
70
        $page = new self($project, $fullPageTitle);
71
        $page->pageInfo['ns'] = $row['page_namespace'];
72
        if (isset($row['page_len'])) {
73
            $page->length = (int)$row['page_len'];
74
        }
75
76
        return $page;
77
    }
78
79
    /**
80
     * Unique identifier for this Page, to be used in cache keys.
81
     * Use of md5 ensures the cache key does not contain reserved characters.
82
     * @see Repository::getCacheKey()
83
     * @return string
84
     * @codeCoverageIgnore
85
     */
86
    public function getCacheKey(): string
87
    {
88
        return md5((string)$this->getId());
89
    }
90
91
    /**
92
     * Get basic information about this page from the repository.
93
     * @return array|null
94
     */
95
    protected function getPageInfo(): ?array
96
    {
97
        if (empty($this->pageInfo)) {
98
            $this->pageInfo = $this->getRepository()
99
                ->getPageInfo($this->project, $this->unnormalizedPageName);
0 ignored issues
show
Bug introduced by
The method getPageInfo() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

99
                ->/** @scrutinizer ignore-call */ getPageInfo($this->project, $this->unnormalizedPageName);
Loading history...
100
        }
101
        return $this->pageInfo;
102
    }
103
104
    /**
105
     * Get the page's title.
106
     * @param bool $useUnnormalized Use the unnormalized page title to avoid an API call. This should be used only if
107
     *   you fetched the page title via other means (SQL query), and is not from user input alone.
108
     * @return string
109
     */
110
    public function getTitle(bool $useUnnormalized = false): string
111
    {
112
        if ($useUnnormalized) {
113
            return $this->unnormalizedPageName;
114
        }
115
        $info = $this->getPageInfo();
116
        return $info['title'] ?? $this->unnormalizedPageName;
117
    }
118
119
    /**
120
     * Get the page's title without the namespace.
121
     * @return string
122
     */
123
    public function getTitleWithoutNamespace(): string
124
    {
125
        $info = $this->getPageInfo();
126
        $title = $info['title'] ?? $this->unnormalizedPageName;
127
        $nsName = $this->getNamespaceName();
128
        return $nsName
129
            ? str_replace($nsName . ':', '', $title)
130
            : $title;
131
    }
132
133
    /**
134
     * Get this page's database ID.
135
     * @return int|null Null if nonexistent.
136
     */
137
    public function getId(): ?int
138
    {
139
        $info = $this->getPageInfo();
140
        return isset($info['pageid']) ? (int)$info['pageid'] : null;
141
    }
142
143
    /**
144
     * Get this page's length in bytes.
145
     * @return int|null Null if nonexistent.
146
     */
147
    public function getLength(): ?int
148
    {
149
        if (isset($this->length)) {
150
            return $this->length;
151
        }
152
        $info = $this->getPageInfo();
153
        $this->length = isset($info['length']) ? (int)$info['length'] : null;
154
        return $this->length;
155
    }
156
157
    /**
158
     * Get HTML for the stylized display of the title.
159
     * The text will be the same as Page::getTitle().
160
     * @return string
161
     */
162
    public function getDisplayTitle(): string
163
    {
164
        $info = $this->getPageInfo();
165
        if (isset($info['displaytitle'])) {
166
            return $info['displaytitle'];
167
        }
168
        return $this->getTitle();
169
    }
170
171
    /**
172
     * Get the full URL of this page.
173
     * @return string|null Null if nonexistent.
174
     */
175
    public function getUrl(): ?string
176
    {
177
        $info = $this->getPageInfo();
178
        return $info['fullurl'] ?? null;
179
    }
180
181
    /**
182
     * Get the numerical ID of the namespace of this page.
183
     * @return int|null Null if page doesn't exist.
184
     */
185
    public function getNamespace(): ?int
186
    {
187
        if (isset($this->pageInfo['ns']) && is_numeric($this->pageInfo['ns'])) {
188
            return (int)$this->pageInfo['ns'];
189
        }
190
        $info = $this->getPageInfo();
191
        return isset($info['ns']) ? (int)$info['ns'] : null;
192
    }
193
194
    /**
195
     * Get the name of the namespace of this page.
196
     * @return string|null Null if could not be determined.
197
     */
198
    public function getNamespaceName(): ?string
199
    {
200
        $info = $this->getPageInfo();
201
        return isset($info['ns'])
202
            ? ($this->getProject()->getNamespaces()[$info['ns']] ?? null)
203
            : null;
204
    }
205
206
    /**
207
     * Get the number of page watchers.
208
     * @return int|null Null if unknown.
209
     */
210
    public function getWatchers(): ?int
211
    {
212
        $info = $this->getPageInfo();
213
        return isset($info['watchers']) ? (int)$info['watchers'] : null;
214
    }
215
216
    /**
217
     * Get the HTML content of the body of the page.
218
     * @param DateTime|int $target If a DateTime object, the
219
     *   revision at that time will be returned. If an integer, it is
220
     *   assumed to be the actual revision ID.
221
     * @return string
222
     */
223
    public function getHTMLContent($target = null): string
224
    {
225
        if (is_a($target, 'DateTime')) {
226
            $target = $this->getRepository()->getRevisionIdAtDate($this, $target);
0 ignored issues
show
Bug introduced by
The method getRevisionIdAtDate() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

226
            $target = $this->getRepository()->/** @scrutinizer ignore-call */ getRevisionIdAtDate($this, $target);
Loading history...
227
        }
228
        return $this->getRepository()->getHTMLContent($this, $target);
0 ignored issues
show
Bug introduced by
The method getHTMLContent() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

228
        return $this->getRepository()->/** @scrutinizer ignore-call */ getHTMLContent($this, $target);
Loading history...
229
    }
230
231
    /**
232
     * Whether or not this page exists.
233
     * @return bool
234
     */
235
    public function exists(): bool
236
    {
237
        $info = $this->getPageInfo();
238
        return null !== $info && !isset($info['missing']) && !isset($info['invalid']) && !isset($info['interwiki']);
239
    }
240
241
    /**
242
     * Get the Project to which this page belongs.
243
     * @return Project
244
     */
245
    public function getProject(): Project
246
    {
247
        return $this->project;
248
    }
249
250
    /**
251
     * Get the language code for this page.
252
     * If not set, the language code for the project is returned.
253
     * @return string
254
     */
255
    public function getLang(): string
256
    {
257
        $info = $this->getPageInfo();
258
        if (isset($info['pagelanguage'])) {
259
            return $info['pagelanguage'];
260
        } else {
261
            return $this->getProject()->getLang();
262
        }
263
    }
264
265
    /**
266
     * Get the Wikidata ID of this page.
267
     * @return string|null Null if none exists.
268
     */
269
    public function getWikidataId(): ?string
270
    {
271
        $info = $this->getPageInfo();
272
        if (isset($info['pageprops']['wikibase_item'])) {
273
            return $info['pageprops']['wikibase_item'];
274
        } else {
275
            return null;
276
        }
277
    }
278
279
    /**
280
     * Get the number of revisions the page has.
281
     * @param ?User $user Optionally limit to those of this user.
282
     * @param false|int $start
283
     * @param false|int $end
284
     * @return int
285
     */
286
    public function getNumRevisions(?User $user = null, $start = false, $end = false): int
287
    {
288
        // If a user is given, we will not cache the result via instance variable.
289
        if (null !== $user) {
290
            return (int)$this->getRepository()->getNumRevisions($this, $user, $start, $end);
0 ignored issues
show
Bug introduced by
The method getNumRevisions() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

290
            return (int)$this->getRepository()->/** @scrutinizer ignore-call */ getNumRevisions($this, $user, $start, $end);
Loading history...
291
        }
292
293
        // Return cached value, if present.
294
        if (null !== $this->numRevisions) {
295
            return $this->numRevisions;
296
        }
297
298
        // Otherwise, return the count of all revisions if already present.
299
        if (null !== $this->revisions) {
300
            $this->numRevisions = count($this->revisions);
301
        } else {
302
            // Otherwise do a COUNT in the event fetching all revisions is not desired.
303
            $this->numRevisions = (int)$this->getRepository()->getNumRevisions($this, null, $start, $end);
304
        }
305
306
        return $this->numRevisions;
307
    }
308
309
    /**
310
     * Get all edits made to this page.
311
     * @param User|null $user Specify to get only revisions by the given user.
312
     * @param false|int $start
313
     * @param false|int $end
314
     * @return array
315
     */
316
    public function getRevisions(?User $user = null, $start = false, $end = false): array
317
    {
318
        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...
319
            return $this->revisions;
320
        }
321
322
        $this->revisions = $this->getRepository()->getRevisions($this, $user, $start, $end);
0 ignored issues
show
Bug introduced by
The method getRevisions() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\GlobalContribsRepository or App\Repository\PageRepository or App\Repository\EditSummaryRepository. ( Ignorable by Annotation )

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

322
        $this->revisions = $this->getRepository()->/** @scrutinizer ignore-call */ getRevisions($this, $user, $start, $end);
Loading history...
323
324
        return $this->revisions;
325
    }
326
327
    /**
328
     * Get the full page wikitext.
329
     * @return string|null Null if nothing was found.
330
     */
331
    public function getWikitext(): ?string
332
    {
333
        $content = $this->getRepository()->getPagesWikitext(
0 ignored issues
show
Bug introduced by
The method getPagesWikitext() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

333
        $content = $this->getRepository()->/** @scrutinizer ignore-call */ getPagesWikitext(
Loading history...
334
            $this->getProject(),
335
            [ $this->getTitle() ]
336
        );
337
338
        return $content[$this->getTitle()] ?? null;
339
    }
340
341
    /**
342
     * Get the statement for a single revision, so that you can iterate row by row.
343
     * @see PageRepository::getRevisionsStmt()
344
     * @param User|null $user Specify to get only revisions by the given user.
345
     * @param ?int $limit Max number of revisions to process.
346
     * @param ?int $numRevisions Number of revisions, if known. This is used solely to determine the
347
     *   OFFSET if we are given a $limit. If $limit is set and $numRevisions is not set, a
348
     *   separate query is ran to get the nuber of revisions.
349
     * @param false|int $start
350
     * @param false|int $end
351
     * @return ResultStatement
352
     */
353
    public function getRevisionsStmt(
354
        ?User $user = null,
355
        ?int $limit = null,
356
        ?int $numRevisions = null,
357
        $start = false,
358
        $end = false
359
    ): ResultStatement {
360
        // If we have a limit, we need to know the total number of revisions so that PageRepo
361
        // will properly set the OFFSET. See PageRepository::getRevisionsStmt() for more info.
362
        if (isset($limit) && null === $numRevisions) {
363
            $numRevisions = $this->getNumRevisions($user, $start, $end);
364
        }
365
        return $this->getRepository()->getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
0 ignored issues
show
Bug introduced by
The method getRevisionsStmt() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

365
        return $this->getRepository()->/** @scrutinizer ignore-call */ getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
Loading history...
366
    }
367
368
    /**
369
     * Get the revision ID that immediately precedes the given date.
370
     * @param DateTime $date
371
     * @return int|null Null if none found.
372
     */
373
    public function getRevisionIdAtDate(DateTime $date): ?int
374
    {
375
        return $this->getRepository()->getRevisionIdAtDate($this, $date);
376
    }
377
378
    /**
379
     * Get CheckWiki errors for this page
380
     * @return string[] See getErrors() for format
381
     */
382
    public function getCheckWikiErrors(): array
383
    {
384
        return $this->getRepository()->getCheckWikiErrors($this);
0 ignored issues
show
Bug introduced by
The method getCheckWikiErrors() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

384
        return $this->getRepository()->/** @scrutinizer ignore-call */ getCheckWikiErrors($this);
Loading history...
385
    }
386
387
    /**
388
     * Get Wikidata errors for this page
389
     * @return string[] See getErrors() for format
390
     */
391
    public function getWikidataErrors(): array
392
    {
393
        $errors = [];
394
395
        if (empty($this->getWikidataId())) {
396
            return [];
397
        }
398
399
        $wikidataInfo = $this->getRepository()->getWikidataInfo($this);
0 ignored issues
show
Bug introduced by
The method getWikidataInfo() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

399
        $wikidataInfo = $this->getRepository()->/** @scrutinizer ignore-call */ getWikidataInfo($this);
Loading history...
400
401
        $terms = array_map(function ($entry) {
402
            return $entry['term'];
403
        }, $wikidataInfo);
404
405
        $lang = $this->getLang();
406
407
        if (!in_array('label', $terms)) {
408
            $errors[] = [
409
                'prio' => 2,
410
                'name' => 'Wikidata',
411
                'notice' => "Label for language <em>$lang</em> is missing", // FIXME: i18n
412
                'explanation' => "See: <a target='_blank' " .
413
                    "href='//www.wikidata.org/wiki/Help:Label'>Help:Label</a>",
414
            ];
415
        }
416
417
        if (!in_array('description', $terms)) {
418
            $errors[] = [
419
                'prio' => 3,
420
                'name' => 'Wikidata',
421
                'notice' => "Description for language <em>$lang</em> is missing", // FIXME: i18n
422
                'explanation' => "See: <a target='_blank' " .
423
                    "href='//www.wikidata.org/wiki/Help:Description'>Help:Description</a>",
424
            ];
425
        }
426
427
        return $errors;
428
    }
429
430
    /**
431
     * Get Wikidata and CheckWiki errors, if present
432
     * @return string[] List of errors in the format:
433
     *    [[
434
     *         'prio' => int,
435
     *         'name' => string,
436
     *         'notice' => string (HTML),
437
     *         'explanation' => string (HTML)
438
     *     ], ... ]
439
     */
440
    public function getErrors(): array
441
    {
442
        // Includes label and description
443
        $wikidataErrors = $this->getWikidataErrors();
444
445
        $checkWikiErrors = $this->getCheckWikiErrors();
446
447
        return array_merge($wikidataErrors, $checkWikiErrors);
448
    }
449
450
    /**
451
     * Get all wikidata items for the page, not just languages of sister projects
452
     * @return string[]
453
     */
454
    public function getWikidataItems(): array
455
    {
456
        if (!is_array($this->wikidataItems)) {
0 ignored issues
show
introduced by
The condition is_array($this->wikidataItems) is always true.
Loading history...
457
            $this->wikidataItems = $this->getRepository()->getWikidataItems($this);
458
        }
459
        return $this->wikidataItems;
460
    }
461
462
    /**
463
     * Count wikidata items for the page, not just languages of sister projects
464
     * @return int Number of records.
465
     */
466
    public function countWikidataItems(): int
467
    {
468
        if (is_array($this->wikidataItems)) {
0 ignored issues
show
introduced by
The condition is_array($this->wikidataItems) is always true.
Loading history...
469
            $this->numWikidataItems = count($this->wikidataItems);
470
        } elseif (null === $this->numWikidataItems) {
471
            $this->numWikidataItems = (int)$this->getRepository()->countWikidataItems($this);
472
        }
473
        return $this->numWikidataItems;
474
    }
475
476
    /**
477
     * Get number of in and outgoing links and redirects to this page.
478
     * @return string[] Counts with keys 'links_ext_count', 'links_out_count', 'links_in_count' and 'redirects_count'.
479
     */
480
    public function countLinksAndRedirects(): array
481
    {
482
        return $this->getRepository()->countLinksAndRedirects($this);
0 ignored issues
show
Bug introduced by
The method countLinksAndRedirects() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

482
        return $this->getRepository()->/** @scrutinizer ignore-call */ countLinksAndRedirects($this);
Loading history...
483
    }
484
485
    /**
486
     * Get the sum of pageviews for the given page and timeframe.
487
     * @param string|DateTime $start In the format YYYYMMDD
488
     * @param string|DateTime $end In the format YYYYMMDD
489
     * @return int
490
     */
491
    public function getPageviews($start, $end): int
492
    {
493
        try {
494
            $pageviews = $this->getRepository()->getPageviews($this, $start, $end);
0 ignored issues
show
Bug introduced by
The method getPageviews() does not exist on App\Repository\Repository. It seems like you code against a sub-type of App\Repository\Repository such as App\Repository\PageRepository. ( Ignorable by Annotation )

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

494
            $pageviews = $this->getRepository()->/** @scrutinizer ignore-call */ getPageviews($this, $start, $end);
Loading history...
495
        } catch (\GuzzleHttp\Exception\ClientException $e) {
496
            // 404 means zero pageviews
497
            return 0;
498
        }
499
500
        return array_sum(array_map(function ($item) {
501
            return (int)$item['views'];
502
        }, $pageviews['items']));
503
    }
504
505
    /**
506
     * Get the sum of pageviews over the last N days
507
     * @param int $days Default 30
508
     * @return int Number of pageviews
509
     */
510
    public function getLastPageviews(int $days = 30): int
511
    {
512
        $start = date('Ymd', strtotime("-$days days"));
513
        $end = date('Ymd');
514
        return $this->getPageviews($start, $end);
515
    }
516
517
    /**
518
     * Is the page the project's Main Page?
519
     * @return bool
520
     */
521
    public function isMainPage(): bool
522
    {
523
        return $this->getProject()->getMainPage() === $this->getTitle();
524
    }
525
}
526