Current File : /home/jvzmxxx/wiki1/extensions/Flow/includes/TemplateHelper.php
<?php

namespace Flow;

use Flow\Exception\FlowException;
use Flow\Exception\WrongNumberArgumentsException;
use Flow\Model\UUID;
use Closure;
use HTML;
use OOUI\IconWidget;
use LightnCandy;
use MWTimestamp;
use RequestContext;
use Title;

class TemplateHelper {

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

	/**
	 * @var callable[]
	 */
	protected $renderers;

	/**
	 * @var bool Always compile template files
	 */
	protected $forceRecompile = false;

	/**
	 * @param string $templateDir
	 * @param boolean $forceRecompile
	 */
	public function __construct( $templateDir, $forceRecompile = false ) {
		$this->templateDir = $templateDir;
		$this->forceRecompile = $forceRecompile;
	}

	/**
	 * Constructs the location of the the source handlebars template
	 * and the compiled php code that goes with it.
	 *
	 * @param string $templateName
	 *
	 * @return string[]
	 * @throws FlowException Disallows upwards directory traversal via $templateName
	 */
	public function getTemplateFilenames( $templateName ) {
		// Prevent upwards directory traversal using same methods as Title::secureAndSplit,
		// which is implemented in MediaWikiTitleCodec::splitTitleString.
		if (
			strpos( $templateName, '.' ) !== false &&
			(
				$templateName === '.' || $templateName === '..' ||
				strpos( $templateName, './' ) === 0 ||
				strpos( $templateName, '../' ) === 0 ||
				strpos( $templateName, '/./' ) !== false ||
				strpos( $templateName, '/../' ) !== false ||
				substr( $templateName, -2 ) === '/.' ||
				substr( $templateName, -3 ) === '/..'
			)
		) {
			throw new FlowException( "Malformed \$templateName: $templateName" );
		}

		return array(
			'template' => "{$this->templateDir}/{$templateName}.handlebars",
			'compiled' => "{$this->templateDir}/compiled/{$templateName}.handlebars.php",
		);
	}

	/**
	 * Returns a given template function if found, otherwise throws an exception.
	 *
	 * @param string $templateName
	 *
	 * @return Closure
	 * @throws FlowException
	 * @throws \Exception
	 */
	public function getTemplate( $templateName ) {
		if ( isset( $this->renderers[$templateName] ) ) {
			return $this->renderers[$templateName];
		}

		$filenames = $this->getTemplateFilenames( $templateName );

		if ( $this->forceRecompile ) {
			if ( !file_exists( $filenames['template'] ) ) {
				throw new FlowException( "Could not locate template: {$filenames['template']}" );
			}

			$code = self::compile( file_get_contents( $filenames['template'] ), $this->templateDir );

			if ( !$code ) {
				throw new FlowException( "Failed to compile template '$templateName'." );
			}
			$success = file_put_contents( $filenames['compiled'], $code );

			// failed to recompile template (OS permissions?); unless the
			// content hasn't changes, throw an exception!
			if ( !$success && file_get_contents( $filenames['compiled'] ) !== $code ) {
				throw new FlowException( "Failed to save updated compiled template '$templateName'" );
			}
		}

		/** @var callable $renderer */
		$renderer = require $filenames['compiled'];
		return $this->renderers[$templateName] = function( $args, array $scopes = array() ) use ( $templateName, $renderer ) {
			return $renderer( $args, $scopes );
		};
	}

	/**
	 * @param string $code Handlebars code
	 * @param string $templateDir Directory templates are stored in
	 *
	 * @return string PHP code
	 */
	static public function compile( $code, $templateDir ) {
		return LightnCandy::compile(
			$code,
			array(
				'flags' => LightnCandy::FLAG_ERROR_EXCEPTION
					| LightnCandy::FLAG_EXTHELPER
					| LightnCandy::FLAG_SPVARS
					| LightnCandy::FLAG_HANDLEBARS
					| LightnCandy::FLAG_RUNTIMEPARTIAL,
				'basedir' => array( $templateDir ),
				'fileext' => array( '.partial.handlebars' ),
				'helpers' => array(
					'l10n' => 'Flow\TemplateHelper::l10n',
					'uuidTimestamp' => 'Flow\TemplateHelper::uuidTimestamp',
					'timestamp' => 'Flow\TemplateHelper::timestampHelper',
					'html' => 'Flow\TemplateHelper::htmlHelper',
					'block' => 'Flow\TemplateHelper::block',
					'author' => 'Flow\TemplateHelper::author',
					'post' => 'Flow\TemplateHelper::post',
					'historyTimestamp' => 'Flow\TemplateHelper::historyTimestamp',
					'historyDescription' => 'Flow\TemplateHelper::historyDescription',
					'showCharacterDifference' => 'Flow\TemplateHelper::showCharacterDifference',
					'l10nParse' => 'Flow\TemplateHelper::l10nParse',
					'diffRevision' => 'Flow\TemplateHelper::diffRevision',
					'diffUndo' => 'Flow\TemplateHelper::diffUndo',
					'moderationAction' => 'Flow\TemplateHelper::moderationAction',
					'concat' => 'Flow\TemplateHelper::concat',
					'user' => 'Flow\TemplateHelper::user',
					'linkWithReturnTo' => 'Flow\TemplateHelper::linkWithReturnTo',
					'escapeContent' => 'Flow\TemplateHelper::escapeContent',
					'enablePatrollingLink' => 'Flow\TemplateHelper::enablePatrollingLink',
					'oouify' => 'Flow\TemplateHelper::oouify',
				),
				'hbhelpers' => array(
					'eachPost' => 'Flow\TemplateHelper::eachPost',
					'ifAnonymous' => 'Flow\TemplateHelper::ifAnonymous',
					'ifCond' => 'Flow\TemplateHelper::ifCond',
					'tooltip' => 'Flow\TemplateHelper::tooltip',
					'progressiveEnhancement' => 'Flow\TemplateHelper::progressiveEnhancement',
				),
			)
		);
	}

	/**
	 * Returns HTML for a given template by calling the template function with the given args.
	 *
	 * @param string $templateName
	 * @param array $args
	 * @param array $scopes
	 *
	 * @return string
	 */
	static public function processTemplate( $templateName, $args, array $scopes = array() ) {
		// Undesirable, but lightncandy helpers have to be static methods
		/** @var TemplateHelper $lightncandy */
		$lightncandy = Container::get( 'lightncandy' );
		$template = $lightncandy->getTemplate( $templateName );
		// @todo ugly hack...remove someday.  Requires switching to newest version
		// of lightncandy which supports recursive partial templates.
		if ( !array_key_exists( 'rootBlock', $args ) ) {
			$args['rootBlock'] = $args;
		}
		return call_user_func( $template, $args, $scopes );
	}

	// Helpers

	/**
	 * Generates a timestamp using the UUID, then calls the timestamp helper with it.
	 *
	 * @param array $args Expects string $uuid, string $str, bool $timeAgoOnly = false
	 * @param array $named No named arguments expected
	 *
	 * @return null|string
	 * @throws WrongNumberArgumentsException
	 */
	static public function uuidTimestamp( array $args, array $named ) {
		if ( count( $args ) !== 1 ) {
			throw new WrongNumberArgumentsException( $args, 'one' );
		}
		$uuid = $args[0];

		$obj = UUID::create( $uuid );
		if ( !$obj ) {
			return null;
		}

		// timestamp helper expects ms timestamp
		$timestamp = $obj->getTimestampObj()->getTimestamp() * 1000;
		return self::timestamp( $timestamp );
	}

	/**
	 * @param array $args Expects string $timestamp, string $str, bool $timeAgoOnly = false
	 * @param array $named No named arguments expected
	 *
	 * @return string
	 * @throws WrongNumberArgumentsException
	 */
	static public function timestampHelper( array $args, array $named ) {
		if ( count( $args ) < 1 || count( $args ) > 2 ) {
			throw new WrongNumberArgumentsException( $args, 'one', 'two' );
		}
		return self::timestamp(
			$args[0],
			isset( $args[1] ) ? $args[1] : false
		);
	}

	/**
	 * @param integer $timestamp milliseconds since the unix epoch
	 *
	 * @return string|false
	 */
	static protected function timestamp( $timestamp ) {
		global $wgLang, $wgUser;

		if ( !$timestamp ) {
			return false;
		}

		// source timestamps are in ms
		$timestamp /= 1000;
		$ts = new MWTimestamp( $timestamp );

		return self::html( self::processTemplate(
			'timestamp',
			array(
				'time_iso' => $timestamp,
				'time_ago' => $ts->getHumanTimestamp(),
				'time_readable' => $wgLang->userTimeAndDate( $timestamp, $wgUser ),
				'guid' => null, //generated client-side
			)
		) );
	}

	/**
	 * Takes in HTML string, returns array that tells lightncandy to skip escaping.
	 * Only works for values returned from helpers, does not work when passing
	 * variable into a template or helper.
	 *
	 * @param string $string
	 *
	 * @return string[] array(html, 'raw')
	 */
	static protected function html( $string ) {
		return array( $string, 'raw' );
	}

	/**
	 * @param array $args Expects one string argument to be output unescaped.
	 * @param array $named unused
	 *
	 * @return string[] array(html, 'raw')
	 */
	static public function htmlHelper( array $args, array $named ) {
		return self::html( isset( $args[0] ) ? $args[0] : 'undefined' );
	}

	/**
	 * @param array $args Expects one array $block
	 * @param array $named No named arguments expected
	 *
	 * @return string[]
	 * @throws WrongNumberArgumentsException
	 */
	static public function block( array $args, array $named ) {
		if ( !isset( $args[0] ) ) {
			throw new WrongNumberArgumentsException( $args, 'one' );
		}
		$block = $args[0];
		$template = "flow_block_" . $block['type'];
		if ( $block['block-action-template'] ) {
			$template .= '_' . $block['block-action-template'];
		}
		return self::html( self::processTemplate(
			$template,
			$block
		) );
	}

	/**
	 * @param array $context The 'this' value of the calling context
	 * @param array $postIds List of ids (roots)
	 * @param array $options blockhelper specific invocation options
	 *
	 * @return null|string HTML
	 * @throws FlowException When callbacks are not Closure instances
	 */
	static public function eachPost( $context, $postIds, $options ) {
		/** @var callable $inverse */
		$inverse = isset( $options['inverse'] ) ? $options['inverse'] : null;
		/** @var callable $fn */
		$fn = $options['fn'];

		if ( $postIds && !is_array( $postIds ) ) {
			$postIds = array( $postIds );
		} elseif ( count( $postIds ) === 0 ) {
			// Failure callback, if any
			if ( !$inverse ) {
				return null;
			}
			if ( !$inverse instanceof Closure ) {
				throw new FlowException( 'Invalid inverse callback, expected Closure' );
			}
			return $inverse( $options['cx'], array() );
		} else {
			return null;
		}

		if ( !$fn instanceof Closure ) {
			throw new FlowException( 'Invalid callback, expected Closure' );
		}
		$html = array();
		foreach ( $postIds as $id ) {
			$revId = $context['posts'][$id][0];

			if ( !isset( $context['revisions'][$revId] ) ) {
				throw new FlowException( "Revision not available: $revId" );
			}

			// $fn is always safe return value, it's the inner template content.
			$html[] = $fn( $context['revisions'][$revId] );
		}

		// Return the resulting HTML
		return implode( '', $html );
	}

	/**
	 * Required to prevent recursion loop rendering nested posts
	 *
	 * @param array $args Expects array $rootBlock, array $revision
	 * @param array $named No named arguments expected
	 *
	 * @return string[]
	 * @throws WrongNumberArgumentsException
	 */
	static public function post( array $args, array $named ) {
		if ( count( $args ) !== 2 ) {
			throw new WrongNumberArgumentsException( $args, 'two' );
		}
		list( $rootBlock, $revision ) = $args;
		return self::html( self::processTemplate( 'flow_post', array(
			'revision' => $revision,
			'rootBlock' => $rootBlock,
		) ) );
	}

	/**
	 * @param array $args Expects array $revision, string $key = 'timeAndDate'
	 * @param array $named No named arguments expected
	 *
	 * @return string[]
	 * @throws WrongNumberArgumentsException
	 */
	static public function historyTimestamp( array $args, array $named ) {
		if ( !$args ) {
			throw new WrongNumberArgumentsException( $args, 'one', 'two' );
		}
		$revision = $args[0];
		$raw = false;
		$formattedTime = $revision['dateFormats']['timeAndDate'];
		$linkKeys = array( 'header-revision', 'topic-revision', 'post-revision', 'summary-revision' );
		foreach ( $linkKeys as $linkKey ) {
			if ( isset( $revision['links'][$linkKey] ) ) {
				$link = $revision['links'][$linkKey];
				$formattedTime = Html::element(
					'a',
					array(
						'href' => $link['url'],
						'title' => $link['title'],
					),
					$formattedTime
				);
				$raw = true;
				break;
			}
		}

		if ( $raw === false ) {
			$formattedTime = htmlspecialchars( $formattedTime );
		}

		$class = array( 'mw-changeslist-date' );
		if ( $revision['isModeratedNotLocked'] ) {
			$class[] = 'history-deleted';
		}

		return self::html(
			'<span class="plainlinks">'
			. Html::rawElement( 'span', array( 'class' => $class ), $formattedTime )
			. '</span>'
		);
	}

	/**
	 * @param array $args Expects array $revision
	 * @param array $named No named arguments expected
	 *
	 * @return string[]
	 * @throws WrongNumberArgumentsException
	 */
	static public function historyDescription( array $args, array $named ) {
		if ( count( $args ) !== 1 ) {
			throw new WrongNumberArgumentsException( $args, 'one' );
		}
		$revision = $args[0];
		if ( !isset( $revision['properties']['_key'] ) ) {
			return '';
		}

		$i18nKey = $revision['properties']['_key'];
		unset( $revision['properties']['_key'] );

		// a variety of the i18n history messages contain wikitext and require ->parse()
		return self::html( wfMessage( $i18nKey, $revision['properties'] )->parse() );
	}

	/**
	 * @param array $args Expects string $old, string $new
	 * @param array $named No named arguments expected
	 *
	 * @return string[]
	 * @throws WrongNumberArgumentsException
	 */
	static public function showCharacterDifference( array $args, array $named ) {
		if ( count( $args ) !== 2 ) {
			throw new WrongNumberArgumentsException( $args, 'two' );
		}
		list( $old, $new ) = $args;
		return self::html( \ChangesList::showCharacterDifference( $old, $new ) );
	}

	/**
	 * Creates a special script tag to be processed client-side. This contains extra template HTML, which allows
	 * the front-end to "progressively enhance" the page with more content which isn't needed in a non-JS state.
	 *
	 * @see FlowHandlebars.prototype.progressiveEnhancement in flow-handlebars.js for more details.
	 *
	 * @param array $options
	 *
	 * @return string[]
	 */
	static public function progressiveEnhancement( array $options ) {
		$fn = $options['fn'];
		$input = $options['hash'];
		$insertionType = empty( $input['type'] ) ? 'insert' : htmlspecialchars( $input['type'] );
		$target = empty( $input['target'] ) ? '' : 'data-target="' . htmlspecialchars( $input['target'] ) . '"';
		$sectionId = empty( $input['id'] ) ? '' : 'id="' . htmlspecialchars( $input['id'] ) . '"';

		return self::html(
			'<script name="handlebars-template-progressive-enhancement"' .
				' type="text/x-handlebars-template-progressive-enhancement"' .
				' data-type="' . $insertionType . '"' .
				' ' . $target .
				' ' . $sectionId .
			'>' .
				// Replace the nested script tag with a placeholder tag for recursive progressiveEnhancement
				str_replace( '</script>', '</flowprogressivescript>', $fn() ) .
			'</script>'
		);
	}

	/**
	 * A helper to output OOUI widgets.
	 *
	 * @param array $args one or more arguments, i18n key and parameters
	 * @param array $named named object for arguments given by handlebars
	 * @return string Representation of an ooui widget dom
	 */
	static public function oouify( array $args, array $named ) {
		$widgetType = $named[ 'type' ];
		$data = array();

		$classes = array();
		if ( isset( $named['classes'] ) ) {
			$classes = explode( ' ', $named[ 'classes' ] );
		}

		// Push raw arguments
		$data['args'] = $args;
		$baseConfig = array(
			// 'infusable' => true,
			'id' => isset( $named[ 'name' ] ) ? isset( $named[ 'name' ] ) : null,
			'classes' => $classes,
			'data' => $data
		);
		switch( $widgetType ) {
			case 'BoardDescriptionWidget':
				$dataArgs = array(
					'infusable' => false,
					'description' => $args[0],
					'editLink' => $args[1]
				);
				$widget = new OOUI\BoardDescriptionWidget( $baseConfig + $dataArgs );
				break;
			case 'IconWidget':
				$dataArgs = array(
					'icon' => $args[0],
				);
				$widget = new IconWidget( $baseConfig + $dataArgs );
				break;
		}

		return $widget;
	}

	/**
	 * @param array $args one or more arguments, i18n key and parameters
	 * @param array $named unused
	 *
	 * @return string Message output, using the 'text' format
	 */
	static public function l10n( array $args, array $named ) {
		$message = null;
		$str = array_shift( $args );

		return wfMessage( $str )->params( $args )->text();
	}
	/**
	 * @param array $args one or more arguments, i18n key and parameters
	 * @param array $named unused
	 *
	 * @return string[] HTML
	 */
	static public function l10nParse( array $args, array $named ) {
		$str = array_shift( $args );
		return self::html( wfMessage( $str, $args )->parse() );
	}

	/**
	 * @param array $args Expects 1 argument:
	 *	   array $data RevisionDiffViewFormatter::formatApi return value
	 * @param array $named No named arguments expected
	 *
	 * @return string[] HTML wrapped in array to prevent lightncandy from escaping
	 * @throws WrongNumberArgumentsException
	 */
	static public function diffRevision( array $args, array $named ) {
		if ( count( $args ) !== 1 ) {
			throw new WrongNumberArgumentsException( $args, 'one' );
		}

		$data = $args[0];
		$differenceEngine = new \DifferenceEngine();
		$multi = $differenceEngine->getMultiNotice();
		// Display a message when the diff is empty
		$notice = '';
		if ( $data['diff_content'] === '' ) {
			$notice .= '<div class="mw-diff-empty">' .
				wfMessage( 'diff-empty' )->parse() .
				"</div>\n";
		}
		$differenceEngine->showDiffStyle();

		$renderer = Container::get( 'lightncandy' )->getTemplate( 'flow_revision_diff_header' );

		return self::html( $differenceEngine->addHeader(
			$data['diff_content'],
			$renderer( array(
				'old' => true,
				'revision' => $data['old'],
				'links' => $data['links'],
			) ),
			$renderer( array(
				'new' => true,
				'revision' => $data['new'],
				'links' => $data['links'],
			) ),
			$multi,
			$notice
		) );
	}

	static public function diffUndo( array $args, array $named ) {
		if ( count( $args ) !== 1 ) {
			throw new WrongNumberArgumentsException( $args, 'one' );
		}
		list( $diffContent ) = $args;

		$differenceEngine = new \DifferenceEngine();
		$multi = $differenceEngine->getMultiNotice();
		$notice = '';
		if ( $diffContent === '' ) {
			$notice = '<div class="mw-diff-empty">' .
				wfMessage( 'diff-empty' )->parse() .
				"</div>\n";
		}
		$differenceEngine->showDiffStyle();

		return self::html( $differenceEngine->addHeader(
			$diffContent,
			wfMessage( 'flow-undo-latest-revision' ),
			wfMessage( 'flow-undo-your-text' ),
			$multi,
			$notice
		) );
	}

	/**
	 * @param array $args Expects array $actions, string $moderationState
	 * @param array $named No named arguments expected
	 *
	 * @return string
	 * @throws WrongNumberArgumentsException
	 */
	static public function moderationAction( array $args, array $named ) {
		if ( count( $args ) !== 2 ) {
			throw new WrongNumberArgumentsException( $args, 'two' );
		}
		list( $actions, $moderationState ) = $args;
		return isset( $actions[$moderationState] ) ? $actions[$moderationState]['url'] : '';
	}

	/**
	 * @param array $args Expects one or more strings to join
	 * @param array $named No named arguments expected
	 *
	 * @return string all unnamed arguments joined together
	 */
	static public function concat( array $args, array $named ) {
		return implode( '', $args );
	}

	/**
	 * Return information about given user
	 *
	 * @param string[] $args Expects string $feature e.g. name, id
	 * @param array $named No named arguments expected
	 *
	 * @return string value of property
	 */
	static public function user( array $args, array $named ) {
		$feature = isset( $args[0] ) ? $args[0] : 'name';
		$user = RequestContext::getMain()->getUser();
		$userInfo = array(
			'id' => $user->getId(),
			'name' => $user->getName(),
		);

		return $userInfo[$feature];
	}

	/**
	 * Runs a callback when user is anonymous
	 *
	 * @param array $options which must contain fn and inverse key mapping to functions.
	 *
	 * @return mixed result of callback
	 * @throws FlowException Fails when callbacks are not Closure instances
	 */
	static public function ifAnonymous( $options ) {
		if ( RequestContext::getMain()->getUser()->isAnon() ) {
			$fn = $options['fn'];
			if ( !$fn instanceof Closure ) {
				throw new FlowException( 'Expected callback to be Closuire instance' );
			}
		} elseif ( isset( $options['inverse'] ) ) {
			$fn = $options['inverse'];
			if ( !$fn instanceof Closure ) {
				throw new FlowException( 'Expected inverse callback to be Closuire instance' );
			}
		} else {
			return '';
		}

		return $fn();
	}

	/**
	 * Adds returnto parameter pointing to current page to existing URL
	 *
	 * @param string $url to modify
	 *
	 * @return string modified url
	 */
	static protected function addReturnTo( $url ) {
		$ctx = RequestContext::getMain();
		$returnTo = $ctx->getTitle();
		if ( !$returnTo ) {
			return $url;
		}
		// We can't get only the query parameters from
		$returnToQuery = $ctx->getRequest()->getQueryValues();

		unset( $returnToQuery['title'] );

		$args = array(
			'returnto' => $returnTo->getPrefixedUrl(),
		);
		if ( $returnToQuery ) {
			$args['returntoquery'] = wfArrayToCgi( $returnToQuery );
		}
		return wfAppendQuery( $url, wfArrayToCgi( $args ) );
	}

	/**
	 * Adds returnto parameter pointing to given Title to an existing URL
	 *
	 * @param string[] $args Expects string $title
	 * @param array $named No named arguments expected
	 *
	 * @return string modified url
	 * @throws WrongNumberArgumentsException
	 */
	static public function linkWithReturnTo( array $args, array $named ) {
		if ( count( $args ) !== 1 ) {
			throw new WrongNumberArgumentsException( $args, 'one' );
		}
		$title = Title::newFromText( $args[0] );
		if ( !$title ) {
			return '';
		}
		// FIXME: This should use local url to avoid redirects on mobile. See bug 66746.
		$url = $title->getFullUrl();

		return self::addReturnTo( $url );
	}

	/**
	 * Accepts the contentType and content properties returned from the api
	 * for individual revisions and ensures that content is included in the
	 * final html page in an xss safe maner.
	 *
	 * It is expected that all content with contentType of html has been
	 * processed by parsoid and is safe for direct output into the document.
	 *
	 * @param string[] $args Expects string $contentType, string $content
	 * @param array $named No named arguments expected
	 *
	 * @return string
	 * @throws WrongNumberArgumentsException
	 */
	static public function escapeContent( array $args, array $named ) {
		if ( count( $args ) !== 2 ) {
			throw new WrongNumberArgumentsException( $args, 'two' );
		}
		list( $contentType, $content ) = $args;
		return in_array( $contentType, array( 'html', 'fixed-html', 'topic-title-html' ) ) ? self::html( $content ) : $content;
	}

	/**
	 * Only perform action when conditions match
	 *
	 * @param string $value
	 * @param string $operator e.g. 'or'
	 * @param string $value2 to compare with
	 * @param array $options lightncandy hbhelper options
	 *
	 * @return mixed result of callback
	 * @throws FlowException Fails when callbacks are not Closure instances
	 */
	static public function ifCond( $value, $operator, $value2, $options ) {
		$doCallback = false;

		// Perform operator
		// FIXME: Rename to || to be consistent with other operators
		if ( $operator === 'or' ) {
			if ( $value || $value2 ) {
				$doCallback = true;
			}
		} elseif ( $operator === '===' ) {
			if ( $value === $value2 ) {
				$doCallback = true;
			}
		} elseif ( $operator === '!==' ) {
			if ( $value !== $value2 ) {
				$doCallback = true;
			}
		} else {
			return '';
		}

		if ( $doCallback ) {
			$fn = $options['fn'];
			if ( !$fn instanceof Closure ) {
				throw new FlowException( 'Expected callback to be Closure instance' );
			}
			return $fn();
		} elseif ( isset( $options['inverse'] ) ) {
			$inverse = $options['inverse'];
			if ( !$inverse instanceof Closure ) {
				throw new FlowException( 'Expected inverse callback to be Closure instance' );
			}
			return $inverse();
		} else {
			return '';
		}
	}

	/**
	 * @param array $options
	 *
	 * @return string tooltip
	 */
	static public function tooltip( $options ) {
		$fn = $options['fn'];
		$params = $options['hash'];

		return (
			self::processTemplate( 'flow_tooltip', array(
				'positionClass' => $params['positionClass'] ? 'flow-ui-tooltip-' . $params['positionClass'] : null,
				'contextClass' => $params['contextClass'] ? 'mw-ui-' . $params['contextClass'] : null,
				'extraClass' => $params['extraClass'] ?: '',
				'blockClass' => $params['isBlock'] ? 'flow-ui-tooltip-block' : null,
				'content' => $fn(),
			) )
		);
	}

	/**
	 * Adds required resource and protection for patrolling link.
	 */
	static public function enablePatrollingLink() {
		$outputPage = RequestContext::getMain()->getOutput();

		$outputPage->preventClickjacking();
		$outputPage->addModules( 'mediawiki.page.patrol.ajax' );
	}
}