Passed
Push — bad-gateway-exception ( 993998 )
by MusikAnimal
06:21
created

Page::getCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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

224
            /** @scrutinizer ignore-call */ 
225
            $target = $this->repository->getRevisionIdAtDate($this, $target);
Loading history...
225
        }
226
        return $this->repository->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

226
        return $this->repository->/** @scrutinizer ignore-call */ getHTMLContent($this, $target);
Loading history...
227
    }
228
229
    /**
230
     * Whether or not this page exists.
231
     * @return bool
232
     */
233
    public function exists(): bool
234
    {
235
        $info = $this->getPageInfo();
236
        return null !== $info && !isset($info['missing']) && !isset($info['invalid']) && !isset($info['interwiki']);
237
    }
238
239
    /**
240
     * Get the Project to which this page belongs.
241
     * @return Project
242
     */
243
    public function getProject(): Project
244
    {
245
        return $this->project;
246
    }
247
248
    /**
249
     * Get the language code for this page.
250
     * If not set, the language code for the project is returned.
251
     * @return string
252
     */
253
    public function getLang(): string
254
    {
255
        $info = $this->getPageInfo();
256
        return $info['pagelanguage'] ?? $this->getProject()->getLang();
257
    }
258
259
    /**
260
     * Get the Wikidata ID of this page.
261
     * @return string|null Null if none exists.
262
     */
263
    public function getWikidataId(): ?string
264
    {
265
        $info = $this->getPageInfo();
266
        return $info['pageprops']['wikibase_item'] ?? null;
267
    }
268
269
    /**
270
     * Get the number of revisions the page has.
271
     * @param ?User $user Optionally limit to those of this user.
272
     * @param false|int $start
273
     * @param false|int $end
274
     * @return int
275
     */
276
    public function getNumRevisions(?User $user = null, $start = false, $end = false): int
277
    {
278
        // If a user is given, we will not cache the result via instance variable.
279
        if (null !== $user) {
280
            return $this->repository->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

280
            return $this->repository->/** @scrutinizer ignore-call */ getNumRevisions($this, $user, $start, $end);
Loading history...
281
        }
282
283
        // Return cached value, if present.
284
        if (isset($this->numRevisions)) {
285
            return $this->numRevisions;
286
        }
287
288
        // Otherwise, return the count of all revisions if already present.
289
        if (isset($this->revisions)) {
290
            $this->numRevisions = count($this->revisions);
291
        } else {
292
            // Otherwise do a COUNT in the event fetching all revisions is not desired.
293
            $this->numRevisions = $this->repository->getNumRevisions($this, null, $start, $end);
294
        }
295
296
        return $this->numRevisions;
297
    }
298
299
    /**
300
     * Get all edits made to this page.
301
     * @param User|null $user Specify to get only revisions by the given user.
302
     * @param false|int $start
303
     * @param false|int $end
304
     * @return array
305
     */
306
    public function getRevisions(?User $user = null, $start = false, $end = false): array
307
    {
308
        if (isset($this->revisions)) {
309
            return $this->revisions;
310
        }
311
312
        $this->revisions = $this->repository->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

312
        /** @scrutinizer ignore-call */ 
313
        $this->revisions = $this->repository->getRevisions($this, $user, $start, $end);
Loading history...
313
314
        return $this->revisions;
315
    }
316
317
    /**
318
     * Get the full page wikitext.
319
     * @return string|null Null if nothing was found.
320
     */
321
    public function getWikitext(): ?string
322
    {
323
        $content = $this->repository->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

323
        /** @scrutinizer ignore-call */ 
324
        $content = $this->repository->getPagesWikitext(
Loading history...
324
            $this->getProject(),
325
            [ $this->getTitle() ]
326
        );
327
328
        return $content[$this->getTitle()] ?? null;
329
    }
330
331
    /**
332
     * Get the statement for a single revision, so that you can iterate row by row.
333
     * @see PageRepository::getRevisionsStmt()
334
     * @param User|null $user Specify to get only revisions by the given user.
335
     * @param ?int $limit Max number of revisions to process.
336
     * @param ?int $numRevisions Number of revisions, if known. This is used solely to determine the
337
     *   OFFSET if we are given a $limit. If $limit is set and $numRevisions is not set, a
338
     *   separate query is ran to get the nuber of revisions.
339
     * @param false|int $start
340
     * @param false|int $end
341
     * @return ResultStatement
342
     */
343
    public function getRevisionsStmt(
344
        ?User $user = null,
345
        ?int $limit = null,
346
        ?int $numRevisions = null,
347
        $start = false,
348
        $end = false
349
    ): ResultStatement {
350
        // If we have a limit, we need to know the total number of revisions so that PageRepo
351
        // will properly set the OFFSET. See PageRepository::getRevisionsStmt() for more info.
352
        if (isset($limit) && null === $numRevisions) {
353
            $numRevisions = $this->getNumRevisions($user, $start, $end);
354
        }
355
        return $this->repository->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

355
        return $this->repository->/** @scrutinizer ignore-call */ getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
Loading history...
356
    }
357
358
    /**
359
     * Get the revision ID that immediately precedes the given date.
360
     * @param DateTime $date
361
     * @return int|null Null if none found.
362
     */
363
    public function getRevisionIdAtDate(DateTime $date): ?int
364
    {
365
        return $this->repository->getRevisionIdAtDate($this, $date);
366
    }
367
368
    /**
369
     * Get CheckWiki errors for this page
370
     * @return string[] See getErrors() for format
371
     */
372
    public function getCheckWikiErrors(): array
373
    {
374
        return $this->repository->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

374
        return $this->repository->/** @scrutinizer ignore-call */ getCheckWikiErrors($this);
Loading history...
375
    }
376
377
    /**
378
     * Get Wikidata errors for this page
379
     * @return string[][] See getErrors() for format
380
     */
381
    public function getWikidataErrors(): array
382
    {
383
        $errors = [];
384
385
        if (empty($this->getWikidataId())) {
386
            return [];
387
        }
388
389
        $wikidataInfo = $this->repository->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

389
        /** @scrutinizer ignore-call */ 
390
        $wikidataInfo = $this->repository->getWikidataInfo($this);
Loading history...
390
391
        $terms = array_map(function ($entry) {
392
            return $entry['term'];
393
        }, $wikidataInfo);
394
395
        $lang = $this->getLang();
396
397
        if (!in_array('label', $terms)) {
398
            $errors[] = [
399
                'prio' => 2,
400
                'name' => 'Wikidata',
401
                'notice' => "Label for language <em>$lang</em> is missing", // FIXME: i18n
402
                'explanation' => "See: <a target='_blank' " .
403
                    "href='//www.wikidata.org/wiki/Help:Label'>Help:Label</a>",
404
            ];
405
        }
406
407
        if (!in_array('description', $terms)) {
408
            $errors[] = [
409
                'prio' => 3,
410
                'name' => 'Wikidata',
411
                'notice' => "Description for language <em>$lang</em> is missing", // FIXME: i18n
412
                'explanation' => "See: <a target='_blank' " .
413
                    "href='//www.wikidata.org/wiki/Help:Description'>Help:Description</a>",
414
            ];
415
        }
416
417
        return $errors;
418
    }
419
420
    /**
421
     * Get Wikidata and CheckWiki errors, if present
422
     * @return string[][] List of errors in the format:
423
     *    [[
424
     *         'prio' => int,
425
     *         'name' => string,
426
     *         'notice' => string (HTML),
427
     *         'explanation' => string (HTML)
428
     *     ], ... ]
429
     */
430
    public function getErrors(): array
431
    {
432
        // Includes label and description
433
        $wikidataErrors = $this->getWikidataErrors();
434
435
        $checkWikiErrors = $this->getCheckWikiErrors();
436
437
        return array_merge($wikidataErrors, $checkWikiErrors);
438
    }
439
440
    /**
441
     * Get all wikidata items for the page, not just languages of sister projects
442
     * @return string[]
443
     */
444
    public function getWikidataItems(): array
445
    {
446
        if (!isset($this->wikidataItems)) {
447
            $this->wikidataItems = $this->repository->getWikidataItems($this);
0 ignored issues
show
Bug introduced by
The method getWikidataItems() 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

447
            /** @scrutinizer ignore-call */ 
448
            $this->wikidataItems = $this->repository->getWikidataItems($this);
Loading history...
448
        }
449
        return $this->wikidataItems;
450
    }
451
452
    /**
453
     * Count wikidata items for the page, not just languages of sister projects
454
     * @return int Number of records.
455
     */
456
    public function countWikidataItems(): int
457
    {
458
        if (isset($this->wikidataItems)) {
459
            $this->numWikidataItems = count($this->wikidataItems);
460
        } elseif (!isset($this->numWikidataItems)) {
461
            $this->numWikidataItems = $this->repository->countWikidataItems($this);
0 ignored issues
show
Bug introduced by
The method countWikidataItems() 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

461
            /** @scrutinizer ignore-call */ 
462
            $this->numWikidataItems = $this->repository->countWikidataItems($this);
Loading history...
462
        }
463
        return $this->numWikidataItems;
464
    }
465
466
    /**
467
     * Get number of in and outgoing links and redirects to this page.
468
     * @return string[] Counts with keys 'links_ext_count', 'links_out_count', 'links_in_count' and 'redirects_count'.
469
     */
470
    public function countLinksAndRedirects(): array
471
    {
472
        return $this->repository->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

472
        return $this->repository->/** @scrutinizer ignore-call */ countLinksAndRedirects($this);
Loading history...
473
    }
474
475
    /**
476
     * Get the sum of pageviews for the given page and timeframe.
477
     * @param string|DateTime $start In the format YYYYMMDD
478
     * @param string|DateTime $end In the format YYYYMMDD
479
     * @return int|null Total pageviews or null if data is unavailable.
480
     */
481
    public function getPageviews($start, $end): ?int
482
    {
483
        try {
484
            $pageviews = $this->repository->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

484
            /** @scrutinizer ignore-call */ 
485
            $pageviews = $this->repository->getPageviews($this, $start, $end);
Loading history...
485
        } catch (ClientException $e) {
486
            // 404 means zero pageviews
487
            return 0;
488
        }
489
490
        if (null === $pageviews) {
491
            return null;
492
        }
493
494
        return array_sum(array_map(function ($item) {
495
            return (int)$item['views'];
496
        }, $pageviews['items']));
497
    }
498
499
    /**
500
     * Get the sum of pageviews over the last N days
501
     * @param int $days Default ArticleInfoApi::PAGEVIEWS_OFFSET
502
     * @see ArticleInfoApi::PAGEVIEWS_OFFSET
503
     * @return int|null Number of pageviews or null if data is unavailable.
504
     */
505
    public function getLatestPageviews(int $days = ArticleInfoApi::PAGEVIEWS_OFFSET): ?int
506
    {
507
        $start = date('Ymd', strtotime("-$days days"));
508
        $end = date('Ymd');
509
        return $this->getPageviews($start, $end);
510
    }
511
512
    /**
513
     * Is the page the project's Main Page?
514
     * @return bool
515
     */
516
    public function isMainPage(): bool
517
    {
518
        return $this->getProject()->getMainPage() === $this->getTitle();
519
    }
520
}
521