Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
PageConfigFactory
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 2
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
240
1<?php
2/**
3 * Copyright (C) 2011-2022 Wikimedia Foundation and others.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20namespace MediaWiki\Parser\Parsoid\Config;
21
22use IDBAccessObject;
23use MediaWiki\Languages\LanguageFactory;
24use MediaWiki\Logger\LoggerFactory;
25use MediaWiki\Page\PageIdentity;
26use MediaWiki\Revision\RevisionAccessException;
27use MediaWiki\Revision\RevisionRecord;
28use MediaWiki\Revision\RevisionStore;
29use MediaWiki\Revision\SlotRecord;
30use MediaWiki\Revision\SlotRoleRegistry;
31use MediaWiki\Revision\SuppressedDataException;
32use MediaWiki\Title\Title;
33use MediaWiki\User\UserIdentity;
34use ParserOptions;
35use Wikimedia\Bcp47Code\Bcp47Code;
36
37/**
38 * Helper class used by MediaWiki to create Parsoid PageConfig objects.
39 *
40 * @since 1.39
41 * @internal
42 */
43class PageConfigFactory extends \Wikimedia\Parsoid\Config\PageConfigFactory {
44    private RevisionStore $revisionStore;
45    private SlotRoleRegistry $slotRoleRegistry;
46    private LanguageFactory $languageFactory;
47
48    /**
49     * @param RevisionStore $revisionStore
50     * @param SlotRoleRegistry $slotRoleRegistry
51     * @param LanguageFactory $languageFactory
52     */
53    public function __construct(
54        RevisionStore $revisionStore,
55        SlotRoleRegistry $slotRoleRegistry,
56        LanguageFactory $languageFactory
57    ) {
58        $this->revisionStore = $revisionStore;
59        $this->slotRoleRegistry = $slotRoleRegistry;
60        $this->languageFactory = $languageFactory;
61    }
62
63    /**
64     * Create a new PageConfig.
65     *
66     * Note that Parsoid isn't supposed to use the user context by design; all
67     * user-specific processing is expected to be introduced as a post-parse
68     * transform. The $user parameter is therefore usually null, especially
69     * in background job parsing, although there are corner cases during
70     * extension processing where a non-null $user could affect the output.
71     *
72     * @param PageIdentity $pageId The page represented by the PageConfig.
73     * @param ?UserIdentity $user User who is doing rendering (for parsing options).
74     * @param int|RevisionRecord|null $revision Revision id or a revision record
75     * @param ?string $unused
76     * @param ?Bcp47Code $pageLanguageOverride
77     * @param bool $ensureAccessibleContent If true, ensures that we can get content
78     *   from the newly constructed pageConfig's RevisionRecord and throws a
79     *   RevisionAccessException if not.
80     * @return \Wikimedia\Parsoid\Config\PageConfig
81     * @throws RevisionAccessException
82     */
83    public function create(
84        PageIdentity $pageId,
85        ?UserIdentity $user = null,
86        $revision = null,
87        ?string $unused = null, /* Added to mollify CI with cross-repo uses */
88        ?Bcp47Code $pageLanguageOverride = null,
89        bool $ensureAccessibleContent = false
90    ): \Wikimedia\Parsoid\Config\PageConfig {
91        $title = Title::newFromPageIdentity( $pageId );
92
93        if ( $unused !== null ) {
94            wfDeprecated( __METHOD__ . ' with non-null 4th arg', '1.40' );
95        }
96
97        if ( $revision === null ) {
98            // Fetch the 'latest' revision for the given title.
99            // Note: This initial fetch of the page context revision is
100            // *not* using Parser::fetchCurrentRevisionRecordOfTitle()
101            // (which usually invokes Parser::statelessFetchRevisionRecord
102            // and from there RevisionStore::getKnownCurrentRevision)
103            // because we don't have a Parser object to give to that callback.
104            // We could create one if needed for greater compatibility.
105            $revisionRecord = $this->revisionStore->getKnownCurrentRevision(
106                $title
107            ) ?: null;
108            // Note that $revisionRecord could still be null here if no
109            // page with that $title yet exists.
110        } elseif ( !is_int( $revision ) ) {
111            $revisionRecord = $revision;
112        } else {
113            if ( $revision === 0 ) {
114                // The client may explicitly provide 0 as the revision ID to indicate that
115                // the content doesn't belong to any saved revision, and provide wikitext
116                // in some way. Calling code should handle this case and provide a (fake)
117                // RevisionRecord based on the data in the request. If we get here, the
118                // code processing the request didn't handle this case properly.
119                throw new \UnexpectedValueException(
120                    "Got revision ID 0 indicating unsaved content. " .
121                    "Unsaved content must be provided as a RevisionRecord object."
122                );
123            }
124
125            // Fetch the correct revision record by the supplied id.
126            // This accesses the replica DB and may (or may not) fail over to
127            // the primary DB if the revision isn't found.
128            $revisionRecord = $this->revisionStore->getRevisionById( $revision );
129            if ( $revisionRecord === null ) {
130                // This revision really ought to exist.  Check the primary DB.
131                // This *could* cause two requests to the primary DB if there
132                // were pending writes, but this codepath should be very rare.
133                // [T259855]
134                $revisionRecord = $this->revisionStore->getRevisionById(
135                    $revision, IDBAccessObject::READ_LATEST
136                );
137                $success = ( $revisionRecord !== null ) ? 'success' : 'failure';
138                LoggerFactory::getInstance( 'Parsoid' )->error(
139                    "Retried revision fetch after failure: {$success}", [
140                        'id' => $revision,
141                        'title' => $title->getPrefixedText(),
142                    ]
143                );
144            }
145            if ( $revisionRecord === null ) {
146                throw new RevisionAccessException( "Can't find revision {$revision}" );
147            }
148        }
149
150        // If we have a revision record, check that we are allowed to see it.
151        // Mirrors the check from RevisionRecord::getContent
152        if (
153            $revisionRecord !== null &&
154            !$revisionRecord->audienceCan(
155                RevisionRecord::DELETED_TEXT, RevisionRecord::FOR_PUBLIC
156            )
157        ) {
158            throw new SuppressedDataException( 'Not an available content version.' );
159        }
160
161        $parserOptions =
162            $user
163            ? ParserOptions::newFromUser( $user )
164            : ParserOptions::newFromAnon();
165
166        // Turn off some options since Parsoid/JS currently doesn't
167        // do anything with this. As we proceed with closer integration,
168        // we can figure out if there is any value to these limit reports.
169        $parserOptions->setOption( 'enableLimitReport', false );
170
171        $slotRoleHandler = $this->slotRoleRegistry->getRoleHandler( SlotRecord::MAIN );
172        if ( $pageLanguageOverride ) {
173            $pageLanguage = $this->languageFactory->getLanguage( $pageLanguageOverride );
174        } else {
175            $pageLanguage = $title->getPageLanguage();
176        }
177
178        $pageConfig = new PageConfig(
179            $parserOptions,
180            $slotRoleHandler,
181            $title,
182            $revisionRecord,
183            $pageLanguage,
184            $pageLanguage->getDir()
185        );
186
187        if ( $ensureAccessibleContent ) {
188            if ( $revisionRecord === null ) {
189                // T234549
190                throw new RevisionAccessException( 'The specified revision does not exist.' );
191            }
192            // Try to get the content so that we can fail early.  Otherwise,
193            // a RevisionAccessException is thrown.  It's expensive, but the
194            // result will be cached for later calls.
195            $pageConfig->getRevisionContent()->getContent( SlotRecord::MAIN );
196        }
197
198        return $pageConfig;
199    }
200
201}