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