Test Failed
Push — master ( 78ede5...a61c57 )
by MusikAnimal
07:20
created

Page::newFromRev()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 12
ccs 1
cts 1
cp 1
crap 2
rs 10
c 1
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 42
    public function __construct(Project $project, string $pageName)
41
    {
42 42
        $this->project = $project;
43 42
        $this->unnormalizedPageName = $pageName;
44 42
    }
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
    public static function newFromRev(Project $project, array $rev): self
53
    {
54
        $namespaces = $project->getNamespaces();
55
        $pageTitle = $rev['page_title'];
56
57
        if (0 === (int)$rev['page_namespace']) {
58
            $fullPageTitle = $pageTitle;
59
        } else {
60
            $fullPageTitle = $namespaces[$rev['page_namespace']].":$pageTitle";
61
        }
62 9
63
        return new self($project, $fullPageTitle);
64 9
    }
65 9
66 9
    /**
67
     * Unique identifier for this Page, to be used in cache keys.
68 9
     * 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 3
78
    /**
79 3
     * Get basic information about this page from the repository.
80 1
     * @return array|null
81
     */
82 3
    protected function getPageInfo(): ?array
83 3
    {
84
        if (empty($this->pageInfo)) {
85
            $this->pageInfo = $this->getRepository()
86
                ->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
        return $this->pageInfo;
89
    }
90 1
91
    /**
92 1
     * Get the page's title.
93 1
     * @param bool $useUnnormalized Use the unnormalized page title to avoid an API call. This should be used only if
94 1
     *   you fetched the page title via other means (SQL query), and is not from user input alone.
95 1
     * @return string
96
     */
97
    public function getTitle(bool $useUnnormalized = false): string
98
    {
99
        if ($useUnnormalized) {
100
            return $this->unnormalizedPageName;
101
        }
102 1
        $info = $this->getPageInfo();
103
        return $info['title'] ?? $this->unnormalizedPageName;
104 1
    }
105 1
106
    /**
107
     * Get the page's title without the namespace.
108
     * @return string
109
     */
110
    public function getTitleWithoutNamespace(): string
111
    {
112 1
        $info = $this->getPageInfo();
113
        $title = $info['title'] ?? $this->unnormalizedPageName;
114 1
        $nsName = $this->getNamespaceName();
115 1
        return str_replace($nsName . ':', '', $title);
116
    }
117
118
    /**
119
     * Get this page's database ID.
120
     * @return int|null Null if nonexistent.
121
     */
122
    public function getId(): ?int
123 1
    {
124
        $info = $this->getPageInfo();
125 1
        return isset($info['pageid']) ? (int)$info['pageid'] : null;
126 1
    }
127 1
128
    /**
129 1
     * Get this page's length in bytes.
130
     * @return int|null Null if nonexistent.
131
     */
132
    public function getLength(): ?int
133
    {
134
        $info = $this->getPageInfo();
135
        return isset($info['length']) ? (int)$info['length'] : null;
136 1
    }
137
138 1
    /**
139 1
     * Get HTML for the stylized display of the title.
140
     * The text will be the same as Page::getTitle().
141
     * @return string
142
     */
143
    public function getDisplayTitle(): string
144
    {
145
        $info = $this->getPageInfo();
146 2
        if (isset($info['displaytitle'])) {
147
            return $info['displaytitle'];
148 2
        }
149 2
        return $this->getTitle();
150
    }
151
152
    /**
153
     * Get the full URL of this page.
154
     * @return string|null Null if nonexistent.
155
     */
156 1
    public function getUrl(): ?string
157
    {
158 1
        $info = $this->getPageInfo();
159 1
        return $info['fullurl'] ?? null;
160 1
    }
161 1
162
    /**
163
     * Get the numerical ID of the namespace of this page.
164
     * @return int|null Null if page doesn't exist.
165
     */
166
    public function getNamespace(): ?int
167
    {
168 1
        $info = $this->getPageInfo();
169
        return isset($info['ns']) ? (int)$info['ns'] : null;
170 1
    }
171 1
172
    /**
173
     * Get the name of the namespace of this page.
174
     * @return string|null Null if could not be determined.
175
     */
176
    public function getNamespaceName(): ?string
177
    {
178
        $info = $this->getPageInfo();
179
        return isset($info['ns'])
180
            ? ($this->getProject()->getNamespaces()[$info['ns']] ?? null)
181 1
            : null;
182
    }
183 1
184
    /**
185
     * Get the number of page watchers.
186 1
     * @return int|null Null if unknown.
187
     */
188
    public function getWatchers(): ?int
189
    {
190
        $info = $this->getPageInfo();
191
        return isset($info['watchers']) ? (int)$info['watchers'] : null;
192
    }
193 1
194
    /**
195 1
     * Get the HTML content of the body of the page.
196 1
     * @param DateTime|int $target If a DateTime object, the
197
     *   revision at that time will be returned. If an integer, it is
198
     *   assumed to be the actual revision ID.
199
     * @return string
200
     */
201
    public function getHTMLContent($target = null): string
202
    {
203 16
        if (is_a($target, 'DateTime')) {
204
            $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

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

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

268
            return (int)$this->getRepository()->/** @scrutinizer ignore-call */ getNumRevisions($this, $user, $start, $end);
Loading history...
269
        }
270
271
        // Return cached value, if present.
272
        if (null !== $this->numRevisions) {
273
            return $this->numRevisions;
274
        }
275
276
        // Otherwise, return the count of all revisions if already present.
277
        if (null !== $this->revisions) {
278
            $this->numRevisions = count($this->revisions);
279
        } else {
280
            // Otherwise do a COUNT in the event fetching all revisions is not desired.
281
            $this->numRevisions = (int)$this->getRepository()->getNumRevisions($this, null, $start, $end);
282
        }
283
284
        return $this->numRevisions;
285
    }
286
287
    /**
288
     * Get all edits made to this page.
289 1
     * @param User|null $user Specify to get only revisions by the given user.
290
     * @param false|int $start
291 1
     * @param false|int $end
292 1
     * @return array
293 1
     */
294
    public function getRevisions(?User $user = null, $start = false, $end = false): array
295
    {
296 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...
297
            return $this->revisions;
298
        }
299
300
        $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

300
        $this->revisions = $this->getRepository()->/** @scrutinizer ignore-call */ getRevisions($this, $user, $start, $end);
Loading history...
301
302
        return $this->revisions;
303
    }
304
305
    /**
306
     * Get the full page wikitext.
307
     * @return string|null Null if nothing was found.
308
     */
309
    public function getWikitext(): ?string
310
    {
311
        $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

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

343
        return $this->getRepository()->/** @scrutinizer ignore-call */ getRevisionsStmt($this, $user, $limit, $numRevisions, $start, $end);
Loading history...
344
    }
345
346
    /**
347
     * Get the revision ID that immediately precedes the given date.
348
     * @param DateTime $date
349 2
     * @return int|null Null if none found.
350
     */
351 2
    public function getRevisionIdAtDate(DateTime $date): ?int
352
    {
353 2
        return $this->getRepository()->getRevisionIdAtDate($this, $date);
354
    }
355
356
    /**
357 2
     * Get CheckWiki errors for this page
358
     * @return string[] See getErrors() for format
359 2
     */
360 2
    public function getCheckWikiErrors(): array
361 2
    {
362
        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

362
        return $this->getRepository()->/** @scrutinizer ignore-call */ getCheckWikiErrors($this);
Loading history...
363 2
    }
364
365 2
    /**
366
     * Get Wikidata errors for this page
367
     * @return string[] See getErrors() for format
368
     */
369
    public function getWikidataErrors(): array
370
    {
371
        $errors = [];
372
373
        if (empty($this->getWikidataId())) {
374
            return [];
375 2
        }
376 2
377 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

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

460
        return $this->getRepository()->/** @scrutinizer ignore-call */ countLinksAndRedirects($this);
Loading history...
461
    }
462
463
    /**
464
     * Get the sum of pageviews for the given page and timeframe.
465
     * @param string|DateTime $start In the format YYYYMMDD
466
     * @param string|DateTime $end In the format YYYYMMDD
467
     * @return int
468 1
     */
469
    public function getPageviews($start, $end): int
470 1
    {
471 1
        try {
472 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

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