Page MenuHomeWickedGov Phorge

DataAccess.php
No OneTemporary

Size
17 KB
Referenced Files
None
Subscribers
None

DataAccess.php

<?php
declare( strict_types = 1 );
/**
* Copyright (C) 2011-2022 Wikimedia Foundation and others.
*
* @license GPL-2.0-or-later
*/
namespace MediaWiki\Parser\Parsoid\Config;
use MediaTransformError;
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\Category\TrackingCategories;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Content\Transform\ContentTransformer;
use MediaWiki\FileRepo\File\File;
use MediaWiki\FileRepo\RepoGroup;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Html\Html;
use MediaWiki\Language\LanguageCode;
use MediaWiki\Linker\Linker;
use MediaWiki\MainConfigNames;
use MediaWiki\Page\File\BadFileLookup;
use MediaWiki\Parser\Parser;
use MediaWiki\Parser\ParserFactory;
use MediaWiki\Parser\ParserOptions;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Parser\PPFrame;
use MediaWiki\Title\Title;
use Wikimedia\Assert\UnreachableException;
use Wikimedia\Parsoid\Config\DataAccess as IDataAccess;
use Wikimedia\Parsoid\Config\PageConfig as IPageConfig;
use Wikimedia\Parsoid\Config\PageContent as IPageContent;
use Wikimedia\Parsoid\Core\ContentMetadataCollector;
use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
use Wikimedia\Parsoid\Fragments\HtmlPFragment;
use Wikimedia\Parsoid\Fragments\PFragment;
use Wikimedia\Rdbms\ReadOnlyMode;
/**
* Implement Parsoid's abstract class for data access.
*
* @since 1.39
* @internal
*/
class DataAccess extends IDataAccess {
public const CONSTRUCTOR_OPTIONS = [
MainConfigNames::SVGMaxSize,
];
private readonly HookRunner $hookRunner;
/** Lazy-created via self::prepareParser() */
private ?Parser $parser = null;
private PPFrame $ppFrame;
private ?PageConfig $previousPageConfig = null;
private int $markerIndex = 0;
/**
* @param ServiceOptions $config MediaWiki main configuration object
* @param RepoGroup $repoGroup
* @param BadFileLookup $badFileLookup
* @param HookContainer $hookContainer
* @param ContentTransformer $contentTransformer
* @param TrackingCategories $trackingCategories
* @param ReadOnlyMode $readOnlyMode used to disable linting when the
* database is read-only.
* @param ParserFactory $parserFactory A legacy parser factory,
* for PST/preprocessing/extension handling
* @param LinkBatchFactory $linkBatchFactory
*/
public function __construct(
private readonly ServiceOptions $config,
private readonly RepoGroup $repoGroup,
private readonly BadFileLookup $badFileLookup,
private readonly HookContainer $hookContainer,
private readonly ContentTransformer $contentTransformer,
private readonly TrackingCategories $trackingCategories,
private readonly ReadOnlyMode $readOnlyMode,
private readonly ParserFactory $parserFactory,
private readonly LinkBatchFactory $linkBatchFactory,
) {
$config->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->hookRunner = new HookRunner( $hookContainer );
$this->previousPageConfig = null; // ensure we initialize parser options
}
/**
* @param IPageConfig $pageConfig
* @param File $file
* @param array $hp
* @return array
*/
private function makeTransformOptions( IPageConfig $pageConfig, $file, array $hp ): array {
// Validate the input parameters like Parser::makeImage()
$handler = $file->getHandler();
if ( !$handler ) {
return []; // will get iconThumb()
}
foreach ( $hp as $name => $value ) {
if ( !$handler->validateParam( $name, $value ) ) {
unset( $hp[$name] );
}
}
// This part is similar to Linker::makeImageLink(). If there is no width,
// set one based on the source file size.
$page = $hp['page'] ?? 0;
if ( !isset( $hp['width'] ) ) {
if ( isset( $hp['height'] ) && $file->isVectorized() ) {
// If it's a vector image, and user only specifies height
// we don't want it to be limited by its "normal" width.
$hp['width'] = $this->config->get( MainConfigNames::SVGMaxSize );
} else {
$hp['width'] = $file->getWidth( $page );
}
// We don't need to fill in a default thumbnail width here, since
// that is done by Parsoid. Parsoid always sets the width parameter
// for thumbnails.
}
// Parser::makeImage() always sets this
$hp['targetlang'] = LanguageCode::bcp47ToInternal(
$pageConfig->getPageLanguageBcp47()
);
return $hp;
}
/** @inheritDoc */
public function getPageInfo( $pageConfigOrTitle, array $titles ): array {
if ( $pageConfigOrTitle instanceof IPageConfig ) {
$context_title = Title::newFromLinkTarget(
$pageConfigOrTitle->getLinkTarget()
);
} elseif ( is_string( $pageConfigOrTitle ) ) {
// Temporary, deprecated.
$context_title = Title::newFromTextThrow( $pageConfigOrTitle );
} elseif ( $pageConfigOrTitle instanceof ParsoidLinkTarget ) {
$context_title = Title::newFromLinkTarget( $pageConfigOrTitle );
} else {
throw new UnreachableException( "Bad type for argument 1" );
}
$titleObjs = [];
$pagemap = [];
$classes = [];
$ret = [];
foreach ( $titles as $name ) {
$t = Title::newFromText( $name );
// Filter out invalid titles. Title::newFromText in core (not our bespoke
// version in src/Utils/Title.php) can return null for invalid titles.
if ( !$t ) {
// FIXME: This is a bandaid to patch up the fact that Env::makeTitle treats
// this as a valid title, but Title::newFromText treats it as invalid.
// T237535
// This matches what ApiQuery::outputGeneralPageInfo() would
// return for an invalid title.
$ret[$name] = [
'pageId' => -1,
'revId' => -1,
'invalid' => true,
'invalidreason' => 'The requested page title is invalid',
];
} else {
$titleObjs[$name] = $t;
}
}
$this->linkBatchFactory->newLinkBatch( $titleObjs )
->setCaller( __METHOD__ )
->execute();
foreach ( $titleObjs as $obj ) {
$pdbk = $obj->getPrefixedDBkey();
$pagemap[$obj->getArticleID()] = $pdbk;
$classes[$pdbk] = $obj->isRedirect() ? 'mw-redirect' : '';
}
$this->hookRunner->onGetLinkColours(
# $classes is passed by reference and mutated
$pagemap, $classes, $context_title
);
foreach ( $titleObjs as $name => $obj ) {
/** @var Title $obj */
$pdbk = $obj->getPrefixedDBkey();
$c = preg_split(
'/\s+/', $classes[$pdbk] ?? '', -1, PREG_SPLIT_NO_EMPTY
);
$ret[$name] = [
'pageId' => $obj->getArticleID(),
'revId' => $obj->getLatestRevID(),
'missing' => !$obj->exists(),
'known' => $obj->isKnown(),
'redirect' => $obj->isRedirect(),
'linkclasses' => $c, # See ApiQueryInfo::getLinkClasses() in core
];
}
return $ret;
}
/** @inheritDoc */
public function getFileInfo( IPageConfig $pageConfig, array $files ): array {
$page = Title::newFromLinkTarget( $pageConfig->getLinkTarget() );
$keys = [];
foreach ( $files as $f ) {
$keys[] = $f[0];
}
$fileObjs = $this->repoGroup->findFiles( $keys );
$ret = [];
foreach ( $files as $f ) {
$filename = $f[0];
$dims = $f[1];
/** @var File $file */
$file = $fileObjs[$filename] ?? null;
if ( !$file ) {
$ret[] = null;
continue;
}
// See Linker::makeImageLink; 'page' is a key in $handlerParams
// core uses 'false' as the default then casts to (int) => 0
$pageNum = $dims['page'] ?? 0;
$result = [
'width' => $file->getWidth( $pageNum ),
'height' => $file->getHeight( $pageNum ),
'size' => $file->getSize(),
'mediatype' => $file->getMediaType(),
'mime' => $file->getMimeType(),
'url' => $file->getFullUrl(),
'mustRender' => $file->mustRender(),
'isVectorized' => $file->isVectorized(),
'badFile' => $this->badFileLookup->isBadFile( $filename, $page ),
'timestamp' => $file->getTimestamp(),
'sha1' => $file->getSha1(),
];
$length = $file->getLength();
if ( $length ) {
$result['duration'] = (float)$length;
}
if ( isset( $dims['seek'] ) ) {
$dims['thumbtime'] = $dims['seek'];
}
$txopts = $this->makeTransformOptions( $pageConfig, $file, $dims );
$mto = $file->transform( $txopts );
if ( $mto ) {
if ( $mto->isError() && $mto instanceof MediaTransformError ) {
$result['thumberror'] = $mto->toText();
} else {
if ( $txopts ) {
// Do srcset scaling
Linker::processResponsiveImages( $file, $mto, $txopts );
if ( count( $mto->responsiveUrls ) ) {
$result['responsiveUrls'] = [];
foreach ( $mto->responsiveUrls as $density => $url ) {
$result['responsiveUrls'][$density] = $url;
}
}
}
// Proposed MediaTransformOutput serialization method for T51896 etc.
// Note that getAPIData(['fullurl']) would return
// UrlUtils::expand(), which wouldn't respect the wiki's
// protocol preferences -- instead it would use the
// protocol used for the API request.
if ( is_callable( [ $mto, 'getAPIData' ] ) ) {
$result['thumbdata'] = $mto->getAPIData( [ 'withhash' ] );
}
$result['thumburl'] = $mto->getUrl();
$result['thumbwidth'] = $mto->getWidth();
$result['thumbheight'] = $mto->getHeight();
}
} else {
$result['thumberror'] = "Presumably, invalid parameters, despite validation.";
}
$ret[] = $result;
}
return $ret;
}
/**
* Prepare MediaWiki's parser for preprocessing or extension tag parsing,
* clearing its state if necessary.
*
* @note The caller is expected to call Parser::resetOutput() and
* reset the watcher if needed on $pageConfig->getParserOptions()
* as needed.
*
* @param IPageConfig $pageConfig
* @param int $outputType
* @return Parser
*/
private function prepareParser( IPageConfig $pageConfig, int $outputType ) {
'@phan-var PageConfig $pageConfig'; // @var PageConfig $pageConfig
// Clear the state only when the PageConfig changes, so that Parser's internal caches can
// be retained. This should also provide better compatibility with extension tags.
$clearState = $this->previousPageConfig !== $pageConfig;
$this->previousPageConfig = $pageConfig;
$parserOptions = $pageConfig->getParserOptions();
$oldWatcher = $parserOptions->registerWatcher( null );
// Use the same legacy parser object for all calls to extension tag
// processing, for greater compatibility.
$this->parser ??= $this->parserFactory->create();
$this->parser->setStripExtTags( false );
$this->parser->startExternalParse(
Title::newFromLinkTarget( $pageConfig->getLinkTarget() ),
$parserOptions,
$outputType, $clearState, $pageConfig->getRevisionId() );
// Retain a PPFrame object between preprocess requests since it contains
// some useful caches.
if ( $clearState ) {
$this->ppFrame = $this->parser->getPreprocessor()->newFrame();
// If $clearState is true, then we've reset the parser output and
// clobbered the watcher on the parser options; restore the old
// one.
$parserOptions->registerWatcher( $oldWatcher );
}
return $this->parser;
}
/** @internal */
public function makeLimitReport(
IPageConfig $pageConfig,
ParserOptions $parserOptions,
ParserOutput $parserOutput
) {
$parser = $this->prepareParser( $pageConfig, Parser::OT_HTML );
// This next call doesn't touch $parser::$mParserOutput so we
// don't need to call Parser::resetOutput() here.
$parser->makeLimitReport( $parserOptions, $parserOutput );
}
/** @inheritDoc */
public function parseWikitext(
IPageConfig $pageConfig,
ContentMetadataCollector $metadata,
string $wikitext
): string {
'@phan-var PageConfig $pageConfig'; // @var PageConfig $pageConfig
$parser = $this->prepareParser( $pageConfig, Parser::OT_HTML );
// XXX: Ideally we will eventually have the legacy parser use our
// ContentMetadataCollector instead of having a new ParserOutput
// created (in Parser::resetOutput() here) which we then have to
// manually merge. On the other hand, this will let us precisely
// identify metadata added by $wikitext.
$parserOptions = $pageConfig->getParserOptions();
$oldWatcher = $parserOptions->registerWatcher( null );
$parser->resetOutput();
$html = $parser->parseExtensionTagAsTopLevelDoc( $wikitext );
$out = $parser->getOutput();
$out->collectMetadata( $metadata ); # merges $out into $metadata
$parserOptions->registerWatcher( $oldWatcher );
return Parser::extractBody( $html );
}
/** @inheritDoc */
public function preprocessWikitext(
IPageConfig $pageConfig,
ContentMetadataCollector $metadata,
$wikitext
) {
'@phan-var PageConfig $pageConfig'; // @var PageConfig $pageConfig
$parser = $this->prepareParser( $pageConfig, Parser::OT_PREPROCESS );
// XXX: Ideally we will eventually have the legacy parser use our
// ContentMetadataCollector instead of having a new ParserOutput
// created (in Parser::resetOutput() here) which we then have to
// manually merge. On the other hand, this will let us precisely
// identify metadata added by $wikitext.
$parserOptions = $pageConfig->getParserOptions();
$oldWatcher = $parserOptions->registerWatcher( null );
$parser->resetOutput();
if ( $wikitext instanceof PFragment ) {
$result = [];
$index = 1;
foreach ( $wikitext->split() as $fragment ) {
if ( is_string( $fragment ) ) {
$result[] = $fragment;
} else {
$marker = Parser::MARKER_PREFIX . '-parsoid-' .
sprintf( '%08X', $this->markerIndex++ ) .
Parser::MARKER_SUFFIX;
$parser->getStripState()->addParsoidOpaque(
$marker, $fragment
);
$result[] = $marker;
}
}
$wikitext = implode( $result );
}
$this->hookRunner->onParserBeforePreprocess(
# $wikitext is passed by reference and mutated
$parser, $wikitext, $parser->getStripState()
);
// New PFragment-based support (T374616)
$wikitext = $parser->replaceVariables(
$wikitext, $this->ppFrame, false, [
'parsoidTopLevelCall' => true,
// This is implied by stripExtTags=false and
// probably doesn't need to be explicitly passed
// any more.
'processNowiki' => true,
]
);
// Where the result has strip state markers, tunnel this content
// through Parsoid as a PFragment type.
$pieces = $parser->getStripState()->split( $wikitext );
if ( count( $pieces ) > 1 || ( $pieces[0]['type'] ?? null ) !== 'string' ) {
for ( $i = 0; $i < count( $pieces ); $i++ ) {
[ 'type' => $type, 'content' => $content ] = $pieces[$i];
if ( $type === 'string' ) {
// wikitext (could include extension tag snippets like <tag..>...</tag>)
$pieces[$i] = $content;
} elseif ( $type === 'parsoid' ) {
// @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
$pieces[$i] = $pieces[$i]['extra']; // replace w/ fragment
} elseif ( $type === 'nowiki' ) {
$extra = $pieces[$i]['extra'] ?? null;
// T388819: If this is from an actual <nowiki>, we
// wrap <span typeof="mw:Nowiki"> around $contents.
if ( $extra === 'nowiki' ) {
$content = Html::rawElement( 'span', [
'typeof' => 'mw:Nowiki',
], $content );
}
$pieces[$i] = $content ? HtmlPFragment::newFromHtmlString( $content, null ) : '';
} else {
// T381709: technically this fragment should
// be subject to language conversion and some
// additional processing
$pieces[$i] = $content ? HtmlPFragment::newFromHtmlString( $content, null ) : '';
}
}
// Concatenate wikitext strings generated by extension tags,
// so that PFragment doesn't try to add <nowiki>s between
// the pieces to prevent token-gluing.
$result = [];
$wt = '';
foreach ( $pieces as $p ) {
if ( is_string( $p ) ) {
$wt .= $p;
} else {
$result[] = $wt;
$result[] = $p;
$wt = '';
}
}
$result[] = $wt;
// result will be a PFragment, no longer a string.
$wikitext = PFragment::fromSplitWt( $result );
}
$out = $parser->getOutput();
$out->collectMetadata( $metadata ); # merges $out into $metadata
$parserOptions->registerWatcher( $oldWatcher );
return $wikitext;
}
/** @inheritDoc */
public function fetchTemplateSource(
IPageConfig $pageConfig, $title
): ?IPageContent {
'@phan-var PageConfig $pageConfig'; // @var PageConfig $pageConfig
if ( is_string( $title ) ) {
$titleObj = Title::newFromTextThrow( $title );
} else {
$titleObj = Title::newFromLinkTarget( $title );
}
// Use the PageConfig to take advantage of custom template
// fetch hooks like FlaggedRevisions, etc.
$revRecord = $pageConfig->fetchRevisionRecordOfTemplate( $titleObj );
return $revRecord ? new PageContent( $revRecord ) : null;
}
/** @inheritDoc */
public function fetchTemplateData( IPageConfig $pageConfig, $title ): ?array {
$ret = [];
if ( !is_string( $title ) ) {
$titleObj = Title::newFromLinkTarget( $title );
$title = $titleObj->getPrefixedText();
}
// @todo: This hook needs some clean up: T304899
$this->hookRunner->onParserFetchTemplateData(
[ $title ],
$ret # value returned by reference
);
// Cast value to array since the hook returns this as a stdclass
$tplData = $ret[$title] ?? null;
if ( $tplData ) {
// Deep convert to associative array
$tplData = json_decode( json_encode( $tplData ), true );
}
return $tplData;
}
/**
* Add a tracking category with the given key to the metadata for the page.
* @param IPageConfig $pageConfig the page on which the tracking category
* is to be added
* @param ContentMetadataCollector $metadata The metadata for the page
* @param string $key Message key (not localized)
*/
public function addTrackingCategory(
IPageConfig $pageConfig,
ContentMetadataCollector $metadata,
string $key
): void {
$page = Title::newFromLinkTarget( $pageConfig->getLinkTarget() );
$this->trackingCategories->addTrackingCategory(
$metadata, $key, $page
);
}
/** @inheritDoc */
public function logLinterData( IPageConfig $pageConfig, array $lints ): void {
if ( $this->readOnlyMode->isReadOnly() ) {
return;
}
$revId = $pageConfig->getRevisionId();
$title = Title::newFromLinkTarget(
$pageConfig->getLinkTarget()
)->getPrefixedText();
$pageInfo = $this->getPageInfo( $pageConfig, [ $title ] );
$latest = $pageInfo[$title]['revId'];
// Only send the request if it the latest revision
if ( $revId !== null && $revId === $latest ) {
$this->hookRunner->onParserLogLinterData(
$title, $revId, $lints
);
}
}
}

File Metadata

Mime Type
text/x-php
Expires
Wed, Aug 19, 07:27 (2 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
36/08/84c815f53e6f45b3886f9c3d9b7d
Default Alt Text
DataAccess.php (17 KB)

Event Timeline