Completed
Branch master (227f0c)
by
unknown
30:54
created

LinkRenderer::makePreloadedLink()   B

Complexity

Conditions 4
Paths 5

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 4
eloc 17
nc 5
nop 5
dl 0
loc 26
rs 8.5806
c 1
b 0
f 1
1
<?php
2
/**
3
 * This program is free software; you can redistribute it and/or modify
4
 * it under the terms of the GNU General Public License as published by
5
 * the Free Software Foundation; either version 2 of the License, or
6
 * (at your option) any later version.
7
 *
8
 * This program is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
 * GNU General Public License for more details.
12
 *
13
 * You should have received a copy of the GNU General Public License along
14
 * with this program; if not, write to the Free Software Foundation, Inc.,
15
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16
 * http://www.gnu.org/copyleft/gpl.html
17
 *
18
 * @file
19
 * @license GPL-2.0+
20
 * @author Kunal Mehta <[email protected]>
21
 */
22
namespace MediaWiki\Linker;
23
24
use DummyLinker;
25
use Hooks;
26
use Html;
27
use HtmlArmor;
28
use Linker;
29
use MediaWiki\MediaWikiServices;
30
use Sanitizer;
31
use Title;
32
use TitleFormatter;
33
34
/**
35
 * Class that generates HTML <a> links for pages.
36
 *
37
 * @since 1.28
38
 */
39
class LinkRenderer {
40
41
	/**
42
	 * Whether to force the pretty article path
43
	 *
44
	 * @var bool
45
	 */
46
	private $forceArticlePath = false;
47
48
	/**
49
	 * A PROTO_* constant or false
50
	 *
51
	 * @var string|bool|int
52
	 */
53
	private $expandUrls = false;
54
55
	/**
56
	 * @var int
57
	 */
58
	private $stubThreshold = 0;
59
60
	/**
61
	 * @var TitleFormatter
62
	 */
63
	private $titleFormatter;
64
65
	/**
66
	 * Whether to run the legacy Linker hooks
67
	 *
68
	 * @var bool
69
	 */
70
	private $runLegacyBeginHook = true;
71
72
	/**
73
	 * @param TitleFormatter $titleFormatter
74
	 */
75
	public function __construct( TitleFormatter $titleFormatter ) {
76
		$this->titleFormatter = $titleFormatter;
77
	}
78
79
	/**
80
	 * @param bool $force
81
	 */
82
	public function setForceArticlePath( $force ) {
83
		$this->forceArticlePath = $force;
84
	}
85
86
	/**
87
	 * @return bool
88
	 */
89
	public function getForceArticlePath() {
90
		return $this->forceArticlePath;
91
	}
92
93
	/**
94
	 * @param string|bool|int $expand A PROTO_* constant or false
95
	 */
96
	public function setExpandURLs( $expand ) {
97
		$this->expandUrls = $expand;
98
	}
99
100
	/**
101
	 * @return string|bool|int a PROTO_* constant or false
102
	 */
103
	public function getExpandURLs() {
104
		return $this->expandUrls;
105
	}
106
107
	/**
108
	 * @param int $threshold
109
	 */
110
	public function setStubThreshold( $threshold ) {
111
		$this->stubThreshold = $threshold;
112
	}
113
114
	/**
115
	 * @return int
116
	 */
117
	public function getStubThreshold() {
118
		return $this->stubThreshold;
119
	}
120
121
	/**
122
	 * @param bool $run
123
	 */
124
	public function setRunLegacyBeginHook( $run ) {
125
		$this->runLegacyBeginHook = $run;
126
	}
127
128
	/**
129
	 * @param LinkTarget $target
130
	 * @param string|HtmlArmor|null $text
131
	 * @param array $extraAttribs
132
	 * @param array $query
133
	 * @return string
134
	 */
135
	public function makeLink(
136
		LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
137
	) {
138
		$title = Title::newFromLinkTarget( $target );
139
		if ( $title->isKnown() ) {
140
			return $this->makeKnownLink( $target, $text, $extraAttribs, $query );
141
		} else {
142
			return $this->makeBrokenLink( $target, $text, $extraAttribs, $query );
143
		}
144
	}
145
146
	/**
147
	 * Get the options in the legacy format
148
	 *
149
	 * @param bool $isKnown Whether the link is known or broken
150
	 * @return array
151
	 */
152
	private function getLegacyOptions( $isKnown ) {
153
		$options = [ 'stubThreshold' => $this->stubThreshold ];
154
		if ( $this->forceArticlePath ) {
155
			$options[] = 'forcearticlepath';
156
		}
157
		if ( $this->expandUrls === PROTO_HTTP ) {
158
			$options[] = 'http';
159
		} elseif ( $this->expandUrls === PROTO_HTTPS ) {
160
			$options[] = 'https';
161
		}
162
163
		$options[] = $isKnown ? 'known' : 'broken';
164
165
		return $options;
166
	}
167
168
	private function runBeginHook( LinkTarget $target, &$text, &$extraAttribs, &$query, $isKnown ) {
169
		$ret = null;
170
		if ( !Hooks::run( 'HtmlPageLinkRendererBegin',
171
			[ $this, $target, &$text, &$extraAttribs, &$query, &$ret ] )
172
		) {
173
			return $ret;
174
		}
175
176
		// Now run the legacy hook
177
		return $this->runLegacyBeginHook( $target, $text, $extraAttribs, $query, $isKnown );
178
	}
179
180
	private function runLegacyBeginHook( LinkTarget $target, &$text, &$extraAttribs, &$query,
181
		$isKnown
182
	) {
183
		if ( !$this->runLegacyBeginHook || !Hooks::isRegistered( 'LinkBegin' ) ) {
184
			// Disabled, or nothing registered
185
			return null;
186
		}
187
188
		$realOptions = $options = $this->getLegacyOptions( $isKnown );
189
		$ret = null;
190
		$dummy = new DummyLinker();
191
		$title = Title::newFromLinkTarget( $target );
192
		if ( $text !== null ) {
193
			$realHtml = $html = HtmlArmor::getHtml( $text );
194
		} else {
195
			$realHtml = $html = null;
196
		}
197
		if ( !Hooks::run( 'LinkBegin',
198
			[ $dummy, $title, &$html, &$extraAttribs, &$query, &$options, &$ret ] )
199
		) {
200
			return $ret;
201
		}
202
203
		if ( $html !== null && $html !== $realHtml ) {
204
			// &$html was modified, so re-armor it as $text
205
			$text = new HtmlArmor( $html );
206
		}
207
208
		// Check if they changed any of the options, hopefully not!
209
		if ( $options !== $realOptions ) {
210
			$factory = MediaWikiServices::getInstance()->getLinkRendererFactory();
211
			// They did, so create a separate instance and have that take over the rest
212
			$newRenderer = $factory->createFromLegacyOptions( $options );
213
			// Don't recurse the hook...
214
			$newRenderer->setRunLegacyBeginHook( false );
215
			if ( in_array( 'known', $options, true ) ) {
216
				return $newRenderer->makeKnownLink( $title, $text, $extraAttribs, $query );
217
			} elseif ( in_array( 'broken', $options, true ) ) {
218
				return $newRenderer->makeBrokenLink( $title, $text, $extraAttribs, $query );
219
			} else {
220
				return $newRenderer->makeLink( $title, $text, $extraAttribs, $query );
221
			}
222
		}
223
224
		return null;
225
	}
226
227
	/**
228
	 * If you have already looked up the proper CSS classes using Linker::getLinkColour()
229
	 * or some other method, use this to avoid looking it up again.
230
	 *
231
	 * @param LinkTarget $target
232
	 * @param string|HtmlArmor|null $text
233
	 * @param string $classes CSS classes to add
234
	 * @param array $extraAttribs
235
	 * @param array $query
236
	 * @return string
237
	 */
238
	public function makePreloadedLink(
239
		LinkTarget $target, $text = null, $classes, array $extraAttribs = [], array $query = []
240
	) {
241
		// Run begin hook
242
		$ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, true );
243
		if ( $ret !== null ) {
244
			return $ret;
245
		}
246
		$target = $this->normalizeTarget( $target );
247
		$url = $this->getLinkURL( $target, $query );
248
		$attribs = [ 'class' => $classes ];
249
		$prefixedText = $this->titleFormatter->getPrefixedText( $target );
250
		if ( $prefixedText !== '' ) {
251
			$attribs['title'] = $prefixedText;
252
		}
253
254
		$attribs = [
255
			'href' => $url,
256
		] + $this->mergeAttribs( $attribs, $extraAttribs );
257
258
		if ( $text === null ) {
259
			$text = $this->getLinkText( $target );
260
		}
261
262
		return $this->buildAElement( $target, $text, $attribs, true );
263
	}
264
265
	/**
266
	 * @param LinkTarget $target
267
	 * @param string|HtmlArmor|null $text
268
	 * @param array $extraAttribs
269
	 * @param array $query
270
	 * @return string
271
	 */
272
	public function makeKnownLink(
273
		LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
274
	) {
275
		$classes = [];
276
		if ( $target->isExternal() ) {
277
			$classes[] = 'extiw';
278
		}
279
		$colour = Linker::getLinkColour( $target, $this->stubThreshold );
280
		if ( $colour !== '' ) {
281
			$classes[] = $colour;
282
		}
283
284
		return $this->makePreloadedLink(
285
			$target,
286
			$text,
287
			$classes ? implode( ' ', $classes ) : '',
288
			$extraAttribs,
289
			$query
290
		);
291
	}
292
293
	/**
294
	 * @param LinkTarget $target
295
	 * @param string|HtmlArmor|null $text
296
	 * @param array $extraAttribs
297
	 * @param array $query
298
	 * @return string
299
	 */
300
	public function makeBrokenLink(
301
		LinkTarget $target, $text = null, array $extraAttribs = [], array $query = []
302
	) {
303
		// Run legacy hook
304
		$ret = $this->runBeginHook( $target, $text, $extraAttribs, $query, false );
305
		if ( $ret !== null ) {
306
			return $ret;
307
		}
308
309
		# We don't want to include fragments for broken links, because they
310
		# generally make no sense.
311
		if ( $target->hasFragment() ) {
312
			$target = $target->createFragmentTarget( '' );
313
		}
314
		$target = $this->normalizeTarget( $target );
315
316
		if ( !isset( $query['action'] ) && $target->getNamespace() !== NS_SPECIAL ) {
317
			$query['action'] = 'edit';
318
			$query['redlink'] = '1';
319
		}
320
321
		$url = $this->getLinkURL( $target, $query );
322
		$attribs = [ 'class' => 'new' ];
323
		$prefixedText = $this->titleFormatter->getPrefixedText( $target );
324
		if ( $prefixedText !== '' ) {
325
			// This ends up in parser cache!
326
			$attribs['title'] = wfMessage( 'red-link-title', $prefixedText )
327
				->inContentLanguage()
328
				->text();
329
		}
330
331
		$attribs = [
332
			'href' => $url,
333
		] + $this->mergeAttribs( $attribs, $extraAttribs );
334
335
		if ( $text === null ) {
336
			$text = $this->getLinkText( $target );
337
		}
338
339
		return $this->buildAElement( $target, $text, $attribs, false );
340
	}
341
342
	/**
343
	 * Builds the final <a> element
344
	 *
345
	 * @param LinkTarget $target
346
	 * @param string|HtmlArmor $text
347
	 * @param array $attribs
348
	 * @param bool $isKnown
349
	 * @return null|string
350
	 */
351
	private function buildAElement( LinkTarget $target, $text, array $attribs, $isKnown ) {
352
		$ret = null;
353
		if ( !Hooks::run( 'HtmlPageLinkRendererEnd',
354
			[ $this, $target, $isKnown, &$text, &$attribs, &$ret ] )
355
		) {
356
			return $ret;
357
		}
358
359
		$html = HtmlArmor::getHtml( $text );
360
361
		// Run legacy hook
362
		if ( Hooks::isRegistered( 'LinkEnd' ) ) {
363
			$dummy = new DummyLinker();
364
			$title = Title::newFromLinkTarget( $target );
365
			$options = $this->getLegacyOptions( $isKnown );
366
			if ( !Hooks::run( 'LinkEnd',
367
				[ $dummy, $title, $options, &$html, &$attribs, &$ret ] )
368
			) {
369
				return $ret;
370
			}
371
		}
372
373
		return Html::rawElement( 'a', $attribs, $html );
374
	}
375
376
	/**
377
	 * @param LinkTarget $target
378
	 * @return string non-escaped text
379
	 */
380
	private function getLinkText( LinkTarget $target ) {
381
		$prefixedText = $this->titleFormatter->getPrefixedText( $target );
382
		// If the target is just a fragment, with no title, we return the fragment
383
		// text.  Otherwise, we return the title text itself.
384
		if ( $prefixedText === '' && $target->hasFragment() ) {
385
			return $target->getFragment();
386
		}
387
388
		return $prefixedText;
389
	}
390
391
	private function getLinkURL( LinkTarget $target, array $query = [] ) {
392
		// TODO: Use a LinkTargetResolver service instead of Title
393
		$title = Title::newFromLinkTarget( $target );
394
		$proto = $this->expandUrls !== false
395
			? $this->expandUrls
396
			: PROTO_RELATIVE;
397
		if ( $this->forceArticlePath ) {
398
			$realQuery = $query;
399
			$query = [];
400
		} else {
401
			$realQuery = [];
402
		}
403
		$url = $title->getLinkURL( $query, false, $proto );
404
405
		if ( $this->forceArticlePath && $realQuery ) {
406
			$url = wfAppendQuery( $url, $realQuery );
407
		}
408
409
		return $url;
410
	}
411
412
	/**
413
	 * Normalizes the provided target
414
	 *
415
	 * @todo move the code from Linker actually here
416
	 * @param LinkTarget $target
417
	 * @return LinkTarget
418
	 */
419
	private function normalizeTarget( LinkTarget $target ) {
420
		return Linker::normaliseSpecialPage( $target );
421
	}
422
423
	/**
424
	 * Merges two sets of attributes
425
	 *
426
	 * @param array $defaults
427
	 * @param array $attribs
428
	 *
429
	 * @return array
430
	 */
431
	private function mergeAttribs( $defaults, $attribs ) {
432
		if ( !$attribs ) {
433
			return $defaults;
434
		}
435
		# Merge the custom attribs with the default ones, and iterate
436
		# over that, deleting all "false" attributes.
437
		$ret = [];
438
		$merged = Sanitizer::mergeAttributes( $defaults, $attribs );
439
		foreach ( $merged as $key => $val ) {
440
			# A false value suppresses the attribute
441
			if ( $val !== false ) {
442
				$ret[$key] = $val;
443
			}
444
		}
445
		return $ret;
446
	}
447
448
}
449