diff options
author | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
---|---|---|
committer | Yaco <franco@reevo.org> | 2020-06-04 11:01:00 -0300 |
commit | fc7369835258467bf97eb64f184b93691f9a9fd5 (patch) | |
tree | daabd60089d2dd76d9f5fb416b005fbe159c799d /www/wiki/includes/linker |
first commit
Diffstat (limited to 'www/wiki/includes/linker')
-rw-r--r-- | www/wiki/includes/linker/LinkRenderer.php | 480 | ||||
-rw-r--r-- | www/wiki/includes/linker/LinkRendererFactory.php | 95 | ||||
-rw-r--r-- | www/wiki/includes/linker/LinkTarget.php | 116 |
3 files changed, 691 insertions, 0 deletions
diff --git a/www/wiki/includes/linker/LinkRenderer.php b/www/wiki/includes/linker/LinkRenderer.php new file mode 100644 index 00000000..160d2d11 --- /dev/null +++ b/www/wiki/includes/linker/LinkRenderer.php @@ -0,0 +1,480 @@ +<?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 + * @author Kunal Mehta <legoktm@member.fsf.org> + */ +namespace MediaWiki\Linker; + +use DummyLinker; +use Hooks; +use Html; +use HtmlArmor; +use LinkCache; +use Linker; +use MediaWiki\MediaWikiServices; +use MWNamespace; +use Sanitizer; +use Title; +use TitleFormatter; + +/** + * Class that generates HTML <a> links for pages. + * + * @see https://www.mediawiki.org/wiki/Manual:LinkRenderer + * @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; + + /** + * @var int + */ + private $stubThreshold = 0; + + /** + * @var TitleFormatter + */ + private $titleFormatter; + + /** + * @var LinkCache + */ + private $linkCache; + + /** + * Whether to run the legacy Linker hooks + * + * @var bool + */ + private $runLegacyBeginHook = true; + + /** + * @param TitleFormatter $titleFormatter + * @param LinkCache $linkCache + */ + public function __construct( TitleFormatter $titleFormatter, LinkCache $linkCache ) { + $this->titleFormatter = $titleFormatter; + $this->linkCache = $linkCache; + } + + /** + * @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 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->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 ); + if ( $text !== null ) { + $realHtml = $html = HtmlArmor::getHtml( $text ); + } else { + $realHtml = $html = null; + } + 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; + } + + /** + * If you have already looked up the proper CSS classes using LinkRenderer::getLinkClasses() + * or some other method, use this to avoid looking it up again. + * + * @param LinkTarget $target + * @param string|HtmlArmor|null $text + * @param string $classes CSS classes to add + * @param array $extraAttribs + * @param array $query + * @return string + */ + public function makePreloadedLink( + LinkTarget $target, $text = null, $classes, 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 = [ 'class' => $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 makeKnownLink( + LinkTarget $target, $text = null, array $extraAttribs = [], array $query = [] + ) { + $classes = []; + if ( $target->isExternal() ) { + $classes[] = 'extiw'; + } + $colour = $this->getLinkClasses( $target ); + if ( $colour !== '' ) { + $classes[] = $colour; + } + + return $this->makePreloadedLink( + $target, + $text, + $classes ? implode( ' ', $classes ) : '', + $extraAttribs, + $query + ); + } + + /** + * @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 = [ '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 ); + if ( $this->forceArticlePath ) { + $realQuery = $query; + $query = []; + } else { + $realQuery = []; + } + $url = $title->getLinkURL( $query, false, $this->expandUrls ); + + 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; + } + + /** + * Return the CSS classes of a known link + * + * @param LinkTarget $target + * @return string CSS class + */ + public function getLinkClasses( LinkTarget $target ) { + // Make sure the target is in the cache + $id = $this->linkCache->addLinkObj( $target ); + if ( $id == 0 ) { + // Doesn't exist + return ''; + } + + if ( $this->linkCache->getGoodLinkFieldObj( $target, 'redirect' ) ) { + # Page is a redirect + return 'mw-redirect'; + } elseif ( $this->stubThreshold > 0 && MWNamespace::isContent( $target->getNamespace() ) + && $this->linkCache->getGoodLinkFieldObj( $target, 'length' ) < $this->stubThreshold + ) { + # Page is a stub + return 'stub'; + } + + return ''; + } +} diff --git a/www/wiki/includes/linker/LinkRendererFactory.php b/www/wiki/includes/linker/LinkRendererFactory.php new file mode 100644 index 00000000..240ea09b --- /dev/null +++ b/www/wiki/includes/linker/LinkRendererFactory.php @@ -0,0 +1,95 @@ +<?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 + * @author Kunal Mehta <legoktm@member.fsf.org> + */ +namespace MediaWiki\Linker; + +use LinkCache; +use TitleFormatter; +use User; + +/** + * Factory to create LinkRender objects + * @since 1.28 + */ +class LinkRendererFactory { + + /** + * @var TitleFormatter + */ + private $titleFormatter; + + /** + * @var LinkCache + */ + private $linkCache; + + /** + * @param TitleFormatter $titleFormatter + * @param LinkCache $linkCache + */ + public function __construct( TitleFormatter $titleFormatter, LinkCache $linkCache ) { + $this->titleFormatter = $titleFormatter; + $this->linkCache = $linkCache; + } + + /** + * @return LinkRenderer + */ + public function create() { + return new LinkRenderer( $this->titleFormatter, $this->linkCache ); + } + + /** + * @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( '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; + } +} diff --git a/www/wiki/includes/linker/LinkTarget.php b/www/wiki/includes/linker/LinkTarget.php new file mode 100644 index 00000000..56407aec --- /dev/null +++ b/www/wiki/includes/linker/LinkTarget.php @@ -0,0 +1,116 @@ +<?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 + * @author Addshore + */ +namespace MediaWiki\Linker; + +/** + * @since 1.27 + */ +interface LinkTarget { + + /** + * Get the namespace index. + * @since 1.27 + * + * @return int Namespace index + */ + public function getNamespace(); + + /** + * Convenience function to test if it is in the namespace + * @since 1.27 + * + * @param int $ns + * @return bool + */ + public function inNamespace( $ns ); + + /** + * Get the link fragment (i.e. the bit after the #) in text form. + * @since 1.27 + * + * @return string link fragment + */ + public function getFragment(); + + /** + * Whether the link target has a fragment + * @since 1.27 + * + * @return bool + */ + public function hasFragment(); + + /** + * Get the main part with underscores. + * @since 1.27 + * + * @return string Main part of the link, with underscores (for use in href attributes) + */ + public function getDBkey(); + + /** + * Returns the link in text form, without namespace prefix or fragment. + * This is computed from the DB key by replacing any underscores with spaces. + * @since 1.27 + * + * @return string + */ + public function getText(); + + /** + * Creates a new LinkTarget for a different fragment of the same page. + * It is expected that the same type of object will be returned, but the + * only requirement is that it is a LinkTarget. + * @since 1.27 + * + * @param string $fragment The fragment name, or "" for the entire page. + * + * @return LinkTarget + */ + public function createFragmentTarget( $fragment ); + + /** + * Whether this LinkTarget has an interwiki component + * @since 1.27 + * + * @return bool + */ + public function isExternal(); + + /** + * The interwiki component of this LinkTarget + * @since 1.27 + * + * @return string + */ + public function getInterwiki(); + + /** + * Returns an informative human readable representation of the link target, + * for use in logging and debugging. There is no requirement for the return + * value to have any relationship with the input of TitleParser. + * @since 1.31 + * + * @return string + */ + public function __toString(); + +} |