MediaWiki REL1_35
LinkRenderer.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Linker;
22
23use DummyLinker;
24use Html;
25use HtmlArmor;
26use LinkCache;
32use Sanitizer;
33use SpecialPage;
34use Title;
36
44
50 private $forceArticlePath = false;
51
57 private $expandUrls = false;
58
62 private $stubThreshold = 0;
63
68
72 private $linkCache;
73
77 private $nsInfo;
78
84 private $runLegacyBeginHook = true;
85
88
90 private $hookRunner;
91
96
105 public function __construct(
111 ) {
112 $this->titleFormatter = $titleFormatter;
113 $this->linkCache = $linkCache;
114 $this->nsInfo = $nsInfo;
115 $this->specialPageFactory = $specialPageFactory;
116 $this->hookContainer = $hookContainer;
117 $this->hookRunner = new HookRunner( $hookContainer );
118 }
119
123 public function setForceArticlePath( $force ) {
124 $this->forceArticlePath = $force;
125 }
126
130 public function getForceArticlePath() {
132 }
133
137 public function setExpandURLs( $expand ) {
138 $this->expandUrls = $expand;
139 }
140
144 public function getExpandURLs() {
145 return $this->expandUrls;
146 }
147
151 public function setStubThreshold( $threshold ) {
152 $this->stubThreshold = $threshold;
153 }
154
158 public function getStubThreshold() {
160 }
161
165 public function setRunLegacyBeginHook( $run ) {
166 $this->runLegacyBeginHook = $run;
167 }
168
176 public function makeLink(
177 LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
178 ) {
179 $title = Title::newFromLinkTarget( $target );
180 if ( $title->isKnown() ) {
181 return $this->makeKnownLink( $target, $text, $extraAttribs, $query );
182 } else {
183 return $this->makeBrokenLink( $target, $text, $extraAttribs, $query );
184 }
185 }
186
193 private function getLegacyOptions( $isKnown ) {
194 $options = [ 'stubThreshold' => $this->stubThreshold ];
195 if ( $this->forceArticlePath ) {
196 $options[] = 'forcearticlepath';
197 }
198 if ( $this->expandUrls === PROTO_HTTP ) {
199 $options[] = 'http';
200 } elseif ( $this->expandUrls === PROTO_HTTPS ) {
201 $options[] = 'https';
202 }
203
204 $options[] = $isKnown ? 'known' : 'broken';
205
206 return $options;
207 }
208
209 private function runBeginHook( LinkTarget $target, &$text, &$extraAttribs, &$query, $isKnown ) {
210 $ret = null;
211 if ( !$this->hookRunner->onHtmlPageLinkRendererBegin(
212 $this, $target, $text, $extraAttribs, $query, $ret )
213 ) {
214 return $ret;
215 }
216
217 // Now run the legacy hook
218 return $this->runLegacyBeginHook( $target, $text, $extraAttribs, $query, $isKnown );
219 }
220
221 private function runLegacyBeginHook( LinkTarget $target, &$text, &$extraAttribs, &$query,
222 $isKnown
223 ) {
224 if ( !$this->runLegacyBeginHook || !$this->hookContainer->isRegistered( 'LinkBegin' ) ) {
225 // Disabled, or nothing registered
226 return null;
227 }
228
229 $realOptions = $options = $this->getLegacyOptions( $isKnown );
230 $ret = null;
231 $dummy = new DummyLinker();
232 $title = Title::newFromLinkTarget( $target );
233 if ( $text !== null ) {
234 $realHtml = $html = HtmlArmor::getHtml( $text );
235 } else {
236 $realHtml = $html = null;
237 }
238 if ( !$this->hookRunner->onLinkBegin(
239 $dummy, $title, $html, $extraAttribs, $query, $options, $ret )
240 ) {
241 return $ret;
242 }
243
244 if ( $html !== null && $html !== $realHtml ) {
245 // &$html was modified, so re-armor it as $text
246 $text = new HtmlArmor( $html );
247 }
248
249 // Check if they changed any of the options, hopefully not!
250 if ( $options !== $realOptions ) {
251 $factory = MediaWikiServices::getInstance()->getLinkRendererFactory();
252 // They did, so create a separate instance and have that take over the rest
253 $newRenderer = $factory->createFromLegacyOptions( $options );
254 // Don't recurse the hook...
255 $newRenderer->setRunLegacyBeginHook( false );
256 if ( in_array( 'known', $options, true ) ) {
257 return $newRenderer->makeKnownLink( $title, $text, $extraAttribs, $query );
258 } elseif ( in_array( 'broken', $options, true ) ) {
259 return $newRenderer->makeBrokenLink( $title, $text, $extraAttribs, $query );
260 } else {
261 return $newRenderer->makeLink( $title, $text, $extraAttribs, $query );
262 }
263 }
264
265 return null;
266 }
267
279 public function makePreloadedLink(
280 LinkTarget $target, $text = null, $classes = '', array $extraAttribs = [], array $query = []
281 ) {
282 // Run begin hook
283 $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, true );
284 if ( $ret !== null ) {
285 return $ret;
286 }
287 $target = $this->normalizeTarget( $target );
288 $url = $this->getLinkURL( $target, $query );
289 $attribs = [ 'class' => $classes ];
290 $prefixedText = $this->titleFormatter->getPrefixedText( $target );
291 if ( $prefixedText !== '' ) {
292 $attribs['title'] = $prefixedText;
293 }
294
295 $attribs = [
296 'href' => $url,
297 ] + $this->mergeAttribs( $attribs, $extraAttribs );
298
299 if ( $text === null ) {
300 $text = $this->getLinkText( $target );
301 }
302
303 return $this->buildAElement( $target, $text, $attribs, true );
304 }
305
313 public function makeKnownLink(
314 LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
315 ) {
316 $classes = [];
317 if ( $target->isExternal() ) {
318 $classes[] = 'extiw';
319 }
320 $colour = $this->getLinkClasses( $target );
321 if ( $colour !== '' ) {
322 $classes[] = $colour;
323 }
324
325 return $this->makePreloadedLink(
326 $target,
327 $text,
328 implode( ' ', $classes ),
329 $extraAttribs,
330 $query
331 );
332 }
333
342 public function makeBrokenLink(
343 LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
344 ) {
345 // Run legacy hook
346 $ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, false );
347 if ( $ret !== null ) {
348 return $ret;
349 }
350
351 # We don't want to include fragments for broken links, because they
352 # generally make no sense.
353 if ( $target->hasFragment() ) {
354 $target = $target->createFragmentTarget( '' );
355 }
356 $target = $this->normalizeTarget( $target );
357
358 if ( !isset( $query['action'] ) && $target->getNamespace() !== NS_SPECIAL ) {
359 $query['action'] = 'edit';
360 $query['redlink'] = '1';
361 }
362
363 $url = $this->getLinkURL( $target, $query );
364 $attribs = [ 'class' => 'new' ];
365 $prefixedText = $this->titleFormatter->getPrefixedText( $target );
366 if ( $prefixedText !== '' ) {
367 // This ends up in parser cache!
368 $attribs['title'] = wfMessage( 'red-link-title', $prefixedText )
369 ->inContentLanguage()
370 ->text();
371 }
372
373 $attribs = [
374 'href' => $url,
375 ] + $this->mergeAttribs( $attribs, $extraAttribs );
376
377 if ( $text === null ) {
378 $text = $this->getLinkText( $target );
379 }
380
381 return $this->buildAElement( $target, $text, $attribs, false );
382 }
383
393 private function buildAElement( LinkTarget $target, $text, array $attribs, $isKnown ) {
394 $ret = null;
395 if ( !$this->hookRunner->onHtmlPageLinkRendererEnd(
396 $this, $target, $isKnown, $text, $attribs, $ret )
397 ) {
398 return $ret;
399 }
400
401 $html = HtmlArmor::getHtml( $text );
402
403 // Run legacy hook
404 if ( $this->hookContainer->isRegistered( 'LinkEnd' ) ) {
405 $dummy = new DummyLinker();
406 $title = Title::newFromLinkTarget( $target );
407 $options = $this->getLegacyOptions( $isKnown );
408 if ( !$this->hookRunner->onLinkEnd(
409 $dummy, $title, $options, $html, $attribs, $ret )
410 ) {
411 return $ret;
412 }
413 }
414
415 return Html::rawElement( 'a', $attribs, $html );
416 }
417
422 private function getLinkText( LinkTarget $target ) {
423 $prefixedText = $this->titleFormatter->getPrefixedText( $target );
424 // If the target is just a fragment, with no title, we return the fragment
425 // text. Otherwise, we return the title text itself.
426 if ( $prefixedText === '' && $target->hasFragment() ) {
427 return $target->getFragment();
428 }
429
430 return $prefixedText;
431 }
432
433 private function getLinkURL( LinkTarget $target, array $query = [] ) {
434 // TODO: Use a LinkTargetResolver service instead of Title
435 $title = Title::newFromLinkTarget( $target );
436 if ( $this->forceArticlePath ) {
437 $realQuery = $query;
438 $query = [];
439 } else {
440 $realQuery = [];
441 }
442 $url = $title->getLinkURL( $query, false, $this->expandUrls );
443
444 if ( $this->forceArticlePath && $realQuery ) {
445 $url = wfAppendQuery( $url, $realQuery );
446 }
447
448 return $url;
449 }
450
459 public function normalizeTarget( LinkTarget $target ) {
460 if ( $target->getNamespace() == NS_SPECIAL && !$target->isExternal() ) {
461 list( $name, $subpage ) = $this->specialPageFactory->resolveAlias(
462 $target->getDBkey()
463 );
464 if ( $name ) {
465 return SpecialPage::getTitleValueFor( $name, $subpage, $target->getFragment() );
466 }
467 }
468
469 return $target;
470 }
471
480 private function mergeAttribs( $defaults, $attribs ) {
481 if ( !$attribs ) {
482 return $defaults;
483 }
484 # Merge the custom attribs with the default ones, and iterate
485 # over that, deleting all "false" attributes.
486 $ret = [];
487 $merged = Sanitizer::mergeAttributes( $defaults, $attribs );
488 foreach ( $merged as $key => $val ) {
489 # A false value suppresses the attribute
490 if ( $val !== false ) {
491 $ret[$key] = $val;
492 }
493 }
494 return $ret;
495 }
496
503 public function getLinkClasses( LinkTarget $target ) {
504 // Make sure the target is in the cache
505 $id = $this->linkCache->addLinkObj( $target );
506 if ( $id == 0 ) {
507 // Doesn't exist
508 return '';
509 }
510
511 if ( $this->linkCache->getGoodLinkFieldObj( $target, 'redirect' ) ) {
512 # Page is a redirect
513 return 'mw-redirect';
514 } elseif (
515 $this->stubThreshold > 0 && $this->nsInfo->isContent( $target->getNamespace() ) &&
516 $this->linkCache->getGoodLinkFieldObj( $target, 'length' ) < $this->stubThreshold
517 ) {
518 # Page is a stub
519 return 'stub';
520 }
521
522 return '';
523 }
524}
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
This class is a collection of static functions that serve two purposes:
Definition Html.php:49
Cache for article titles (prefixed DB keys) and ids linked from one source.
Definition LinkCache.php:34
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Class that generates HTML links for pages.
__construct(TitleFormatter $titleFormatter, LinkCache $linkCache, NamespaceInfo $nsInfo, SpecialPageFactory $specialPageFactory, HookContainer $hookContainer)
bool $forceArticlePath
Whether to force the pretty article path.
makePreloadedLink(LinkTarget $target, $text=null, $classes='', array $extraAttribs=[], array $query=[])
If you have already looked up the proper CSS classes using LinkRenderer::getLinkClasses() or some oth...
getLegacyOptions( $isKnown)
Get the options in the legacy format.
getLinkURL(LinkTarget $target, array $query=[])
SpecialPageFactory $specialPageFactory
runLegacyBeginHook(LinkTarget $target, &$text, &$extraAttribs, &$query, $isKnown)
buildAElement(LinkTarget $target, $text, array $attribs, $isKnown)
Builds the final element.
runBeginHook(LinkTarget $target, &$text, &$extraAttribs, &$query, $isKnown)
makeBrokenLink(LinkTarget $target, $text=null, array $extraAttribs=[], array $query=[])
getLinkClasses(LinkTarget $target)
Return the CSS classes of a known link.
mergeAttribs( $defaults, $attribs)
Merges two sets of attributes.
getLinkText(LinkTarget $target)
makeKnownLink(LinkTarget $target, $text=null, array $extraAttribs=[], array $query=[])
makeLink(LinkTarget $target, $text=null, array $extraAttribs=[], array $query=[])
normalizeTarget(LinkTarget $target)
Normalizes the provided target.
string bool int $expandUrls
A PROTO_* constant or false.
bool $runLegacyBeginHook
Whether to run the legacy Linker hooks.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
Factory for handling the special page list and generating SpecialPage objects.
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:33
Parent class for all special pages.
static getTitleValueFor( $name, $subpage=false, $fragment='')
Get a localised TitleValue object for a specified special page name.
Represents a title within MediaWiki.
Definition Title.php:42
const PROTO_HTTPS
Definition Defines.php:210
const NS_SPECIAL
Definition Defines.php:59
const PROTO_HTTP
Definition Defines.php:209
hasFragment()
Whether the link target has a fragment.
getFragment()
Get the link fragment (i.e.
getNamespace()
Get the namespace index.
getDBkey()
Get the main part with underscores.
isExternal()
Whether this LinkTarget has an interwiki component.
createFragmentTarget( $fragment)
Creates a new LinkTarget for a different fragment of the same page.
A title formatter service for MediaWiki.