Merge "Add LinkRenderer (rewrite of Linker::link())"

This commit is contained in:
jenkins-bot 2016-05-24 03:29:32 +00:00 committed by Gerrit Code Review
commit 8b9584646b
13 changed files with 945 additions and 170 deletions

View file

@ -557,6 +557,7 @@ $wgAutoloadLocalClasses = [
'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',
@ -834,6 +835,8 @@ $wgAutoloadLocalClasses = [
'MediaWiki\\Interwiki\\InterwikiLookup' => __DIR__ . '/includes/interwiki/InterwikiLookup.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',

View file

@ -1783,7 +1783,8 @@ $title: The page's Title.
$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.
@ -1800,7 +1801,8 @@ $target: the Title that the link is pointing to
&$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.
@ -1835,6 +1837,35 @@ $file: the File object or false if broken link
&$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

View file

@ -20,6 +20,7 @@
* @file
*/
use MediaWiki\Linker\LinkTarget;
use MediaWiki\MediaWikiServices;
/**
* Some internal bits split of from Skin.php. These functions are used
@ -210,55 +211,33 @@ class Linker {
wfDeprecated( __METHOD__ . ' with parameter $query as string (should be array)', '1.20' );
$query = wfCgiToArray( $query );
}
$services = MediaWikiServices::getInstance();
$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';
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 = [];
if ( $html !== null ) {
$text = new HtmlArmor( $html );
} else {
$text = $html; // null
}
# 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 );
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 );
}
$attribs = array_merge(
$attribs,
self::linkAttribs( $target, $customAttribs, $options )
);
if ( is_null( $html ) ) {
$html = self::linkText( $target );
}
$ret = null;
if ( Hooks::run( 'LinkEnd', [ $dummy, $target, $options, &$html, &$attribs, &$ret ] ) ) {
$ret = Html::rawElement( 'a', $attribs, $html );
}
return $ret;
}
/**
@ -274,130 +253,6 @@ class Linker {
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

View file

@ -11,6 +11,8 @@ use LBFactory;
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;
@ -516,6 +518,25 @@ class MediaWikiServices extends ServiceContainer {
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

View file

@ -38,6 +38,8 @@
*/
use MediaWiki\Interwiki\ClassicInterwikiLookup;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\Linker\LinkRendererFactory;
use MediaWiki\MediaWikiServices;
return [
@ -159,6 +161,18 @@ 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();
},

View file

@ -0,0 +1,56 @@
<?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 );
}
}
}

View file

@ -0,0 +1,451 @@
<?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;
}
}

View file

@ -0,0 +1,92 @@
<?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;
}
}

View file

@ -342,7 +342,8 @@ class ParserTest {
$services->resetServiceForTesting( 'TitleFormatter' );
$services->resetServiceForTesting( 'TitleParser' );
$services->resetServiceForTesting( '_MediaWikiTitleCodec' );
$services->resetServiceForTesting( 'LinkRenderer' );
$services->resetServiceForTesting( 'LinkRendererFactory' );
}
public function setupRecorder( $options ) {

View file

@ -1,6 +1,8 @@
<?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;
@ -315,6 +317,8 @@ class MediaWikiServicesTest extends MediaWikiTestCase {
'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 ],

View file

@ -0,0 +1,34 @@
<?php
/**
* @covers HtmlArmor
*/
class HtmlArmorTest extends PHPUnit_Framework_TestCase {
public static function provideHtmlArmor() {
return [
[
'foobar',
'foobar',
],
[
'<script>alert("evil!");</script>',
'&lt;script&gt;alert(&quot;evil!&quot;);&lt;/script&gt;',
],
[
new HtmlArmor( '<script>alert("evil!");</script>' ),
'<script>alert("evil!");</script>',
],
];
}
/**
* @dataProvider provideHtmlArmor
*/
public function testHtmlArmor( $input, $expected ) {
$this->assertEquals(
$expected,
HtmlArmor::getHtml( $input )
);
}
}

View file

@ -0,0 +1,79 @@
<?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() );
}
}

View file

@ -0,0 +1,134 @@
<?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&amp;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&amp;action=edit&amp;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&amp;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&amp;action=edit&amp;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)">&lt;script&gt;evil()&lt;/script&gt;</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>' ) )
);
}
}