Passed
Push — master ( ed4035...f2dd38 )
by MusikAnimal
10:32
created

Page::getCacheKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
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 AppBundle\Model;
9
10
use DateTime;
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 $unnormalizedPageName;
19
20
    /** @var string[] Metadata about this page. */
21
    protected $pageInfo;
22
23
    /** @var string[] Revision history of this page. */
24
    protected $revisions;
25
26
    /** @var int Number of revisions for this page. */
27
    protected $numRevisions;
28
29
    /** @var string[] List of Wikidata sitelinks for this page. */
30
    protected $wikidataItems;
31
32
    /** @var int Number of Wikidata sitelinks for this page. */
33
    protected $numWikidataItems;
34
35
    /**
36
     * Page constructor.
37
     * @param Project $project
38
     * @param string $pageName
39
     */
40 39
    public function __construct(Project $project, string $pageName)
41
    {
42 39
        $this->project = $project;
43 39
        $this->unnormalizedPageName = $pageName;
44 39
    }
45
46
    /**
47
     * Get a Page instance given a revision row (JOINed on the page table).
48
     * @param Project $project
49
     * @param array $rev Must contain 'page_title' and 'page_namespace'.
50
     * @return static
51
     */
52 3
    public static function newFromRev(Project $project, array $rev): self
53
    {
54 3
        $namespaces = $project->getNamespaces();
55 3
        $pageTitle = $rev['page_title'];
56
57 3
        if (0 === (int)$rev['page_namespace']) {
58 2
            $fullPageTitle = $pageTitle;
59
        } else {
60 2
            $fullPageTitle = $namespaces[$rev['page_namespace']].":$pageTitle";
61
        }
62
63 3
        return new self($project, $fullPageTitle);
64
    }
65
66
    /**
67
     * Unique identifier for this Page, to be used in cache keys.
68
     * Use of md5 ensures the cache key does not contain reserved characters.
69
     * @see Repository::getCacheKey()
70
     * @return string
71
     * @codeCoverageIgnore
72
     */
73
    public function getCacheKey(): string
74
    {
75
        return md5((string)$this->getId());
76
    }
77
78
    /**
79
     * Get basic information about this page from the repository.
80
     * @return array|null
81
     */
82 9
    protected function getPageInfo(): ?array
83
    {
84 9
        if (empty($this->pageInfo)) {
85 9
            $this->pageInfo = $this->getRepository()
86 9
                ->getPageInfo($this->project, $this->unnormalizedPageName);
0 ignored issues
show
Bug introduced by
The method getPageInfo() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

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

206
            $target = $this->getRepository()->/** @scrutinizer ignore-call */ getRevisionIdAtDate($this, $target);
Loading history...
207
        }
208 1
        return $this->getRepository()->getHTMLContent($this, $target);
0 ignored issues
show
Bug introduced by
The method getHTMLContent() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

208
        return $this->getRepository()->/** @scrutinizer ignore-call */ getHTMLContent($this, $target);
Loading history...
209
    }
210
211
    /**
212
     * Whether or not this page exists.
213
     * @return bool
214
     */
215 1
    public function exists(): bool
216
    {
217 1
        $info = $this->getPageInfo();
218 1
        return null !== $info && !isset($info['missing']) && !isset($info['invalid']) && !isset($info['interwiki']);
219
    }
220
221
    /**
222
     * Get the Project to which this page belongs.
223
     * @return Project
224
     */
225 16
    public function getProject(): Project
226
    {
227 16
        return $this->project;
228
    }
229
230
    /**
231
     * Get the language code for this page.
232
     * If not set, the language code for the project is returned.
233
     * @return string
234
     */
235 2
    public function getLang(): string
236
    {
237 2
        $info = $this->getPageInfo();
238 2
        if (isset($info['pagelanguage'])) {
239 2
            return $info['pagelanguage'];
240
        } else {
241
            return $this->getProject()->getLang();
242
        }
243
    }
244
245
    /**
246
     * Get the Wikidata ID of this page.
247
     * @return string|null Null if none exists.
248
     */
249 4
    public function getWikidataId(): ?string
250
    {
251 4
        $info = $this->getPageInfo();
252 4
        if (isset($info['pageprops']['wikibase_item'])) {
253 3
            return $info['pageprops']['wikibase_item'];
254
        } else {
255 1
            return null;
256
        }
257
    }
258
259
    /**
260
     * Get the number of revisions the page has.
261
     * @param User $user Optionally limit to those of this user.
262
     * @param false|int $start
263
     * @param false|int $end
264
     * @return int
265
     */
266 4
    public function getNumRevisions(?User $user = null, $start = false, $end = false): int
267
    {
268
        // If a user is given, we will not cache the result via instance variable.
269 4
        if (null !== $user) {
270
            return (int)$this->getRepository()->getNumRevisions($this, $user, $start, $end);
0 ignored issues
show
Bug introduced by
The method getNumRevisions() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

270
            return (int)$this->getRepository()->/** @scrutinizer ignore-call */ getNumRevisions($this, $user, $start, $end);
Loading history...
271
        }
272
273
        // Return cached value, if present.
274 4
        if (null !== $this->numRevisions) {
275
            return $this->numRevisions;
276
        }
277
278
        // Otherwise, return the count of all revisions if already present.
279 4
        if (null !== $this->revisions) {
280
            $this->numRevisions = count($this->revisions);
281
        } else {
282
            // Otherwise do a COUNT in the event fetching all revisions is not desired.
283 4
            $this->numRevisions = (int)$this->getRepository()->getNumRevisions($this, null, $start, $end);
284
        }
285
286 4
        return $this->numRevisions;
287
    }
288
289
    /**
290
     * Get all edits made to this page.
291
     * @param User|null $user Specify to get only revisions by the given user.
292
     * @param false|int $start
293
     * @param false|int $end
294
     * @return array
295
     */
296
    public function getRevisions(?User $user = null, $start = false, $end = false): array
297
    {
298
        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...
299
            return $this->revisions;
300
        }
301
302
        $this->revisions = $this->getRepository()->getRevisions($this, $user, $start, $end);
0 ignored issues
show
Bug introduced by
The method getRevisions() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\Repository\GlobalContribsRepository or AppBundle\Repository\PageRepository or AppBundle\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

302
        $this->revisions = $this->getRepository()->/** @scrutinizer ignore-call */ getRevisions($this, $user, $start, $end);
Loading history...
303
304
        return $this->revisions;
305
    }
306
307
    /**
308
     * Get the full page wikitext.
309
     * @return string|null Null if nothing was found.
310
     */
311 1
    public function getWikitext(): ?string
312
    {
313 1
        $content = $this->getRepository()->getPagesWikitext(
0 ignored issues
show
Bug introduced by
The method getPagesWikitext() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

313
        $content = $this->getRepository()->/** @scrutinizer ignore-call */ getPagesWikitext(
Loading history...
314 1
            $this->getProject(),
315 1
            [ $this->getTitle() ]
316
        );
317
318 1
        return $content[$this->getTitle()] ?? null;
319
    }
320
321
    /**
322
     * Get the statement for a single revision, so that you can iterate row by row.
323
     * @see PageRepository::getRevisionsStmt()
324
     * @param User|null $user Specify to get only revisions by the given user.
325
     * @param int $limit Max number of revisions to process.
326
     * @param int $numRevisions Number of revisions, if known. This is used solely to determine the
327
     *   OFFSET if we are given a $limit. If $limit is set and $numRevisions is not set, a
328
     *   separate query is ran to get the nuber of revisions.
329
     * @param false|int $start
330
     * @param false|int $end
331
     * @return \Doctrine\DBAL\Driver\PDOStatement
332
     */
333
    public function getRevisionsStmt(
334
        ?User $user = null,
335
        ?int $limit = null,
336
        ?int $numRevisions = null,
337
        $start = false,
338
        $end = false
339
    ): \Doctrine\DBAL\Driver\PDOStatement {
340
        // If we have a limit, we need to know the total number of revisions so that PageRepo
341
        // will properly set the OFFSET. See PageRepository::getRevisionsStmt() for more info.
342
        if (isset($limit) && null === $numRevisions) {
343
            $numRevisions = $this->getNumRevisions($user, $start, $end);
344
        }
345
        return $this->getRepository()->getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
0 ignored issues
show
Bug introduced by
The method getRevisionsStmt() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

345
        return $this->getRepository()->/** @scrutinizer ignore-call */ getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
Loading history...
346
    }
347
348
    /**
349
     * Get the revision ID that immediately precedes the given date.
350
     * @param DateTime $date
351
     * @return int|null Null if none found.
352
     */
353
    public function getRevisionIdAtDate(DateTime $date): ?int
354
    {
355
        return $this->getRepository()->getRevisionIdAtDate($this, $date);
356
    }
357
358
    /**
359
     * Get CheckWiki errors for this page
360
     * @return string[] See getErrors() for format
361
     */
362 1
    public function getCheckWikiErrors(): array
363
    {
364 1
        return $this->getRepository()->getCheckWikiErrors($this);
0 ignored issues
show
Bug introduced by
The method getCheckWikiErrors() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

364
        return $this->getRepository()->/** @scrutinizer ignore-call */ getCheckWikiErrors($this);
Loading history...
365
    }
366
367
    /**
368
     * Get Wikidata errors for this page
369
     * @return string[] See getErrors() for format
370
     */
371 2
    public function getWikidataErrors(): array
372
    {
373 2
        $errors = [];
374
375 2
        if (empty($this->getWikidataId())) {
376
            return [];
377
        }
378
379 2
        $wikidataInfo = $this->getRepository()->getWikidataInfo($this);
0 ignored issues
show
Bug introduced by
The method getWikidataInfo() does not exist on AppBundle\Repository\Repository. It seems like you code against a sub-type of AppBundle\Repository\Repository such as AppBundle\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

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

474
            $pageviews = $this->getRepository()->/** @scrutinizer ignore-call */ getPageviews($this, $start, $end);
Loading history...
475
        } catch (\GuzzleHttp\Exception\ClientException $e) {
476
            // 404 means zero pageviews
477
            return 0;
478
        }
479
480
        return array_sum(array_map(function ($item) {
481 1
            return (int)$item['views'];
482 1
        }, $pageviews['items']));
483
    }
484
485
    /**
486
     * Get the sum of pageviews over the last N days
487
     * @param int $days Default 30
488
     * @return int Number of pageviews
489
     */
490 1
    public function getLastPageviews(int $days = 30): int
491
    {
492 1
        $start = date('Ymd', strtotime("-$days days"));
493 1
        $end = date('Ymd');
494 1
        return $this->getPageviews($start, $end);
495
    }
496
497
    /**
498
     * Is the page the project's Main Page?
499
     * @return bool
500
     */
501 1
    public function isMainPage(): bool
502
    {
503 1
        return $this->getProject()->getMainPage() === $this->getTitle();
504
    }
505
}
506