'HistoryPager' => __DIR__ . '/includes/actions/HistoryAction.php',
'Hooks' => __DIR__ . '/includes/Hooks.php',
'Html' => __DIR__ . '/includes/Html.php',
+ 'HtmlArmor' => __DIR__ . '/includes/libs/HtmlArmor.php',
'HtmlFormatter' => __DIR__ . '/includes/HtmlFormatter.php',
'Http' => __DIR__ . '/includes/HttpFunctions.php',
'HttpError' => __DIR__ . '/includes/exception/HttpError.php',
'MediaWiki\\Diff\\WordAccumulator' => __DIR__ . '/includes/diff/WordAccumulator.php',
'MediaWiki\\Languages\\Data\\Names' => __DIR__ . '/languages/data/Names.php',
'MediaWiki\\Languages\\Data\\ZhConversion' => __DIR__ . '/languages/data/ZhConversion.php',
+ 'MediaWiki\\Linker\\LinkRenderer' => __DIR__ . '/includes/linker/LinkRenderer.php',
+ 'MediaWiki\\Linker\\LinkRendererFactory' => __DIR__ . '/includes/linker/LinkRendererFactory.php',
'MediaWiki\\Linker\\LinkTarget' => __DIR__ . '/includes/linker/LinkTarget.php',
'MediaWiki\\Logger\\LegacyLogger' => __DIR__ . '/includes/debug/logger/LegacyLogger.php',
'MediaWiki\\Logger\\LegacySpi' => __DIR__ . '/includes/debug/logger/LegacySpi.php',
$out: The output page.
$cssClassName: CSS class name of the language selector.
-'LinkBegin': Used when generating internal and interwiki links in
+'LinkBegin': DEPRECATED! Use HtmlPageLinkRendererBegin instead.
+Used when generating internal and interwiki links in
Linker::link(), before processing starts. Return false to skip default
processing and return $ret. See documentation for Linker::link() for details on
the expected meanings of parameters.
&$options: array of options. Can include 'known', 'broken', 'noclasses'.
&$ret: the value to return if your hook returns false.
-'LinkEnd': Used when generating internal and interwiki links in Linker::link(),
+'LinkEnd': DEPRECATED! Use HtmlPageLinkRendererEnd hook instead
+Used when generating internal and interwiki links in Linker::link(),
just before the function returns a value. If you return true, an <a> element
with HTML attributes $attribs and contents $html will be returned. If you
return false, $ret will be returned.
&$attribs: the attributes to be applied
&$ret: the value to return if your hook returns false
+'LinkRendererBegin':
+Used when generating internal and interwiki links in
+LinkRenderer, before processing starts. Return false to skip default
+processing and return $ret.
+$linkRenderer: the LinkRenderer object
+$target: the LinkTarget that the link is pointing to
+&$html: the contents that the <a> tag should have (raw HTML); null means
+ "default".
+&$customAttribs: the HTML attributes that the <a> tag should have, in
+ associative array form, with keys and values unescaped. Should be merged
+ with default values, with a value of false meaning to suppress the
+ attribute.
+&$query: the query string to add to the generated URL (the bit after the "?"),
+ in associative array form, with keys and values unescaped.
+&$ret: the value to return if your hook returns false.
+
+'LinkRendererEnd':
+Used when generating internal and interwiki links in LinkRenderer,
+just before the function returns a value. If you return true, an <a> element
+with HTML attributes $attribs and contents $html will be returned. If you
+return false, $ret will be returned.
+$linkRenderer: the LinkRenderer object
+$target: the LinkTarget object that the link is pointing to
+$isKnown: boolean indicating whether the page is known or not
+&$html: the final (raw HTML) contents of the <a> tag, after processing.
+&$attribs: the final HTML attributes of the <a> tag, after processing, in
+ associative array form.
+&$ret: the value to return if your hook returns false.
+
'LinksUpdate': At the beginning of LinksUpdate::doUpdate() just before the
actual update.
&$linksUpdate: the LinksUpdate object
* @file
*/
use MediaWiki\Linker\LinkTarget;
+use MediaWiki\MediaWikiServices;
/**
* Some internal bits split of from Skin.php. These functions are used
wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
$query = wfCgiToArray( $query );
}
- $options = (array)$options;
-
- $dummy = new DummyLinker; // dummy linker instance for bc on the hooks
- $ret = null;
- if ( !Hooks::run( 'LinkBegin',
- [ $dummy, $target, &$html, &$customAttribs, &$query, &$options, &$ret ] )
- ) {
- return $ret;
- }
-
- # Normalize the Title if it's a special page
- $target = self::normaliseSpecialPage( $target );
-
- # If we don't know whether the page exists, let's find out.
- if ( !in_array( 'known', $options, true ) && !in_array( 'broken', $options, true ) ) {
- if ( $target->isKnown() ) {
- $options[] = 'known';
- } else {
- $options[] = 'broken';
+ $services = MediaWikiServices::getInstance();
+ $options = (array)$options;
+ if ( $options ) {
+ // Custom options, create new LinkRenderer
+ if ( !isset( $options['stubThreshold'] ) ) {
+ global $wgUser;
+ $options['stubThreshold'] = $wgUser->getStubThreshold();
}
+ $linkRenderer = $services->getLinkRendererFactory()
+ ->createFromLegacyOptions( $options );
+ } else {
+ $linkRenderer = $services->getLinkRenderer();
}
- $oldquery = [];
- if ( in_array( "forcearticlepath", $options, true ) && $query ) {
- $oldquery = $query;
- $query = [];
- }
-
- # Note: we want the href attribute first, for prettiness.
- $attribs = [ 'href' => self::linkUrl( $target, $query, $options ) ];
- if ( in_array( 'forcearticlepath', $options, true ) && $oldquery ) {
- $attribs['href'] = wfAppendQuery( $attribs['href'], $oldquery );
- }
-
- $attribs = array_merge(
- $attribs,
- self::linkAttribs( $target, $customAttribs, $options )
- );
- if ( is_null( $html ) ) {
- $html = self::linkText( $target );
+ if ( $html !== null ) {
+ $text = new HtmlArmor( $html );
+ } else {
+ $text = $html; // null
}
-
- $ret = null;
- if ( Hooks::run( 'LinkEnd', [ $dummy, $target, $options, &$html, &$attribs, &$ret ] ) ) {
- $ret = Html::rawElement( 'a', $attribs, $html );
+ if ( in_array( 'known', $options, true ) ) {
+ return $linkRenderer->makeKnownLink( $target, $text, $customAttribs, $query );
+ } elseif ( in_array( 'broken', $options, true ) ) {
+ return $linkRenderer->makeBrokenLink( $target, $text, $customAttribs, $query );
+ } else {
+ return $linkRenderer->makeLink( $target, $text, $customAttribs, $query );
}
-
- return $ret;
}
/**
return self::link( $target, $html, $customAttribs, $query, $options );
}
- /**
- * Returns the Url used to link to a Title
- *
- * @param LinkTarget $target
- * @param array $query Query parameters
- * @param array $options
- * @return string
- */
- private static function linkUrl( LinkTarget $target, $query, $options ) {
- # We don't want to include fragments for broken links, because they
- # generally make no sense.
- if ( in_array( 'broken', $options, true ) && $target->hasFragment() ) {
- $target = $target->createFragmentTarget( '' );
- }
-
- # If it's a broken link, add the appropriate query pieces, unless
- # there's already an action specified, or unless 'edit' makes no sense
- # (i.e., for a nonexistent special page).
- if ( in_array( 'broken', $options, true ) && empty( $query['action'] )
- && $target->getNamespace() !== NS_SPECIAL ) {
- $query['action'] = 'edit';
- $query['redlink'] = '1';
- }
-
- if ( in_array( 'http', $options, true ) ) {
- $proto = PROTO_HTTP;
- } elseif ( in_array( 'https', $options, true ) ) {
- $proto = PROTO_HTTPS;
- } else {
- $proto = PROTO_RELATIVE;
- }
-
- $title = Title::newFromLinkTarget( $target );
- $ret = $title->getLinkURL( $query, false, $proto );
- return $ret;
- }
-
- /**
- * Returns the array of attributes used when linking to the Title $target
- *
- * @param Title $target
- * @param array $attribs
- * @param array $options
- *
- * @return array
- */
- private static function linkAttribs( $target, $attribs, $options ) {
- global $wgUser;
- $defaults = [];
-
- if ( !in_array( 'noclasses', $options, true ) ) {
- # Now build the classes.
- $classes = [];
-
- if ( in_array( 'broken', $options, true ) ) {
- $classes[] = 'new';
- }
-
- if ( $target->isExternal() ) {
- $classes[] = 'extiw';
- }
-
- if ( !in_array( 'broken', $options, true ) ) { # Avoid useless calls to LinkCache (see r50387)
- $colour = self::getLinkColour(
- $target,
- isset( $options['stubThreshold'] ) ? $options['stubThreshold'] : $wgUser->getStubThreshold()
- );
- if ( $colour !== '' ) {
- $classes[] = $colour; # mw-redirect or stub
- }
- }
- if ( $classes != [] ) {
- $defaults['class'] = implode( ' ', $classes );
- }
- }
-
- # Get a default title attribute.
- if ( $target->getPrefixedText() == '' ) {
- # A link like [[#Foo]]. This used to mean an empty title
- # attribute, but that's silly. Just don't output a title.
- } elseif ( in_array( 'known', $options, true ) ) {
- $defaults['title'] = $target->getPrefixedText();
- } else {
- // This ends up in parser cache!
- $defaults['title'] = wfMessage( 'red-link-title', $target->getPrefixedText() )
- ->inContentLanguage()
- ->text();
- }
-
- # Finally, merge the custom attribs with the default ones, and iterate
- # over that, deleting all "false" attributes.
- $ret = [];
- $merged = Sanitizer::mergeAttributes( $defaults, $attribs );
- foreach ( $merged as $key => $val ) {
- # A false value suppresses the attribute, and we don't want the
- # href attribute to be overridden.
- if ( $key != 'href' && $val !== false ) {
- $ret[$key] = $val;
- }
- }
- return $ret;
- }
-
- /**
- * Default text of the links to the Title $target
- *
- * @param Title $target
- *
- * @return string
- */
- private static function linkText( $target ) {
- if ( !$target instanceof Title ) {
- wfWarn( __METHOD__ . ': Requires $target to be a Title object.' );
- return '';
- }
- // If the target is just a fragment, with no title, we return the fragment
- // text. Otherwise, we return the title text itself.
- if ( $target->getPrefixedText() === '' && $target->hasFragment() ) {
- return htmlspecialchars( $target->getFragment() );
- }
-
- return htmlspecialchars( $target->getPrefixedText() );
- }
-
/**
* Make appropriate markup for a link to the current article. This is
* currently rendered as the bold link text. The calling sequence is the
use LinkCache;
use Liuggio\StatsdClient\Factory\StatsdDataFactory;
use LoadBalancer;
+use MediaWiki\Linker\LinkRenderer;
+use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\Services\SalvageableService;
use MediaWiki\Services\ServiceContainer;
use MWException;
return $this->getService( 'LinkCache' );
}
+ /**
+ * @since 1.28
+ * @return LinkRendererFactory
+ */
+ public function getLinkRendererFactory() {
+ return $this->getService( 'LinkRendererFactory' );
+ }
+
+ /**
+ * LinkRenderer instance that can be used
+ * if no custom options are needed
+ *
+ * @since 1.28
+ * @return LinkRenderer
+ */
+ public function getLinkRenderer() {
+ return $this->getService( 'LinkRenderer' );
+ }
+
/**
* @since 1.28
* @return TitleFormatter
*/
use MediaWiki\Interwiki\ClassicInterwikiLookup;
+use MediaWiki\Linker\LinkRenderer;
+use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\MediaWikiServices;
return [
);
},
+ 'LinkRendererFactory' => function( MediaWikiServices $services ) {
+ return new LinkRendererFactory(
+ $services->getTitleFormatter()
+ );
+ },
+
+ 'LinkRenderer' => function( MediaWikiServices $services ) {
+ global $wgUser;
+
+ return $services->getLinkRendererFactory()->createForUser( $wgUser );
+ },
+
'GenderCache' => function( MediaWikiServices $services ) {
return new GenderCache();
},
--- /dev/null
+<?php
+/**
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @license GPL-2.0+
+ * @author Kunal Mehta <legoktm@member.fsf.org>
+ */
+
+/**
+ * Marks HTML that shouldn't be escaped
+ *
+ * @since 1.28
+ */
+class HtmlArmor {
+
+ /**
+ * @var string
+ */
+ private $value;
+
+ /**
+ * @param string $value
+ */
+ public function __construct( $value ) {
+ $this->value = $value;
+ }
+
+ /**
+ * Provide a string or HtmlArmor object
+ * and get safe HTML back
+ *
+ * @param string|HtmlArmor $input
+ * @return string safe for usage in HTML
+ */
+ public static function getHtml( $input ) {
+ if ( $input instanceof HtmlArmor ) {
+ return $input->value;
+ } else {
+ return htmlspecialchars( $input );
+ }
+ }
+}
--- /dev/null
+<?php
+/**
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @license GPL-2.0+
+ * @author Kunal Mehta <legoktm@member.fsf.org>
+ */
+namespace MediaWiki\Linker;
+
+use DummyLinker;
+use Hooks;
+use Html;
+use HtmlArmor;
+use Linker;
+use MediaWiki\MediaWikiServices;
+use Sanitizer;
+use Title;
+use TitleFormatter;
+
+/**
+ * Class that generates HTML <a> links for pages.
+ *
+ * @since 1.28
+ */
+class LinkRenderer {
+
+ /**
+ * Whether to force the pretty article path
+ *
+ * @var bool
+ */
+ private $forceArticlePath = false;
+
+ /**
+ * A PROTO_* constant or false
+ *
+ * @var string|bool|int
+ */
+ private $expandUrls = false;
+
+ /**
+ * Whether extra classes should be added
+ *
+ * @var bool
+ */
+ private $noClasses = false;
+
+ /**
+ * @var int
+ */
+ private $stubThreshold = 0;
+
+ /**
+ * @var TitleFormatter
+ */
+ private $titleFormatter;
+
+ /**
+ * Whether to run the legacy Linker hooks
+ *
+ * @var bool
+ */
+ private $runLegacyBeginHook = true;
+
+ /**
+ * @param TitleFormatter $titleFormatter
+ */
+ public function __construct( TitleFormatter $titleFormatter ) {
+ $this->titleFormatter = $titleFormatter;
+ }
+
+ /**
+ * @param bool $force
+ */
+ public function setForceArticlePath( $force ) {
+ $this->forceArticlePath = $force;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getForceArticlePath() {
+ return $this->forceArticlePath;
+ }
+
+ /**
+ * @param string|bool|int $expand A PROTO_* constant or false
+ */
+ public function setExpandURLs( $expand ) {
+ $this->expandUrls = $expand;
+ }
+
+ /**
+ * @return string|bool|int a PROTO_* constant or false
+ */
+ public function getExpandURLs() {
+ return $this->expandUrls;
+ }
+
+ /**
+ * @param bool $no
+ */
+ public function setNoClasses( $no ) {
+ $this->noClasses = $no;
+ }
+
+ /**
+ * @return bool
+ */
+ public function getNoClasses() {
+ return $this->noClasses;
+ }
+
+ /**
+ * @param int $threshold
+ */
+ public function setStubThreshold( $threshold ) {
+ $this->stubThreshold = $threshold;
+ }
+
+ /**
+ * @return int
+ */
+ public function getStubThreshold() {
+ return $this->stubThreshold;
+ }
+
+ /**
+ * @param bool $run
+ */
+ public function setRunLegacyBeginHook( $run ) {
+ $this->runLegacyBeginHook = $run;
+ }
+
+ /**
+ * @param LinkTarget $target
+ * @param string|HtmlArmor|null $text
+ * @param array $extraAttribs
+ * @param array $query
+ * @return string
+ */
+ public function makeLink(
+ LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
+ ) {
+ $title = Title::newFromLinkTarget( $target );
+ if ( $title->isKnown() ) {
+ return $this->makeKnownLink( $target, $text, $extraAttribs, $query );
+ } else {
+ return $this->makeBrokenLink( $target, $text, $extraAttribs, $query );
+ }
+ }
+
+ /**
+ * Get the options in the legacy format
+ *
+ * @param bool $isKnown Whether the link is known or broken
+ * @return array
+ */
+ private function getLegacyOptions( $isKnown ) {
+ $options = [ 'stubThreshold' => $this->stubThreshold ];
+ if ( $this->noClasses ) {
+ $options[] = 'noclasses';
+ }
+ if ( $this->forceArticlePath ) {
+ $options[] = 'forcearticlepath';
+ }
+ if ( $this->expandUrls === PROTO_HTTP ) {
+ $options[] = 'http';
+ } elseif ( $this->expandUrls === PROTO_HTTPS ) {
+ $options[] = 'https';
+ }
+
+ $options[] = $isKnown ? 'known' : 'broken';
+
+ return $options;
+ }
+
+ private function runBeginHook( LinkTarget $target, &$text, &$extraAttribs, &$query, $isKnown ) {
+ $ret = null;
+ if ( !Hooks::run( 'HtmlPageLinkRendererBegin',
+ [ $this, $target, &$text, &$extraAttribs, &$query, &$ret ] )
+ ) {
+ return $ret;
+ }
+
+ // Now run the legacy hook
+ return $this->runLegacyBeginHook( $target, $text, $extraAttribs, $query, $isKnown );
+ }
+
+ private function runLegacyBeginHook( LinkTarget $target, &$text, &$extraAttribs, &$query,
+ $isKnown
+ ) {
+ if ( !$this->runLegacyBeginHook || !Hooks::isRegistered( 'LinkBegin' ) ) {
+ // Disabled, or nothing registered
+ return null;
+ }
+
+ $realOptions = $options = $this->getLegacyOptions( $isKnown );
+ $ret = null;
+ $dummy = new DummyLinker();
+ $title = Title::newFromLinkTarget( $target );
+ $realHtml = $html = HtmlArmor::getHtml( $text );
+ if ( !Hooks::run( 'LinkBegin',
+ [ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ] )
+ ) {
+ return $ret;
+ }
+
+ if ( $html !== null && $html !== $realHtml ) {
+ // &$html was modified, so re-armor it as $text
+ $text = new HtmlArmor( $html );
+ }
+
+ // Check if they changed any of the options, hopefully not!
+ if ( $options !== $realOptions ) {
+ $factory = MediaWikiServices::getInstance()->getLinkRendererFactory();
+ // They did, so create a separate instance and have that take over the rest
+ $newRenderer = $factory->createFromLegacyOptions( $options );
+ // Don't recurse the hook...
+ $newRenderer->setRunLegacyBeginHook( false );
+ if ( in_array( 'known', $options, true ) ) {
+ return $newRenderer->makeKnownLink( $title, $text, $extraAttribs, $query );
+ } elseif ( in_array( 'broken', $options, true ) ) {
+ return $newRenderer->makeBrokenLink( $title, $text, $extraAttribs, $query );
+ } else {
+ return $newRenderer->makeLink( $title, $text, $extraAttribs, $query );
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param LinkTarget $target
+ * @param string|HtmlArmor|null $text
+ * @param array $extraAttribs
+ * @param array $query
+ * @return string
+ */
+ public function makeKnownLink(
+ LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
+ ) {
+ // Run begin hook
+ $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, true );
+ if ( $ret !== null ) {
+ return $ret;
+ }
+ $target = $this->normalizeTarget( $target );
+ $url = $this->getLinkURL( $target, $query );
+ $attribs = [];
+ if ( !$this->noClasses ) {
+ $classes = [];
+ if ( $target->isExternal() ) {
+ $classes[] = 'extiw';
+ }
+ $title = Title::newFromLinkTarget( $target );
+ $colour = Linker::getLinkColour( $title, $this->stubThreshold );
+ if ( $colour !== '' ) {
+ $classes[] = $colour;
+ }
+ if ( $classes ) {
+ $attribs['class'] = implode( ' ', $classes );
+ }
+ }
+
+ $prefixedText = $this->titleFormatter->getPrefixedText( $target );
+ if ( $prefixedText !== '' ) {
+ $attribs['title'] = $prefixedText;
+ }
+
+ $attribs = [
+ 'href' => $url,
+ ] + $this->mergeAttribs( $attribs, $extraAttribs );
+
+ if ( $text === null ) {
+ $text = $this->getLinkText( $target );
+ }
+
+ return $this->buildAElement( $target, $text, $attribs, true );
+ }
+
+ /**
+ * @param LinkTarget $target
+ * @param string|HtmlArmor|null $text
+ * @param array $extraAttribs
+ * @param array $query
+ * @return string
+ */
+ public function makeBrokenLink(
+ LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
+ ) {
+ // Run legacy hook
+ $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, false );
+ if ( $ret !== null ) {
+ return $ret;
+ }
+
+ # We don't want to include fragments for broken links, because they
+ # generally make no sense.
+ if ( $target->hasFragment() ) {
+ $target = $target->createFragmentTarget( '' );
+ }
+ $target = $this->normalizeTarget( $target );
+
+ if ( !isset( $query['action'] ) && $target->getNamespace() !== NS_SPECIAL ) {
+ $query['action'] = 'edit';
+ $query['redlink'] = '1';
+ }
+
+ $url = $this->getLinkURL( $target, $query );
+ $attribs = $this->noClasses ? [] : [ 'class' => 'new' ];
+ $prefixedText = $this->titleFormatter->getPrefixedText( $target );
+ if ( $prefixedText !== '' ) {
+ // This ends up in parser cache!
+ $attribs['title'] = wfMessage( 'red-link-title', $prefixedText )
+ ->inContentLanguage()
+ ->text();
+ }
+
+ $attribs = [
+ 'href' => $url,
+ ] + $this->mergeAttribs( $attribs, $extraAttribs );
+
+ if ( $text === null ) {
+ $text = $this->getLinkText( $target );
+ }
+
+ return $this->buildAElement( $target, $text, $attribs, false );
+ }
+
+ /**
+ * Builds the final <a> element
+ *
+ * @param LinkTarget $target
+ * @param string|HtmlArmor $text
+ * @param array $attribs
+ * @param bool $isKnown
+ * @return null|string
+ */
+ private function buildAElement( LinkTarget $target, $text, array $attribs, $isKnown ) {
+ $ret = null;
+ if ( !Hooks::run( 'HtmlPageLinkRendererEnd',
+ [ $this, $target, $isKnown, &$text, &$attribs, &$ret ] )
+ ) {
+ return $ret;
+ }
+
+ $html = HtmlArmor::getHtml( $text );
+
+ // Run legacy hook
+ if ( Hooks::isRegistered( 'LinkEnd' ) ) {
+ $dummy = new DummyLinker();
+ $title = Title::newFromLinkTarget( $target );
+ $options = $this->getLegacyOptions( $isKnown );
+ if ( !Hooks::run( 'LinkEnd',
+ [ $dummy, $title, $options, &$html, &$attribs, &$ret ] )
+ ) {
+ return $ret;
+ }
+ }
+
+ return Html::rawElement( 'a', $attribs, $html );
+ }
+
+ /**
+ * @param LinkTarget $target
+ * @return string non-escaped text
+ */
+ private function getLinkText( LinkTarget $target ) {
+ $prefixedText = $this->titleFormatter->getPrefixedText( $target );
+ // If the target is just a fragment, with no title, we return the fragment
+ // text. Otherwise, we return the title text itself.
+ if ( $prefixedText === '' && $target->hasFragment() ) {
+ return $target->getFragment();
+ }
+
+ return $prefixedText;
+ }
+
+ private function getLinkURL( LinkTarget $target, array $query = [] ) {
+ // TODO: Use a LinkTargetResolver service instead of Title
+ $title = Title::newFromLinkTarget( $target );
+ $proto = $this->expandUrls !== false
+ ? $this->expandUrls
+ : PROTO_RELATIVE;
+ if ( $this->forceArticlePath ) {
+ $realQuery = $query;
+ $query = [];
+ } else {
+ $realQuery = [];
+ }
+ $url = $title->getLinkURL( $query, false, $proto );
+
+ if ( $this->forceArticlePath && $realQuery ) {
+ $url = wfAppendQuery( $url, $realQuery );
+ }
+
+ return $url;
+ }
+
+ /**
+ * Normalizes the provided target
+ *
+ * @todo move the code from Linker actually here
+ * @param LinkTarget $target
+ * @return LinkTarget
+ */
+ private function normalizeTarget( LinkTarget $target ) {
+ return Linker::normaliseSpecialPage( $target );
+ }
+
+ /**
+ * Merges two sets of attributes
+ *
+ * @param array $defaults
+ * @param array $attribs
+ *
+ * @return array
+ */
+ private function mergeAttribs( $defaults, $attribs ) {
+ if ( !$attribs ) {
+ return $defaults;
+ }
+ # Merge the custom attribs with the default ones, and iterate
+ # over that, deleting all "false" attributes.
+ $ret = [];
+ $merged = Sanitizer::mergeAttributes( $defaults, $attribs );
+ foreach ( $merged as $key => $val ) {
+ # A false value suppresses the attribute
+ if ( $val !== false ) {
+ $ret[$key] = $val;
+ }
+ }
+ return $ret;
+ }
+
+}
--- /dev/null
+<?php
+/**
+ * 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.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @license GPL-2.0+
+ * @author Kunal Mehta <legoktm@member.fsf.org>
+ */
+namespace MediaWiki\Linker;
+
+use TitleFormatter;
+use User;
+
+/**
+ * Factory to create LinkRender objects
+ * @since 1.28
+ */
+class LinkRendererFactory {
+
+ /**
+ * @var TitleFormatter
+ */
+ private $titleFormatter;
+
+ /**
+ * @param TitleFormatter $titleFormatter
+ */
+ public function __construct( TitleFormatter $titleFormatter ) {
+ $this->titleFormatter = $titleFormatter;
+ }
+
+ /**
+ * @return LinkRenderer
+ */
+ public function create() {
+ return new LinkRenderer( $this->titleFormatter );
+ }
+
+ /**
+ * @param User $user
+ * @return LinkRenderer
+ */
+ public function createForUser( User $user ) {
+ $linkRenderer = $this->create();
+ $linkRenderer->setStubThreshold( $user->getStubThreshold() );
+
+ return $linkRenderer;
+ }
+
+ /**
+ * @param array $options
+ * @return LinkRenderer
+ */
+ public function createFromLegacyOptions( array $options ) {
+ $linkRenderer = $this->create();
+
+ if ( in_array( 'noclasses', $options, true ) ) {
+ $linkRenderer->setNoClasses( true );
+ }
+
+ if ( in_array( 'forcearticlepath', $options, true ) ) {
+ $linkRenderer->setForceArticlePath( true );
+ }
+
+ if ( in_array( 'http', $options, true ) ) {
+ $linkRenderer->setExpandURLs( PROTO_HTTP );
+ } elseif ( in_array( 'https', $options, true ) ) {
+ $linkRenderer->setExpandURLs( PROTO_HTTPS );
+ }
+
+ if ( isset( $options['stubThreshold'] ) ) {
+ $linkRenderer->setStubThreshold(
+ $options['stubThreshold']
+ );
+ }
+
+ return $linkRenderer;
+ }
+}
$services->resetServiceForTesting( 'TitleFormatter' );
$services->resetServiceForTesting( 'TitleParser' );
$services->resetServiceForTesting( '_MediaWikiTitleCodec' );
-
+ $services->resetServiceForTesting( 'LinkRenderer' );
+ $services->resetServiceForTesting( 'LinkRendererFactory' );
}
public function setupRecorder( $options ) {
<?php
use Liuggio\StatsdClient\Factory\StatsdDataFactory;
use MediaWiki\Interwiki\InterwikiLookup;
+use MediaWiki\Linker\LinkRenderer;
+use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Services\DestructibleService;
use MediaWiki\Services\SalvageableService;
'WatchedItemStore' => [ 'WatchedItemStore', WatchedItemStore::class ],
'GenderCache' => [ 'GenderCache', GenderCache::class ],
'LinkCache' => [ 'LinkCache', LinkCache::class ],
+ 'LinkRenderer' => [ 'LinkRenderer', LinkRenderer::class ],
+ 'LinkRendererFactory' => [ 'LinkRendererFactory', LinkRendererFactory::class ],
'_MediaWikiTitleCodec' => [ '_MediaWikiTitleCodec', MediaWikiTitleCodec::class ],
'TitleFormatter' => [ 'TitleFormatter', TitleFormatter::class ],
'TitleParser' => [ 'TitleParser', TitleParser::class ],
--- /dev/null
+<?php
+
+/**
+ * @covers HtmlArmor
+ */
+class HtmlArmorTest extends PHPUnit_Framework_TestCase {
+
+ public static function provideHtmlArmor() {
+ return [
+ [
+ 'foobar',
+ 'foobar',
+ ],
+ [
+ '<script>alert("evil!");</script>',
+ '<script>alert("evil!");</script>',
+ ],
+ [
+ new HtmlArmor( '<script>alert("evil!");</script>' ),
+ '<script>alert("evil!");</script>',
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideHtmlArmor
+ */
+ public function testHtmlArmor( $input, $expected ) {
+ $this->assertEquals(
+ $expected,
+ HtmlArmor::getHtml( $input )
+ );
+ }
+}
--- /dev/null
+<?php
+
+use MediaWiki\Linker\LinkRenderer;
+use MediaWiki\Linker\LinkRendererFactory;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @covers LinkRendererFactory
+ */
+class LinkRendererFactoryTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var TitleFormatter
+ */
+ private $titleFormatter;
+
+ public function setUp() {
+ parent::setUp();
+ $this->titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
+ }
+
+ public static function provideCreateFromLegacyOptions() {
+ return [
+ [
+ [ 'noclasses' ],
+ 'getNoClasses',
+ true
+ ],
+ [
+ [ 'forcearticlepath' ],
+ 'getForceArticlePath',
+ true
+ ],
+ [
+ [ 'http' ],
+ 'getExpandURLs',
+ PROTO_HTTP
+ ],
+ [
+ [ 'https' ],
+ 'getExpandURLs',
+ PROTO_HTTPS
+ ],
+ [
+ [ 'stubThreshold' => 150 ],
+ 'getStubThreshold',
+ 150
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideCreateFromLegacyOptions
+ */
+ public function testCreateFromLegacyOptions( $options, $func, $val ) {
+ $factory = new LinkRendererFactory( $this->titleFormatter );
+ $linkRenderer = $factory->createFromLegacyOptions(
+ $options
+ );
+ $this->assertInstanceOf( LinkRenderer::class, $linkRenderer );
+ $this->assertEquals( $val, $linkRenderer->$func(), $func );
+ }
+
+ public function testCreate() {
+ $factory = new LinkRendererFactory( $this->titleFormatter );
+ $this->assertInstanceOf( LinkRenderer::class, $factory->create() );
+ }
+
+ public function testCreateForUser() {
+ $user = $this->getMock( User::class, [ 'getStubThreshold' ] );
+ $user->expects( $this->once() )
+ ->method( 'getStubThreshold' )
+ ->willReturn( 15 );
+ $factory = new LinkRendererFactory( $this->titleFormatter );
+ $linkRenderer = $factory->createForUser( $user );
+ $this->assertInstanceOf( LinkRenderer::class, $linkRenderer );
+ $this->assertEquals( 15, $linkRenderer->getStubThreshold() );
+ }
+}
--- /dev/null
+<?php
+
+use MediaWiki\Linker\LinkRenderer;
+use MediaWiki\MediaWikiServices;
+
+/**
+ * @covers LinkRenderer
+ */
+class LinkRendererTest extends MediaWikiLangTestCase {
+
+ /**
+ * @var TitleFormatter
+ */
+ private $titleFormatter;
+
+ public function setUp() {
+ parent::setUp();
+ $this->setMwGlobals( [
+ 'wgArticlePath' => '/wiki/$1',
+ 'wgServer' => '//example.org',
+ 'wgCanonicalServer' => 'http://example.org',
+ 'wgScriptPath' => '/w',
+ 'wgScript' => '/w/index.php',
+ ] );
+ $this->titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter();
+ }
+
+ public function testMergeAttribs() {
+ $target = new TitleValue( NS_SPECIAL, 'Blankpage' );
+ $linkRenderer = new LinkRenderer( $this->titleFormatter );
+ $link = $linkRenderer->makeBrokenLink( $target, null, [
+ // Appended to class
+ 'class' => 'foobar',
+ // Suppresses href attribute
+ 'href' => false,
+ // Extra attribute
+ 'bar' => 'baz'
+ ] );
+ $this->assertEquals(
+ '<a href="/wiki/Special:BlankPage" class="new foobar" '
+ . 'title="Special:BlankPage (page does not exist)" bar="baz">'
+ . 'Special:BlankPage</a>',
+ $link
+ );
+ }
+
+ public function testMakeKnownLink() {
+ $target = new TitleValue( NS_MAIN, 'Foobar' );
+ $linkRenderer = new LinkRenderer( $this->titleFormatter );
+
+ // Query added
+ $this->assertEquals(
+ '<a href="/w/index.php?title=Foobar&foo=bar" '. 'title="Foobar">Foobar</a>',
+ $linkRenderer->makeKnownLink( $target, null, [], [ 'foo' => 'bar' ] )
+ );
+
+ // forcearticlepath
+ $linkRenderer->setForceArticlePath( true );
+ $this->assertEquals(
+ '<a href="/wiki/Foobar?foo=bar" title="Foobar">Foobar</a>',
+ $linkRenderer->makeKnownLink( $target, null, [], [ 'foo' => 'bar' ] )
+ );
+
+ // expand = HTTPS
+ $linkRenderer->setForceArticlePath( false );
+ $linkRenderer->setExpandURLs( PROTO_HTTPS );
+ $this->assertEquals(
+ '<a href="https://example.org/wiki/Foobar" title="Foobar">Foobar</a>',
+ $linkRenderer->makeKnownLink( $target )
+ );
+ }
+
+ public function testMakeBrokenLink() {
+ $target = new TitleValue( NS_MAIN, 'Foobar' );
+ $special = new TitleValue( NS_SPECIAL, 'Foobar' );
+ $linkRenderer = new LinkRenderer( $this->titleFormatter );
+
+ // action=edit&redlink=1 added
+ $this->assertEquals(
+ '<a href="/w/index.php?title=Foobar&action=edit&redlink=1" '
+ . 'class="new" title="Foobar (page does not exist)">Foobar</a>',
+ $linkRenderer->makeBrokenLink( $target )
+ );
+
+ // action=edit&redlink=1 not added due to action query parameter
+ $this->assertEquals(
+ '<a href="/w/index.php?title=Foobar&action=foobar" class="new" '
+ . 'title="Foobar (page does not exist)">Foobar</a>',
+ $linkRenderer->makeBrokenLink( $target, null, [], [ 'action' => 'foobar' ] )
+ );
+
+ // action=edit&redlink=1 not added due to NS_SPECIAL
+ $this->assertEquals(
+ '<a href="/wiki/Special:Foobar" class="new" title="Special:Foobar '
+ . '(page does not exist)">Special:Foobar</a>',
+ $linkRenderer->makeBrokenLink( $special )
+ );
+
+ // fragment stripped
+ $this->assertEquals(
+ '<a href="/w/index.php?title=Foobar&action=edit&redlink=1" '
+ . 'class="new" title="Foobar (page does not exist)">Foobar</a>',
+ $linkRenderer->makeBrokenLink( $target->createFragmentTarget( 'foobar' ) )
+ );
+ }
+
+ public function testMakeLink() {
+ $linkRenderer = new LinkRenderer( $this->titleFormatter );
+ $foobar = new TitleValue( NS_SPECIAL, 'Foobar' );
+ $blankpage = new TitleValue( NS_SPECIAL, 'Blankpage' );
+ $this->assertEquals(
+ '<a href="/wiki/Special:Foobar" class="new" title="Special:Foobar '
+ . '(page does not exist)">foo</a>',
+ $linkRenderer->makeLink( $foobar, 'foo' )
+ );
+
+ $this->assertEquals(
+ '<a href="/wiki/Special:BlankPage" title="Special:BlankPage">blank</a>',
+ $linkRenderer->makeLink( $blankpage, 'blank' )
+ );
+
+ $this->assertEquals(
+ '<a href="/wiki/Special:Foobar" class="new" title="Special:Foobar '
+ . '(page does not exist)"><script>evil()</script></a>',
+ $linkRenderer->makeLink( $foobar, '<script>evil()</script>' )
+ );
+
+ $this->assertEquals(
+ '<a href="/wiki/Special:Foobar" class="new" title="Special:Foobar '
+ . '(page does not exist)"><script>evil()</script></a>',
+ $linkRenderer->makeLink( $foobar, new HtmlArmor( '<script>evil()</script>' ) )
+ );
+ }
+}