1<?php
2
3use MediaWiki\MediaWikiServices;
4use MediaWiki\Revision\RevisionRecord;
5
6/**
7 * @group Database
8 */
9class LinkerTest extends MediaWikiLangTestCase {
10	/**
11	 * @dataProvider provideCasesForUserLink
12	 * @covers Linker::userLink
13	 */
14	public function testUserLink( $expected, $userId, $userName, $altUserName = false, $msg = '' ) {
15		$this->setMwGlobals( [
16			'wgArticlePath' => '/wiki/$1',
17		] );
18
19		// We'd also test the warning, but injecting a mock logger into a static method is tricky.
20		if ( !$userName ) {
21			Wikimedia\suppressWarnings();
22		}
23		$actual = Linker::userLink( $userId, $userName, $altUserName );
24		if ( !$userName ) {
25			Wikimedia\restoreWarnings();
26		}
27
28		$this->assertEquals( $expected, $actual, $msg );
29	}
30
31	public static function provideCasesForUserLink() {
32		# Format:
33		# - expected
34		# - userid
35		# - username
36		# - optional altUserName
37		# - optional message
38		return [
39			# Empty name (T222529)
40			'Empty username, userid 0' => [ '(no username available)', 0, '' ],
41			'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
42
43			'false instead of username' => [ '(no username available)', 73, false ],
44			'null instead of username' => [ '(no username available)', 0, null ],
45
46			# ## ANONYMOUS USER ########################################
47			[
48				'<a href="/wiki/Special:Contributions/JohnDoe" '
49					. 'class="mw-userlink mw-anonuserlink" '
50					. 'title="Special:Contributions/JohnDoe"><bdi>JohnDoe</bdi></a>',
51				0, 'JohnDoe', false,
52			],
53			[
54				'<a href="/wiki/Special:Contributions/::1" '
55					. 'class="mw-userlink mw-anonuserlink" '
56					. 'title="Special:Contributions/::1"><bdi>::1</bdi></a>',
57				0, '::1', false,
58				'Anonymous with pretty IPv6'
59			],
60			[
61				'<a href="/wiki/Special:Contributions/0:0:0:0:0:0:0:1" '
62					. 'class="mw-userlink mw-anonuserlink" '
63					. 'title="Special:Contributions/0:0:0:0:0:0:0:1"><bdi>::1</bdi></a>',
64				0, '0:0:0:0:0:0:0:1', false,
65				'Anonymous with almost pretty IPv6'
66			],
67			[
68				'<a href="/wiki/Special:Contributions/0000:0000:0000:0000:0000:0000:0000:0001" '
69					. 'class="mw-userlink mw-anonuserlink" '
70					. 'title="Special:Contributions/0000:0000:0000:0000:0000:0000:0000:0001"><bdi>::1</bdi></a>',
71				0, '0000:0000:0000:0000:0000:0000:0000:0001', false,
72				'Anonymous with full IPv6'
73			],
74			[
75				'<a href="/wiki/Special:Contributions/::1" '
76					. 'class="mw-userlink mw-anonuserlink" '
77					. 'title="Special:Contributions/::1"><bdi>AlternativeUsername</bdi></a>',
78				0, '::1', 'AlternativeUsername',
79				'Anonymous with pretty IPv6 and an alternative username'
80			],
81
82			# IPV4
83			[
84				'<a href="/wiki/Special:Contributions/127.0.0.1" '
85					. 'class="mw-userlink mw-anonuserlink" '
86					. 'title="Special:Contributions/127.0.0.1"><bdi>127.0.0.1</bdi></a>',
87				0, '127.0.0.1', false,
88				'Anonymous with IPv4'
89			],
90			[
91				'<a href="/wiki/Special:Contributions/127.0.0.1" '
92					. 'class="mw-userlink mw-anonuserlink" '
93					. 'title="Special:Contributions/127.0.0.1"><bdi>AlternativeUsername</bdi></a>',
94				0, '127.0.0.1', 'AlternativeUsername',
95				'Anonymous with IPv4 and an alternative username'
96			],
97
98			# ## Regular user ##########################################
99			# TODO!
100		];
101	}
102
103	/**
104	 * @dataProvider provideUserToolLinks
105	 * @covers Linker::userToolLinks
106	 * @param string $expected
107	 * @param int $userId
108	 * @param string $userText
109	 */
110	public function testUserToolLinks( $expected, $userId, $userText ) {
111		// We'd also test the warning, but injecting a mock logger into a static method is tricky.
112		if ( $userText === '' ) {
113			Wikimedia\suppressWarnings();
114		}
115		$actual = Linker::userToolLinks( $userId, $userText );
116		if ( $userText === '' ) {
117			Wikimedia\restoreWarnings();
118		}
119
120		$this->assertSame( $expected, $actual );
121	}
122
123	public static function provideUserToolLinks() {
124		return [
125			// Empty name (T222529)
126			'Empty username, userid 0' => [ ' (no username available)', 0, '' ],
127			'Empty username, userid > 0' => [ ' (no username available)', 73, '' ],
128		];
129	}
130
131	/**
132	 * @dataProvider provideUserTalkLink
133	 * @covers Linker::userTalkLink
134	 * @param string $expected
135	 * @param int $userId
136	 * @param string $userText
137	 */
138	public function testUserTalkLink( $expected, $userId, $userText ) {
139		// We'd also test the warning, but injecting a mock logger into a static method is tricky.
140		if ( $userText === '' ) {
141			Wikimedia\suppressWarnings();
142		}
143		$actual = Linker::userTalkLink( $userId, $userText );
144		if ( $userText === '' ) {
145			Wikimedia\restoreWarnings();
146		}
147
148		$this->assertSame( $expected, $actual );
149	}
150
151	public static function provideUserTalkLink() {
152		return [
153			// Empty name (T222529)
154			'Empty username, userid 0' => [ '(no username available)', 0, '' ],
155			'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
156		];
157	}
158
159	/**
160	 * @dataProvider provideBlockLink
161	 * @covers Linker::blockLink
162	 * @param string $expected
163	 * @param int $userId
164	 * @param string $userText
165	 */
166	public function testBlockLink( $expected, $userId, $userText ) {
167		// We'd also test the warning, but injecting a mock logger into a static method is tricky.
168		if ( $userText === '' ) {
169			Wikimedia\suppressWarnings();
170		}
171		$actual = Linker::blockLink( $userId, $userText );
172		if ( $userText === '' ) {
173			Wikimedia\restoreWarnings();
174		}
175
176		$this->assertSame( $expected, $actual );
177	}
178
179	public static function provideBlockLink() {
180		return [
181			// Empty name (T222529)
182			'Empty username, userid 0' => [ '(no username available)', 0, '' ],
183			'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
184		];
185	}
186
187	/**
188	 * @dataProvider provideEmailLink
189	 * @covers Linker::emailLink
190	 * @param string $expected
191	 * @param int $userId
192	 * @param string $userText
193	 */
194	public function testEmailLink( $expected, $userId, $userText ) {
195		// We'd also test the warning, but injecting a mock logger into a static method is tricky.
196		if ( $userText === '' ) {
197			Wikimedia\suppressWarnings();
198		}
199		$actual = Linker::emailLink( $userId, $userText );
200		if ( $userText === '' ) {
201			Wikimedia\restoreWarnings();
202		}
203
204		$this->assertSame( $expected, $actual );
205	}
206
207	public static function provideEmailLink() {
208		return [
209			// Empty name (T222529)
210			'Empty username, userid 0' => [ '(no username available)', 0, '' ],
211			'Empty username, userid > 0' => [ '(no username available)', 73, '' ],
212		];
213	}
214
215	/**
216	 * @dataProvider provideCasesForFormatComment
217	 * @covers Linker::formatComment
218	 * @covers Linker::formatAutocomments
219	 * @covers Linker::formatLinksInComment
220	 */
221	public function testFormatComment(
222		$expected, $comment, $title = false, $local = false, $wikiId = null
223	) {
224		$conf = new SiteConfiguration();
225		$conf->settings = [
226			'wgServer' => [
227				'enwiki' => '//en.example.org',
228				'dewiki' => '//de.example.org',
229			],
230			'wgArticlePath' => [
231				'enwiki' => '/w/$1',
232				'dewiki' => '/w/$1',
233			],
234		];
235		$conf->suffixes = [ 'wiki' ];
236
237		$this->setMwGlobals( [
238			'wgScript' => '/wiki/index.php',
239			'wgArticlePath' => '/wiki/$1',
240			'wgCapitalLinks' => true,
241			'wgConf' => $conf,
242			// TODO: update tests when the default changes
243			'wgFragmentMode' => [ 'legacy' ],
244		] );
245
246		if ( $title === false ) {
247			// We need a page title that exists
248			$title = Title::newFromText( 'Special:BlankPage' );
249		}
250
251		$this->assertEquals(
252			$expected,
253			Linker::formatComment( $comment, $title, $local, $wikiId )
254		);
255	}
256
257	public function provideCasesForFormatComment() {
258		$wikiId = 'enwiki'; // $wgConf has a fake entry for this
259
260		// phpcs:disable Generic.Files.LineLength
261		return [
262			// Linker::formatComment
263			[
264				'a&lt;script&gt;b',
265				'a<script>b',
266			],
267			[
268				'a—b',
269				'a&mdash;b',
270			],
271			[
272				"&#039;&#039;&#039;not bolded&#039;&#039;&#039;",
273				"'''not bolded'''",
274			],
275			[
276				"try &lt;script&gt;evil&lt;/scipt&gt; things",
277				"try <script>evil</scipt> things",
278			],
279			// Linker::formatAutocomments
280			[
281				'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→‎autocomment</a></span></span>',
282				"/* autocomment */",
283			],
284			[
285				'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#linkie.3F" title="Special:BlankPage">→‎&#91;[linkie?]]</a></span></span>',
286				"/* [[linkie?]] */",
287			],
288			[
289				'<span dir="auto"><span class="autocomment">: </span> // Edit via via</span>',
290				// Regression test for T222857
291				"/*  */ // Edit via via",
292			],
293			[
294				'<span dir="auto"><span class="autocomment">: </span> foobar</span>',
295				// Regression test for T222857
296				"/**/ foobar",
297			],
298			[
299				'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→‎autocomment</a>: </span> post</span>',
300				"/* autocomment */ post",
301			],
302			[
303				'pre <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→‎autocomment</a></span></span>',
304				"pre /* autocomment */",
305			],
306			[
307				'pre <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→‎autocomment</a>: </span> post</span>',
308				"pre /* autocomment */ post",
309			],
310			[
311				'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→‎autocomment</a>: </span> multiple? <span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment2" title="Special:BlankPage">→‎autocomment2</a></span></span></span>',
312				"/* autocomment */ multiple? /* autocomment2 */",
313			],
314			[
315				'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.2F.2A" title="Special:BlankPage">→‎autocomment containing /*</a>: </span> T70361</span>',
316				"/* autocomment containing /* */ T70361"
317			],
318			[
319				'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.22quotes.22" title="Special:BlankPage">→‎autocomment containing &quot;quotes&quot;</a></span></span>',
320				"/* autocomment containing \"quotes\" */"
321			],
322			[
323				'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment_containing_.3Cscript.3Etags.3C.2Fscript.3E" title="Special:BlankPage">→‎autocomment containing &lt;script&gt;tags&lt;/script&gt;</a></span></span>',
324				"/* autocomment containing <script>tags</script> */"
325			],
326			[
327				'<span dir="auto"><span class="autocomment"><a href="#autocomment">→‎autocomment</a></span></span>',
328				"/* autocomment */",
329				false, true
330			],
331			[
332				'<span dir="auto"><span class="autocomment">autocomment</span></span>',
333				"/* autocomment */",
334				null
335			],
336			[
337				'',
338				"/* */",
339				false, true
340			],
341			[
342				'',
343				"/* */",
344				null
345			],
346			[
347				'<span dir="auto"><span class="autocomment">[[</span></span>',
348				"/* [[ */",
349				false, true
350			],
351			[
352				'<span dir="auto"><span class="autocomment">[[</span></span>',
353				"/* [[ */",
354				null
355			],
356			[
357				"foo <span dir=\"auto\"><span class=\"autocomment\"><a href=\"#.23\">→‎&#91;[#_\t_]]</a></span></span>",
358				"foo /* [[#_\t_]] */",
359				false, true
360			],
361			[
362				"foo <span dir=\"auto\"><span class=\"autocomment\"><a href=\"#_.09\">#_\t_</a></span></span>",
363				"foo /* [[#_\t_]] */",
364				null
365			],
366			[
367				'<span dir="auto"><span class="autocomment"><a href="/wiki/Special:BlankPage#autocomment" title="Special:BlankPage">→‎autocomment</a></span></span>',
368				"/* autocomment */",
369				false, false
370			],
371			[
372				'<span dir="auto"><span class="autocomment"><a class="external" rel="nofollow" href="//en.example.org/w/Special:BlankPage#autocomment">→‎autocomment</a></span></span>',
373				"/* autocomment */",
374				false, false, $wikiId
375			],
376			// Linker::formatLinksInComment
377			[
378				'abc <a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">link</a> def',
379				"abc [[link]] def",
380			],
381			[
382				'abc <a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">text</a> def',
383				"abc [[link|text]] def",
384			],
385			[
386				'abc <a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a> def',
387				"abc [[Special:BlankPage|]] def",
388			],
389			[
390				'abc <a href="/wiki/index.php?title=%C4%84%C5%9B%C5%BC&amp;action=edit&amp;redlink=1" class="new" title="Ąśż (page does not exist)">ąśż</a> def',
391				"abc [[%C4%85%C5%9B%C5%BC]] def",
392			],
393			[
394				'abc <a href="/wiki/Special:BlankPage#section" title="Special:BlankPage">#section</a> def',
395				"abc [[#section]] def",
396			],
397			[
398				'abc <a href="/wiki/index.php?title=/subpage&amp;action=edit&amp;redlink=1" class="new" title="/subpage (page does not exist)">/subpage</a> def',
399				"abc [[/subpage]] def",
400			],
401			[
402				'abc <a href="/wiki/index.php?title=%22evil!%22&amp;action=edit&amp;redlink=1" class="new" title="&quot;evil!&quot; (page does not exist)">&quot;evil!&quot;</a> def',
403				"abc [[\"evil!\"]] def",
404			],
405			[
406				'abc [[&lt;script&gt;very evil&lt;/script&gt;]] def',
407				"abc [[<script>very evil</script>]] def",
408			],
409			[
410				'abc [[|]] def',
411				"abc [[|]] def",
412			],
413			[
414				'abc <a href="/wiki/index.php?title=Link&amp;action=edit&amp;redlink=1" class="new" title="Link (page does not exist)">link</a> def',
415				"abc [[link]] def",
416				false, false
417			],
418			[
419				'abc <a class="external" rel="nofollow" href="//en.example.org/w/Link">link</a> def',
420				"abc [[link]] def",
421				false, false, $wikiId
422			],
423		];
424		// phpcs:enable
425	}
426
427	/**
428	 * @covers Linker::formatLinksInComment
429	 * @dataProvider provideCasesForFormatLinksInComment
430	 */
431	public function testFormatLinksInComment( $expected, $input, $wiki ) {
432		$conf = new SiteConfiguration();
433		$conf->settings = [
434			'wgServer' => [
435				'enwiki' => '//en.example.org'
436			],
437			'wgArticlePath' => [
438				'enwiki' => '/w/$1',
439			],
440		];
441		$conf->suffixes = [ 'wiki' ];
442		$this->setMwGlobals( [
443			'wgScript' => '/wiki/index.php',
444			'wgArticlePath' => '/wiki/$1',
445			'wgCapitalLinks' => true,
446			'wgConf' => $conf,
447		] );
448
449		$this->assertEquals(
450			$expected,
451			Linker::formatLinksInComment( $input, Title::newFromText( 'Special:BlankPage' ), false, $wiki )
452		);
453	}
454
455	/**
456	 * @covers Linker::generateRollback
457	 * @dataProvider provideCasesForRollbackGeneration
458	 */
459	public function testGenerateRollback( $rollbackEnabled, $expectedModules, $title ) {
460		$this->markTestSkippedIfDbType( 'postgres' );
461
462		$context = RequestContext::getMain();
463		$user = $context->getUser();
464		$user->setOption( 'showrollbackconfirmation', $rollbackEnabled );
465
466		$this->assertSame( 0, Title::newFromText( $title )->getArticleID() );
467		$pageData = $this->insertPage( $title );
468		$page = WikiPage::factory( $pageData['title'] );
469
470		$updater = $page->newPageUpdater( $user );
471		$updater->setContent( \MediaWiki\Revision\SlotRecord::MAIN,
472			new TextContent( 'Technical Wishes 123!' )
473		);
474		$summary = CommentStoreComment::newUnsavedComment( 'Some comment!' );
475		$updater->saveRevision( $summary );
476
477		$rollbackOutput = Linker::generateRollback( $page->getRevisionRecord(), $context );
478		$modules = $context->getOutput()->getModules();
479		$currentRev = $page->getRevisionRecord();
480		$revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
481		$oldestRev = $revisionLookup->getFirstRevision( $page->getTitle() );
482
483		$this->assertEquals( $expectedModules, $modules );
484		$this->assertInstanceOf( RevisionRecord::class, $currentRev );
485		$this->assertInstanceOf( User::class, $currentRev->getUser() );
486		$this->assertEquals( $user->getName(), $currentRev->getUser()->getName() );
487		$this->assertEquals(
488			static::getTestSysop()->getUser(),
489			$oldestRev->getUser()->getName()
490		);
491
492		$ids = [];
493		$r = $oldestRev;
494		while ( $r ) {
495			$ids[] = $r->getId();
496			$r = $revisionLookup->getNextRevision( $r );
497		}
498		$this->assertEquals( [ $oldestRev->getId(), $currentRev->getId() ], $ids );
499
500		$this->assertStringContainsString( 'rollback 1 edit', $rollbackOutput );
501	}
502
503	public static function provideCasesForRollbackGeneration() {
504		return [
505			[
506				true,
507				[ 'mediawiki.misc-authed-curate' ],
508				'Rollback_Test_Page'
509			],
510			[
511				false,
512				[],
513				'Rollback_Test_Page2'
514			]
515		];
516	}
517
518	public static function provideCasesForFormatLinksInComment() {
519		// phpcs:disable Generic.Files.LineLength
520		return [
521			[
522				'foo bar <a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>',
523				'foo bar [[Special:BlankPage]]',
524				null,
525			],
526			[
527				'<a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>',
528				'[[ :Special:BlankPage]]',
529				null,
530			],
531			[
532				'[[Foo<a href="/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>',
533				'[[Foo[[Special:BlankPage]]',
534				null,
535			],
536			[
537				'<a class="external" rel="nofollow" href="//en.example.org/w/Foo%27bar">Foo\'bar</a>',
538				"[[Foo'bar]]",
539				'enwiki',
540			],
541			[
542				'foo bar <a class="external" rel="nofollow" href="//en.example.org/w/Special:BlankPage">Special:BlankPage</a>',
543				'foo bar [[Special:BlankPage]]',
544				'enwiki',
545			],
546			[
547				'foo bar <a class="external" rel="nofollow" href="//en.example.org/w/File:Example">Image:Example</a>',
548				'foo bar [[Image:Example]]',
549				'enwiki',
550			],
551		];
552		// phpcs:enable
553	}
554
555	public static function provideLinkBeginHook() {
556		// phpcs:disable Generic.Files.LineLength
557		return [
558			// Modify $html
559			[
560				function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
561					$html = 'foobar';
562				},
563				'<a href="/wiki/Special:BlankPage" title="Special:BlankPage">foobar</a>'
564			],
565			// Modify $attribs
566			[
567				function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
568					$attribs['bar'] = 'baz';
569				},
570				'<a href="/wiki/Special:BlankPage" title="Special:BlankPage" bar="baz">Special:BlankPage</a>'
571			],
572			// Modify $query
573			[
574				function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
575					$query['bar'] = 'baz';
576				},
577				'<a href="/w/index.php?title=Special:BlankPage&amp;bar=baz" title="Special:BlankPage">Special:BlankPage</a>'
578			],
579			// Force HTTP $options
580			[
581				function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
582					$options = [ 'http' ];
583				},
584				'<a href="http://example.org/wiki/Special:BlankPage" title="Special:BlankPage">Special:BlankPage</a>'
585			],
586			// Force 'forcearticlepath' in $options
587			[
588				function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
589					$options = [ 'forcearticlepath' ];
590					$query['foo'] = 'bar';
591				},
592				'<a href="/wiki/Special:BlankPage?foo=bar" title="Special:BlankPage">Special:BlankPage</a>'
593			],
594			// Abort early
595			[
596				function ( $dummy, $title, &$html, &$attribs, &$query, &$options, &$ret ) {
597					$ret = 'foobar';
598					return false;
599				},
600				'foobar'
601			],
602		];
603		// phpcs:enable
604	}
605
606	/**
607	 * @covers MediaWiki\Linker\LinkRenderer::runLegacyBeginHook
608	 * @dataProvider provideLinkBeginHook
609	 */
610	public function testLinkBeginHook( $callback, $expected ) {
611		$this->hideDeprecated( 'LinkBegin hook (used in hook-LinkBegin-closure)' );
612		$this->setMwGlobals( [
613			'wgArticlePath' => '/wiki/$1',
614			'wgServer' => '//example.org',
615			'wgCanonicalServer' => 'http://example.org',
616			'wgScriptPath' => '/w',
617			'wgScript' => '/w/index.php',
618		] );
619
620		$this->setMwGlobals( 'wgHooks', [ 'LinkBegin' => [ $callback ] ] );
621		$title = SpecialPage::getTitleFor( 'Blankpage' );
622		$out = Linker::link( $title );
623		$this->assertEquals( $expected, $out );
624	}
625
626	public static function provideLinkEndHook() {
627		return [
628			// Override $html
629			[
630				function ( $dummy, $title, $options, &$html, &$attribs, &$ret ) {
631					$html = 'foobar';
632				},
633				'<a href="/wiki/Special:BlankPage" title="Special:BlankPage">foobar</a>'
634			],
635			// Modify $attribs
636			[
637				function ( $dummy, $title, $options, &$html, &$attribs, &$ret ) {
638					$attribs['bar'] = 'baz';
639				},
640				'<a href="/wiki/Special:BlankPage" title="Special:BlankPage" bar="baz">Special:BlankPage</a>'
641			],
642			// Fully override return value and abort hook
643			[
644				function ( $dummy, $title, $options, &$html, &$attribs, &$ret ) {
645					$ret = 'blahblahblah';
646					return false;
647				},
648				'blahblahblah'
649			],
650
651		];
652	}
653
654	/**
655	 * @covers MediaWiki\Linker\LinkRenderer::buildAElement
656	 * @dataProvider provideLinkEndHook
657	 */
658	public function testLinkEndHook( $callback, $expected ) {
659		$this->hideDeprecated( 'LinkEnd hook (used in hook-LinkEnd-closure)' );
660		$this->setMwGlobals( [
661			'wgArticlePath' => '/wiki/$1',
662		] );
663
664		$this->setMwGlobals( 'wgHooks', [ 'LinkEnd' => [ $callback ] ] );
665
666		$title = SpecialPage::getTitleFor( 'Blankpage' );
667		$out = Linker::link( $title );
668		$this->assertEquals( $expected, $out );
669	}
670
671	public static function provideTooltipAndAccesskeyAttribs() {
672		return [
673			'Watch no expiry' => [
674				'ca-watch', [], null, [ 'title' => 'Add this page to your watchlist [w]', 'accesskey' => 'w' ]
675			],
676			'Key does not exist' => [
677				'key-does-not-exist', [], null, []
678			],
679			'Unwatch no expiry' => [
680				'ca-unwatch', [], null, [ 'title' => 'Remove this page from your watchlist [w]',
681					'accesskey' => 'w' ]
682			],
683		];
684	}
685
686	/**
687	 * @covers Linker::tooltipAndAccesskeyAttribs
688	 * @dataProvider provideTooltipAndAccesskeyAttribs
689	 */
690	public function testTooltipAndAccesskeyAttribs( $name, $msgParams, $options, $expected ) {
691		$this->setMwGlobals( [
692			'wgWatchlistExpiry' => true,
693		] );
694		$user = $this->createMock( User::class );
695		$user->method( 'isRegistered' )->willReturn( true );
696		$user->method( 'isLoggedIn' )->willReturn( true );
697
698		$title = SpecialPage::getTitleFor( 'Blankpage' );
699
700		$context = RequestContext::getMain();
701		$context->setTitle( $title );
702		$context->setUser( $user );
703
704		$watchedItemWithoutExpiry = new WatchedItem( $user, $title, null, null );
705
706		$result = Linker::tooltipAndAccesskeyAttribs( $name, $msgParams, $options );
707
708		$this->assertEquals( $expected, $result );
709	}
710}
711