Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.72% covered (warning)
83.72%
72 / 86
75.00% covered (warning)
75.00%
12 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageConfig
83.72% covered (warning)
83.72%
72 / 86
75.00% covered (warning)
75.00%
12 / 16
33.88
0.00% covered (danger)
0.00%
0 / 1
 __construct
68.18% covered (warning)
68.18%
15 / 22
0.00% covered (danger)
0.00%
0 / 1
8.58
 mockPageContent
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
2.08
 loadData
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
6.03
 getContentModel
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getLinkTarget
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getPageId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getPageLanguageBcp47
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getPageLanguageDir
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getParentRevisionId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionTimestamp
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionUser
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionUserId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionSha1
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionContent
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare( strict_types = 1 );
4
5namespace Wikimedia\Parsoid\Config\Api;
6
7use Wikimedia\Assert\Assert;
8use Wikimedia\Bcp47Code\Bcp47Code;
9use Wikimedia\Parsoid\Config\PageConfig as IPageConfig;
10use Wikimedia\Parsoid\Config\PageContent;
11use Wikimedia\Parsoid\Config\SiteConfig as ISiteConfig;
12use Wikimedia\Parsoid\Mocks\MockPageContent;
13use Wikimedia\Parsoid\Utils\Title;
14use Wikimedia\Parsoid\Utils\Utils;
15
16/**
17 * PageConfig via MediaWiki's Action API
18 *
19 * Note this is intended for testing, not performance.
20 */
21class PageConfig extends IPageConfig {
22
23    /** @var ?ApiHelper */
24    private $api;
25
26    private ISiteConfig $siteConfig;
27
28    /** @var Title */
29    private $title;
30
31    /** @var string|null */
32    private $revid;
33
34    /** @var array<string,mixed>|null */
35    private $page;
36
37    /** @var array<string,mixed>|null */
38    private $rev;
39
40    /** @var PageContent|null */
41    private $content;
42
43    /** @var ?Bcp47Code */
44    private $pagelanguage;
45
46    /** @var string|null */
47    private $pagelanguageDir;
48
49    /**
50     * @param ?ApiHelper $api (only needed if $opts doesn't provide page info)
51     * @param ISiteConfig $siteConfig
52     * @param array $opts
53     */
54    public function __construct( ?ApiHelper $api, ISiteConfig $siteConfig, array $opts ) {
55        parent::__construct();
56        $this->api = $api;
57        $this->siteConfig = $siteConfig;
58
59        if ( !isset( $opts['title'] ) ) {
60            throw new \InvalidArgumentException( '$opts[\'title\'] must be set' );
61        }
62        if ( !( $opts['title'] instanceof Title ) ) {
63            throw new \InvalidArgumentException( '$opts[\'title\'] must be a Title' );
64        }
65        $this->title = $opts['title'];
66        $this->revid = $opts['revid'] ?? null;
67        # pageLanguage can/should be passed as a Bcp47Code object
68        $this->pagelanguage = !empty( $opts['pageLanguage'] ) ?
69            Utils::mwCodeToBcp47( $opts['pageLanguage'] ) : null;
70        $this->pagelanguageDir = $opts['pageLanguageDir'] ?? null;
71
72        // This option is primarily used to mock the page content.
73        if ( isset( $opts['pageContent'] ) && empty( $opts['loadData'] ) ) {
74            $this->mockPageContent( $opts );
75        } else {
76            Assert::invariant( $api !== null, 'Cannot load page info without an API' );
77            # Lazily load later
78            $this->page = null;
79            $this->rev = null;
80
81            if ( isset( $opts['pageContent'] ) ) {
82                $this->loadData();
83                $this->rev = [
84                    'slots' => [ 'main' => $opts['pageContent'] ],
85                ];
86            }
87        }
88    }
89
90    private function mockPageContent( array $opts ): void {
91        $this->page = [
92            'title' => $this->title->getPrefixedText(),
93            'ns' => $this->title->getNamespace(),
94            'pageid' => -1,
95            'pagelanguage' => $opts['pageLanguage'] ?? 'en',
96            'pagelanguagedir' => $opts['pageLanguageDir'] ?? 'ltr',
97        ];
98        if ( isset( $opts['pageContent'] ) ) {
99            $this->rev = [
100                'slots' => [ 'main' => $opts['pageContent'] ],
101            ];
102        }
103    }
104
105    private function loadData() {
106        if ( $this->page !== null ) {
107            return;
108        }
109
110        $params = [
111            'action' => 'query',
112            'prop' => 'info|revisions',
113            'rvprop' => 'ids|timestamp|user|userid|sha1|size|content',
114            'rvslots' => '*',
115        ];
116
117        if ( !empty( $this->revid ) ) {
118            $params['revids'] = $this->revid;
119        } else {
120            $params['titles'] = $this->title->getPrefixedDBKey();
121            $params['rvlimit'] = 1;
122        }
123
124        $content = $this->api->makeRequest( $params );
125        if ( !isset( $content['query']['pages'][0] ) ) {
126            throw new \RuntimeException( 'Request for page failed' );
127        }
128        $this->page = $content['query']['pages'][0];
129
130        $this->rev = $this->page['revisions'][0] ?? [];
131        unset( $this->page['revisions'] );
132
133        if ( isset( $this->rev['timestamp'] ) ) {
134            $this->rev['timestamp'] = preg_replace( '/\D/', '', $this->rev['timestamp'] );
135        }
136
137        // Well, we tried but the page probably doesn't exist
138        if ( !$this->rev ) {
139            $this->mockPageContent( [] );  // FIXME: T234549
140        }
141    }
142
143    /** @inheritDoc */
144    public function getContentModel(): string {
145        $this->loadData();
146        return $this->rev['slots']['main']['contentmodel'] ?? 'wikitext';
147    }
148
149    /** @inheritDoc */
150    public function getLinkTarget(): Title {
151        $this->loadData();
152        return Title::newFromText(
153            $this->page['title'], $this->siteConfig, $this->page['ns']
154        );
155    }
156
157    /** @inheritDoc */
158    public function getPageId(): int {
159        $this->loadData();
160        return $this->page['pageid'] ?? 0;
161    }
162
163    /** @inheritDoc */
164    public function getPageLanguageBcp47(): Bcp47Code {
165        $this->loadData();
166        # Note that 'en' is a last-resort fail-safe fallback; it shouldn't
167        # ever be reached in practice.
168        return $this->pagelanguage ??
169            # T320662: core should provide an API to get the BCP-47 form directly
170            Utils::mwCodeToBcp47( $this->page['pagelanguage'] ?? 'en' );
171    }
172
173    /** @inheritDoc */
174    public function getPageLanguageDir(): string {
175        $this->loadData();
176        return $this->pagelanguageDir ?? $this->page['pagelanguagedir'] ?? 'ltr';
177    }
178
179    /** @inheritDoc */
180    public function getRevisionId(): ?int {
181        $this->loadData();
182        return $this->rev['revid'] ?? null;
183    }
184
185    /** @inheritDoc */
186    public function getParentRevisionId(): ?int {
187        $this->loadData();
188        return $this->rev['parentid'] ?? null;
189    }
190
191    /** @inheritDoc */
192    public function getRevisionTimestamp(): ?string {
193        $this->loadData();
194        return $this->rev['timestamp'] ?? null;
195    }
196
197    /** @inheritDoc */
198    public function getRevisionUser(): ?string {
199        $this->loadData();
200        return $this->rev['user'] ?? null;
201    }
202
203    /** @inheritDoc */
204    public function getRevisionUserId(): ?int {
205        $this->loadData();
206        return $this->rev['userid'] ?? null;
207    }
208
209    /** @inheritDoc */
210    public function getRevisionSha1(): ?string {
211        $this->loadData();
212        return $this->rev['sha1'] ?? null;
213    }
214
215    /** @inheritDoc */
216    public function getRevisionSize(): ?int {
217        $this->loadData();
218        return $this->rev['size'] ?? null;
219    }
220
221    /** @inheritDoc */
222    public function getRevisionContent(): ?PageContent {
223        $this->loadData();
224        if ( $this->rev && !$this->content ) {
225            $this->content = new MockPageContent( $this->rev['slots'] );
226        }
227        return $this->content;
228    }
229}