Current File : /home/jvzmxxx/wiki/extensions/Kartographer/includes/Tag/TagHandler.php
<?php
/**
 *
 * @license MIT
 * @file
 *
 * @author Yuri Astrakhan
 */

namespace Kartographer\Tag;

use Exception;
use FormatJson;
use Html;
use Kartographer\SimpleStyleParser;
use Language;
use Parser;
use ParserOutput;
use PPFrame;
use Sanitizer;
use Status;
use stdClass;
use Title;

/**
 * Base class for all <map...> tags
 */
abstract class TagHandler {
	/** @var string */
	protected $tag;

	/** @var Status */
	protected $status;

	/** @var stdClass[] */
	protected $geometries = [];

	/** @var string[] */
	protected $args;

	/** @var float */
	protected $lat;

	/** @var float */
	protected $lon;

	/** @var int */
	protected $zoom;

	/** @var string */
	protected $mapStyle;

	/** @var string */
	protected $style = '';

	/** @var string name of the group, or null for private */
	protected $groupName;

	/** @var string[] list of groups to show */
	protected $showGroups = [];

	/** @var int|null */
	protected $counter = null;

	/** @var Parser */
	protected $parser;

	/** @var PPFrame */
	protected $frame;

	/** @var stdClass */
	protected $markerProperties;

	/**
	 * @return stdClass[]
	 */
	public function getGeometries() {
		return $this->geometries;
	}

	/**
	 * @return Status
	 */
	public function getStatus() {
		return $this->status;
	}

	/**
	 * Entry point for all tags
	 *
	 * @param $input
	 * @param array $args
	 * @param Parser $parser
	 * @param PPFrame $frame
	 * @return string
	 */
	public static function entryPoint( $input, array $args, Parser $parser, PPFrame $frame ) {
		$handler = new static();

		return $handler->handle( $input, $args, $parser, $frame );
	}

	/**
	 * @param string $input
	 * @param array $args
	 * @param Parser $parser
	 * @param PPFrame $frame
	 * @return string
	 */
	private final function handle( $input, array $args, Parser $parser, PPFrame $frame ) {
		$this->parser = $parser;
		$this->frame = $frame;
		$output = $parser->getOutput();
		$output->addModuleStyles( 'ext.kartographer.style' );

		$this->status = Status::newGood();
		$this->args = $args;

		$this->parseGeometries( $input, $parser, $frame );
		$this->parseGroups();
		$this->parseArgs();

		if ( !$this->status->isGood() ) {
			return $this->reportError();
		}

		$this->saveData( $output );

		$output->setExtensionData( 'kartographer_valid', true );

		return $this->render();
	}

	/**
	 * Parses and sanitizes GeoJSON+simplestyle contained inside of tags
	 *
	 * @param $input
	 * @param Parser $parser
	 * @param PPFrame $frame
	 */
	protected function parseGeometries( $input, Parser $parser, PPFrame $frame ) {
		$simpleStyle = new SimpleStyleParser( $parser, $frame );

		$this->status = $simpleStyle->parse( $input );
		if ( $this->status->isOK() ) {
			$this->geometries = $this->status->getValue();
		}
	}

	/**
	 * Parses tag attributes in $this->args
	 * @return void
	 */
	protected function parseArgs() {
		global $wgKartographerStyles, $wgKartographerDfltStyle;

		$this->lat = $this->getFloat( 'latitude' );
		$this->lon = $this->getFloat( 'longitude' );
		$this->zoom = $this->getInt( 'zoom' );
		$regexp = '/^(' . implode( '|', $wgKartographerStyles ) . ')$/';
		$this->mapStyle = $this->getText( 'mapstyle', $wgKartographerDfltStyle, $regexp );
		$this->style = Sanitizer::checkCss( trim( $this->getText( 'style', '' ) ) );
	}

	/**
	 * Returns default HTML attributes of the outermost tag of the output
	 * @param string $extraStyle
	 * @return string[]
	 */
	protected function getDefaultAttributes( $extraStyle = '' ) {
		$attrs = [ 'class' => 'mw-kartographer', 'mw-data' => 'interface' ];
		$style = trim( "{$extraStyle} {$this->style}" );
		if ( $style ) {
			$attrs['style'] = $style;
		}
		return $attrs;
	}

	/**
	 * When overridden in a descendant class, returns tag HTML
	 * @return string
	 */
	protected abstract function render();

	private function parseGroups() {
		global $wgKartographerWikivoyageMode;

		if ( !$wgKartographerWikivoyageMode ) {
			// if we ignore all the 'group' and 'show' parameters,
			// each tag stays private, and will be unable to share data
			return;
		}

		$this->groupName = $this->getText( 'group', null, '/^[a-zA-Z0-9]+$/' );

		$text = $this->getText( 'show', null, '/^[a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*$/' );
		if ( $text !== null ) {
			$this->showGroups = array_map( 'trim', explode( ',', $text ) );
		}

		// Make sure the current group is shown for this map, even if there is no geojson
		// Private group will be added during the save, as it requires hash calculation
		if ( $this->groupName !== null ) {
			$this->showGroups[] = $this->groupName;
		}

		// Make sure there are no group name duplicates
		$this->showGroups = array_unique( $this->showGroups );
	}

	protected function getInt( $name, $default = false ) {
		$value = $this->getText( $name, $default, '/^-?[0-9]+$/' );
		if ( $value !== false ) {
			$value = intval( $value );
		}

		return $value;
	}

	/**
	 * @param $name
	 * @param bool $default
	 * @return float|string
	 */
	protected function getFloat( $name, $default = false ) {
		$value = $this->getText( $name, $default, '/^-?[0-9]*\.?[0-9]+$/' );
		if ( $value !== false ) {
			$value = floatval( $value );
		}

		return $value;
	}

	/**
	 * Returns value of a named tag attribute with optional validation
	 *
	 * @param string $name Attribute name
	 * @param string|bool $default Default value or false to trigger error if absent
	 * @param string|bool $regexp Regular expression to validate against or false to not validate
	 * @return string
	 */
	protected function getText( $name, $default, $regexp = false ) {
		if ( !isset( $this->args[$name] ) ) {
			if ( $default === false ) {
				$this->status->fatal( 'kartographer-error-missing-attr', $name );
			}
			return $default;
		}
		$value = trim( $this->args[$name] );
		if ( $regexp && !preg_match( $regexp, $value ) ) {
			$value = false;
			$this->status->fatal( 'kartographer-error-bad_attr', $name );
		}

		return $value;
	}


	protected function saveData( ParserOutput $output ) {
		if ( !$this->geometries ) {
			return;
		}

		// Merge existing data with the new tag's data under the same group name

		// For all GeoJSON items whose marker-symbol value begins with '-counter' and '-letter',
		// recursively replace them with an automatically incremented marker icon.
		$counters = $output->getExtensionData( 'kartographer_counters' ) ?: new stdClass();
		$marker = SimpleStyleParser::doCountersRecursive( $this->geometries, $counters );
		if ( $marker ) {
			list( $this->counter, $this->markerProperties ) = $marker;
		}
		$output->setExtensionData( 'kartographer_counters', $counters );

		if ( $this->groupName === null ) {
			$group = '_' . sha1( FormatJson::encode( $this->geometries, false, FormatJson::ALL_OK ) );
			$this->groupName = $group;
			$this->showGroups[] = $group;
			// no need to array_unique() because it's impossible to manually add a private group
		} else {
			$group = $this->groupName;
		}

		$data = $output->getExtensionData( 'kartographer_data' ) ?: new stdClass();
		if ( isset( $data->$group ) ) {
			$data->$group = array_merge( $data->$group, $this->geometries );
		} else {
			$data->$group = $this->geometries;
		}
		$output->setExtensionData( 'kartographer_data', $data );
	}

	/**
	 * Handles the last step of parse process
	 * @param Parser $parser
	 */
	public static function finalParseStep( Parser $parser ) {
		$output = $parser->getOutput();

		$data = $output->getExtensionData( 'kartographer_data' );
		if ( $data ) {
			$json = FormatJson::encode( $data, false, FormatJson::ALL_OK );
			$output->setProperty( 'kartographer', gzencode( $json ) );
		}

		if ( $output->getExtensionData( 'kartographer_broken' ) ) {
			$output->addTrackingCategory( 'kartographer-broken-category', $parser->getTitle() );
		}
		if ( $output->getExtensionData( 'kartographer_valid' ) ) {
			$output->addTrackingCategory( 'kartographer-tracking-category', $parser->getTitle() );
		}

		$interact = $output->getExtensionData( 'kartographer_interact' );
		if ( $interact ) {
			$interact = array_flip( array_unique( $interact ) );
			$liveData = array_intersect_key( (array)$data, $interact );
			$output->addJsConfigVars( 'wgKartographerLiveData', $liveData );
		}
	}

	/**
	 * @return string
	 * @throws Exception
	 */
	private function reportError() {
		$this->parser->getOutput()->setExtensionData( 'kartographer_broken', true );
		$errors = array_merge( $this->status->getErrorsByType( 'error' ),
			$this->status->getErrorsByType( 'warning' )
		);
		if ( !count( $errors ) ) {
			throw new Exception( __METHOD__ . '(): attempt to report error when none took place' );
		}
		$message = count( $errors ) > 1 ? 'kartographer-error-context-multi'
			: 'kartographer-error-context';
		// Status sucks, redoing a bunch of its code here
		$errorText = implode( "\n* ", array_map( function( array $err ) {
				return wfMessage( $err['message'] )
					->params( $err['params'] )
					->inLanguage( $this->getLanguage() )
					->plain();
			}, $errors ) );
		if ( count( $errors ) > 1 ) {
			$errorText = '* ' . $errorText;
		}
		return Html::rawElement( 'div', array( 'class' => 'mw-kartographer mw-kartographer-error' ),
			wfMessage( $message, $this->tag, $errorText )->inLanguage( $this->getLanguage() )->parse() );
	}

	/**
	 * @return Language
	 */
	protected function getLanguage() {
		return $this->parser->getTitle()->getPageLanguage();
	}
}