| Current Path : /var/www/html/mediawiki/extensions/Graph/includes/ |
| Current File : /var/www/html/mediawiki/extensions/Graph/includes/ParserTag.php |
<?php
/**
*
* @license MIT
* @file
*
* @author Dan Andreescu, Yuri Astrakhan, Frédéric Bolduc, Joseph Seddon, Isabelle Hurbain-Palatin
*/
namespace Graph;
use MediaWiki\Html\Html;
use MediaWiki\Json\FormatJson;
use MediaWiki\Language\Language;
use MediaWiki\Linker\Linker;
use MediaWiki\MediaWikiServices;
use MediaWiki\Message\Message;
use MediaWiki\Output\OutputPage;
use MediaWiki\Page\PageReference;
use MediaWiki\Parser\Parser;
use MediaWiki\Parser\ParserOptions;
use MediaWiki\Parser\ParserOutput;
use MediaWiki\Parser\PPFrame;
use MediaWiki\Status\Status;
use MediaWiki\Title\Title;
class ParserTag {
/** Sync with mapSchema.js */
private const DEFAULT_WIDTH = 500;
private const DEFAULT_HEIGHT = 500;
/** @var ParserOptions */
private $parserOptions;
/** @var ParserOutput */
private $parserOutput;
/** @var Language */
private $language;
/**
* @param Parser $parser
* @param ParserOptions $parserOptions
* @param ParserOutput $parserOutput
*/
public function __construct(
Parser $parser, ParserOptions $parserOptions, ParserOutput $parserOutput
) {
$this->parserOptions = $parserOptions;
$this->parserOutput = $parserOutput;
$this->language = $parser->getTargetLanguage();
}
/**
* <graph> parser tag handler.
* @param string|null $input
* @param array $args
* @param Parser $parser
* @param PPFrame $frame
* @return string
*/
public static function onGraphTag( $input, array $args, Parser $parser, PPFrame $frame ) {
$tag = new self( $parser, $parser->getOptions(), $parser->getOutput() );
$html = $tag->buildHtml( (string)$input, $parser->getRevisionId(), $args );
self::addTagMetadata( $parser->getOutput(), $parser->getPage(), $parser->getOptions()->getIsPreview() );
return $html;
}
/**
* - Add tracking categories
* - Split parser cache for preview, where Graph uses different HTML
* @param ParserOutput $parserOutput
* @param ?PageReference $pageRef
* @param bool $isPreview
*/
public static function addTagMetadata(
ParserOutput $parserOutput, ?PageReference $pageRef, bool $isPreview
): void {
$tc = MediaWikiServices::getInstance()->getTrackingCategories();
if ( $parserOutput->getExtensionData( 'graph_specs_broken' ) ) {
$tc->addTrackingCategory( $parserOutput, 'graph-broken-category', $pageRef );
}
if ( $parserOutput->getExtensionData( 'graph_specs_obsolete' ) ) {
$tc->addTrackingCategory( $parserOutput, 'graph-obsolete-category', $pageRef );
}
$specs = $parserOutput->getExtensionData( 'graph_specs_index' );
if ( $specs === null ) {
return;
}
$tc->addTrackingCategory( $parserOutput, 'graph-tracking-category', $pageRef );
if ( $isPreview ) {
$parserOutput->updateCacheExpiry( 0 );
}
}
/**
* Called on OutputPageParserOutput, handles initializing the client-side logic based on
* the graph data collected in the ParserOutput.
* @param OutputPage $outputPage
* @param ParserOutput $parserOutput
*/
public static function finalizeParserOutput(
OutputPage $outputPage, ParserOutput $parserOutput
): void {
$specs = $parserOutput->getExtensionData( 'graph_specs_index' );
if ( $specs === null ) {
return;
}
$outputPage->addModuleStyles( [ 'ext.graph.styles' ] );
// We can only load one version of vega lib - either 1 or 2
// If the default version is 1, and if any of the graphs need Vega2,
// we treat all graphs as Vega2 and load corresponding libraries.
// All this should go away once we drop Vega1 support.
$liveSpecsIndex = $parserOutput->getExtensionData( 'graph_live_specs_index' );
$outputPage->addModules( [ 'ext.graph.loader' ] );
$liveSpecs = [];
foreach ( $liveSpecsIndex as $hash => $ignore ) {
$liveSpecs[$hash] =
$parserOutput->getExtensionData( 'graph_live_specs[' . $hash . ']' );
}
$outputPage->addJsConfigVars( 'wgGraphSpecs', $liveSpecs );
}
/**
* @param string $mode lazyload|interactable(click to load)|always(live)|''
* @param mixed $data
* @param string $hash
* @return array
*/
public static function buildDivAttributes( $mode = '', $data = false, $hash = '' ) {
$attribs = [ 'class' => 'mw-graph mw-graph-clickable' ];
if ( is_object( $data ) ) {
$width = property_exists( $data, 'width' ) && is_int( $data->width ) ? $data->width : self::DEFAULT_WIDTH;
$height =
property_exists( $data, 'height' ) && is_int( $data->height ) ? $data->height : self::DEFAULT_HEIGHT;
if ( $width && $height ) {
$attribs['style'] = "width:{$width}px;height:{$height}px;aspect-ratio:$width/$height";
}
}
if ( $mode ) {
$attribs['class'] .= ' mw-graph-' . $mode;
}
if ( $hash ) {
$attribs['data-graph-id'] = $hash;
}
return $attribs;
}
/**
* @param Message $msg
* @return string
*/
private function formatError( Message $msg ) {
$this->parserOutput->setExtensionData( 'graph_specs_broken', true );
$error = $msg->inLanguage( $this->language )->parse();
return "<span class=\"error\">{$error}</span>";
}
/**
* @param Status $status
* @return string
*/
private function formatStatus( Status $status ) {
return $this->formatError( $status->getMessage( false, false, $this->language ) );
}
/**
* Generate HTML output for the <graph> parser tag.
* On error, outputs an error message. On success, outputs an empty div with the Vega
* specification's sha1 hash in its 'data-graph-id' attribute.
* Sets the following keys in the ParserOutput extension data:
* - graph_vega2 and graph_specs_obsolete: there is at least one Vega 2 graph on the page
* - graph_vega5: there is at least one Vega 5 graph on the page
* - graph_specs_index: a list of all Vega spec hashes
* - graph_specs[<hash>]: the Vega spec whose sha1 hash is <hash> (note the hash and brackets
* are literally part of the key)
* - graph_live_specs_index and graph_live_specs[<hash>]: same thing but for graphs shown
* during page preview.
* @param string $jsonText <graph> tag contents; expected to be a JSON Vega definition.
* @param int $revid
* @param array|null $args <graph> tag attributes:
* title: no longer used?
* fallback: title of a fallback image for noscript
* fallbackWidth: width of the fallback image
* fallbackHeight: height of the fallback image
* @return string
*/
public function buildHtml( $jsonText, $revid, $args = null ) {
$jsonText = trim( $jsonText );
if ( $jsonText === '' ) {
return $this->formatError( wfMessage( 'graph-error-empty-json' ) );
}
$status = FormatJson::parse( $jsonText, FormatJson::TRY_FIXING | FormatJson::STRIP_COMMENTS );
if ( !$status->isOK() ) {
return $this->formatStatus( $status );
}
$data = $status->getValue();
if ( !is_object( $data ) ) {
return $this->formatError( wfMessage( 'graph-error-not-vega' ) );
}
// Figure out which vega version to use (TODO drop this)
global $wgGraphDefaultVegaVer;
if ( property_exists( $data, '$schema' ) ) {
if ( !preg_match(
// https://vega.github.io/schema/
'!https://vega.github.io/schema/(vega|vega-lite)/v(\d)(?:\.\d){0,2}.json!',
$data->{'$schema'},
$matches
) ) {
return $this->formatError( wfMessage( 'graph-error-not-vega' ) );
} elseif ( $matches[1] === 'vega-lite' ) {
return $this->formatError( wfMessage( 'graph-error-vega-lite-unsupported' ) );
}
$version = (int)$matches[2];
} elseif ( property_exists( $data, 'version' ) && is_numeric( $data->version ) ) {
$version = $data->version;
} else {
$version = $data->version = $wgGraphDefaultVegaVer;
}
if ( $version === 2 ) {
$this->parserOutput->setExtensionData( 'graph_vega2', true );
$this->parserOutput->setExtensionData( 'graph_specs_obsolete', true );
} elseif ( $version === 5 ) {
$this->parserOutput->setExtensionData( 'graph_vega5', true );
} else {
return $this->formatError( wfMessage( 'graph-error-vega-unsupported-version', $version ) );
}
// Make sure that multiple json blobs that only differ in spacing hash the same
$hash = sha1( FormatJson::encode( $data, false, FormatJson::ALL_OK ) );
// graph_specs is used in ApiGraph; graph_specs_index is also used to set up the
// graph tracking category and to gate finalizeParserOutput (we only check whether it's
// null or not in those two instances)
// TODO: consider merging graph_specs and graph_live_specs to a unique "array" instead of 2
$this->parserOutput->appendExtensionData( 'graph_specs_index', $hash );
$this->parserOutput->setExtensionData( 'graph_specs[' . $hash . ']', $data );
// Switching this to false (lazyload), will break cache
$alwaysMode = true;
/* @phan-suppress-next-line PhanRedundantCondition */
if ( $this->parserOptions->getIsPreview() || $alwaysMode ) {
$this->parserOutput->appendExtensionData( 'graph_live_specs_index', $hash );
$this->parserOutput->setExtensionData( 'graph_live_specs[' . $hash . ']', $data );
$attribs = self::buildDivAttributes( 'always', $data, $hash );
} else {
$attribs = self::buildDivAttributes( 'lazyload', $data, $hash );
}
$isFallback = isset( $args[ 'fallback' ] ) && $args[ 'fallback' ] !== '';
if ( $isFallback ) {
global $wgThumbLimits, $wgDefaultUserOptions;
/* @phan-suppress-next-line PhanTypeArraySuspiciousNullable */
$fallbackArgTitle = $args[ 'fallback' ];
$services = MediaWikiServices::getInstance();
$fallbackParser = $services->getParser();
$title = Title::makeTitle( NS_FILE, $fallbackArgTitle );
$file = $services->getRepoGroup()->findFile( $title );
$imgFallbackParams = [];
if ( isset( $args[ 'fallbackWidth' ] ) && $args[ 'fallbackWidth' ] > 0 ) {
$width = $args[ 'fallbackWidth' ];
$imgFallbackParams[ 'width' ] = $width;
} elseif ( property_exists( $data, 'width' ) ) {
$width = is_int( $data->width ) ? $data->width : 0;
$imgFallbackParams[ 'width' ] = $width;
} else {
$imgFallbackParams[ 'width' ] = $wgThumbLimits[ $wgDefaultUserOptions[ 'thumbsize' ] ];
}
$imgFallback = Linker::makeImageLink( $fallbackParser, $title, $file, [ '' ], $imgFallbackParams );
$noSriptAttrs = [
'class' => 'mw-graph-noscript',
];
// $html will be injected with a <canvas> tag
$html = Html::rawElement( 'noscript', $noSriptAttrs, $imgFallback );
} else {
$attribs[ 'class' ] .= ' mw-graph-nofallback';
$html = '';
}
return Html::rawElement( 'div', $attribs, $html );
}
}