Page::getProject()   A
last analyzed

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\Exception\BadGatewayException;
8
use App\Repository\PageRepository;
9
use DateTime;
10
use Doctrine\DBAL\Driver\ResultStatement;
11
use GuzzleHttp\Exception\ClientException;
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 string $unnormalizedPageName;
20
21
    /** @var string[]|null Metadata about this page. */
22
    protected ?array $pageInfo;
23
24
    /** @var string[] Revision history of this page. */
25
    protected array $revisions;
26
27
    /** @var int Number of revisions for this page. */
28
    protected int $numRevisions;
29
30
    /** @var string[] List of Wikidata sitelinks for this page. */
31
    protected array $wikidataItems;
32
33
    /** @var int Number of Wikidata sitelinks for this page. */
34
    protected int $numWikidataItems;
35
36
    /** @var int Length of the page in bytes. */
37
    protected int $length;
38
39
    /**
40
     * Page constructor.
41
     * @param PageRepository $repository
42
     * @param Project $project
43
     * @param string $pageName
44
     */
45
    public function __construct(PageRepository $repository, Project $project, string $pageName)
46
    {
47
        $this->repository = $repository;
48
        $this->project = $project;
49
        $this->unnormalizedPageName = $pageName;
50
    }
51
52
    /**
53
     * Get a Page instance given a database row (either from or JOINed on the page table).
54
     * @param PageRepository $repository
55
     * @param Project $project
56
     * @param array $row Must contain 'page_title' and 'namespace'. May contain 'length'.
57
     * @return static
58
     */
59
    public static function newFromRow(PageRepository $repository, Project $project, array $row): self
60
    {
61
        $pageTitle = $row['page_title'];
62
63
        if (0 === (int)$row['namespace']) {
64
            $fullPageTitle = $pageTitle;
65
        } else {
66
            $namespaces = $project->getNamespaces();
67
            $fullPageTitle = $namespaces[$row['namespace']].":$pageTitle";
68
        }
69
70
        $page = new self($repository, $project, $fullPageTitle);
71
        $page->pageInfo['ns'] = $row['namespace'];
72
        if (isset($row['length'])) {
73
            $page->length = (int)$row['length'];
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 (!isset($this->pageInfo)) {
98
            $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

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

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

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

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

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

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

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

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

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

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

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

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

485
            /** @scrutinizer ignore-call */ 
486
            $pageviews = $this->repository->getPageviews($this, $start, $end);
Loading history...
486
        } catch (ClientException $e) {
487
            // 404 means zero pageviews
488
            return 0;
489
        } catch (BadGatewayException $e) {
490
            // Upstream error, so return null so the view can customize messaging.
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 PageInfoApi::PAGEVIEWS_OFFSET
502
     * @return int|null Number of pageviews or null if data is unavailable.
503
     *@see PageInfoApi::PAGEVIEWS_OFFSET
504
     */
505
    public function getLatestPageviews(int $days = PageInfoApi::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