Passed
Push — main ( d5c675...5a2fdf )
by MusikAnimal
06:34 queued 02:23
created

Page::newFromRow()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 4
nop 2
dl 0
loc 17
ccs 3
cts 3
cp 1
crap 3
rs 9.9332
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
    /** @var string Title of the page. */
36
    protected $pageTitle;
37
38
    /** @var int Length of the page in bytes. */
39
    protected $length;
40 39
41
    /**
42 39
     * Page constructor.
43 39
     * @param Project $project
44 39
     * @param string $pageName
45
     */
46
    public function __construct(Project $project, string $pageName)
47
    {
48
        $this->project = $project;
49
        $this->unnormalizedPageName = $pageName;
50
    }
51
52 3
    /**
53
     * Get a Page instance given a database row (either from or JOINed on the page table).
54 3
     * @param Project $project
55 3
     * @param array $row Must contain 'page_title' and 'page_namespace'. May contain 'page_len'.
56
     * @return static
57 3
     */
58 2
    public static function newFromRow(Project $project, array $row): self
59
    {
60 2
        $pageTitle = $row['page_title'];
61
62
        if (0 === (int)$row['page_namespace']) {
63 3
            $fullPageTitle = $pageTitle;
64
        } else {
65
            $namespaces = $project->getNamespaces();
66
            $fullPageTitle = $namespaces[$row['page_namespace']].":$pageTitle";
67
        }
68
69
        $page = new self($project, $fullPageTitle);
70
        if (isset($row['page_len'])) {
71
            $page->length = (int)$row['page_len'];
72
        }
73
74
        return $page;
75
    }
76
77
    /**
78
     * Unique identifier for this Page, to be used in cache keys.
79
     * Use of md5 ensures the cache key does not contain reserved characters.
80
     * @see Repository::getCacheKey()
81
     * @return string
82 9
     * @codeCoverageIgnore
83
     */
84 9
    public function getCacheKey(): string
85 9
    {
86 9
        return md5((string)$this->getId());
87
    }
88 9
89
    /**
90
     * Get basic information about this page from the repository.
91
     * @return array|null
92
     */
93
    protected function getPageInfo(): ?array
94
    {
95
        if (empty($this->pageInfo)) {
96
            $this->pageInfo = $this->getRepository()
97 3
                ->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

97
                ->/** @scrutinizer ignore-call */ getPageInfo($this->project, $this->unnormalizedPageName);
Loading history...
98
        }
99 3
        return $this->pageInfo;
100 1
    }
101
102 3
    /**
103 3
     * 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 1
        if ($useUnnormalized) {
111
            return $this->unnormalizedPageName;
112 1
        }
113 1
        $info = $this->getPageInfo();
114 1
        return $info['title'] ?? $this->unnormalizedPageName;
115 1
    }
116 1
117 1
    /**
118
     * Get the page's title without the namespace.
119
     * @return string
120
     */
121
    public function getTitleWithoutNamespace(): string
122
    {
123
        $info = $this->getPageInfo();
124 1
        $title = $info['title'] ?? $this->unnormalizedPageName;
125
        $nsName = $this->getNamespaceName();
126 1
        return $nsName
127 1
            ? str_replace($nsName . ':', '', $title)
128
            : $title;
129
    }
130
131
    /**
132
     * Get this page's database ID.
133
     * @return int|null Null if nonexistent.
134 1
     */
135
    public function getId(): ?int
136 1
    {
137 1
        $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 1
    public function getLength(): ?int
146
    {
147 1
        if (isset($this->length)) {
148 1
            return $this->length;
149 1
        }
150
        $info = $this->getPageInfo();
151 1
        $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 1
     * @return string
159
     */
160 1
    public function getDisplayTitle(): string
161 1
    {
162
        $info = $this->getPageInfo();
163
        if (isset($info['displaytitle'])) {
164
            return $info['displaytitle'];
165
        }
166
        return $this->getTitle();
167
    }
168 2
169
    /**
170 2
     * Get the full URL of this page.
171 2
     * @return string|null Null if nonexistent.
172
     */
173
    public function getUrl(): ?string
174
    {
175
        $info = $this->getPageInfo();
176
        return $info['fullurl'] ?? null;
177
    }
178 1
179
    /**
180 1
     * Get the numerical ID of the namespace of this page.
181 1
     * @return int|null Null if page doesn't exist.
182 1
     */
183 1
    public function getNamespace(): ?int
184
    {
185
        $info = $this->getPageInfo();
186
        return isset($info['ns']) ? (int)$info['ns'] : null;
187
    }
188
189
    /**
190 1
     * Get the name of the namespace of this page.
191
     * @return string|null Null if could not be determined.
192 1
     */
193 1
    public function getNamespaceName(): ?string
194
    {
195
        $info = $this->getPageInfo();
196
        return isset($info['ns'])
197
            ? ($this->getProject()->getNamespaces()[$info['ns']] ?? null)
198
            : null;
199
    }
200
201
    /**
202
     * Get the number of page watchers.
203 1
     * @return int|null Null if unknown.
204
     */
205 1
    public function getWatchers(): ?int
206
    {
207
        $info = $this->getPageInfo();
208 1
        return isset($info['watchers']) ? (int)$info['watchers'] : null;
209
    }
210
211
    /**
212
     * Get the HTML content of the body of the page.
213
     * @param DateTime|int $target If a DateTime object, the
214
     *   revision at that time will be returned. If an integer, it is
215 1
     *   assumed to be the actual revision ID.
216
     * @return string
217 1
     */
218 1
    public function getHTMLContent($target = null): string
219
    {
220
        if (is_a($target, 'DateTime')) {
221
            $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

221
            $target = $this->getRepository()->/** @scrutinizer ignore-call */ getRevisionIdAtDate($this, $target);
Loading history...
222
        }
223
        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

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

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

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

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

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

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

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

477
        return $this->getRepository()->/** @scrutinizer ignore-call */ countLinksAndRedirects($this);
Loading history...
478
    }
479
480
    /**
481 1
     * Get the sum of pageviews for the given page and timeframe.
482 1
     * @param string|DateTime $start In the format YYYYMMDD
483
     * @param string|DateTime $end In the format YYYYMMDD
484
     * @return int
485
     */
486
    public function getPageviews($start, $end): int
487
    {
488
        try {
489
            $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

489
            $pageviews = $this->getRepository()->/** @scrutinizer ignore-call */ getPageviews($this, $start, $end);
Loading history...
490 1
        } catch (\GuzzleHttp\Exception\ClientException $e) {
491
            // 404 means zero pageviews
492 1
            return 0;
493 1
        }
494 1
495
        return array_sum(array_map(function ($item) {
496
            return (int)$item['views'];
497
        }, $pageviews['items']));
498
    }
499
500
    /**
501 1
     * Get the sum of pageviews over the last N days
502
     * @param int $days Default 30
503 1
     * @return int Number of pageviews
504
     */
505
    public function getLastPageviews(int $days = 30): 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