Merge "Remove EpicPupper from en.json authors"
[mediawiki.git] / tests / phpunit / includes / actions / ActionEntryPointTest.php
blob0cb643bb74e6a59b86348f7f5fd0d7a17d146f4d
1 <?php
3 namespace MediaWiki\Tests\Action;
5 use BadTitleError;
6 use MediaWiki\Actions\ActionEntryPoint;
7 use MediaWiki\Context\RequestContext;
8 use MediaWiki\Deferred\DeferredUpdates;
9 use MediaWiki\Deferred\DeferredUpdatesScopeMediaWikiStack;
10 use MediaWiki\Deferred\DeferredUpdatesScopeStack;
11 use MediaWiki\MainConfigNames;
12 use MediaWiki\Request\FauxRequest;
13 use MediaWiki\Request\FauxResponse;
14 use MediaWiki\Request\WebRequest;
15 use MediaWiki\SpecialPage\SpecialPage;
16 use MediaWiki\Tests\MockEnvironment;
17 use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
18 use MediaWiki\Title\MalformedTitleException;
19 use MediaWiki\Title\Title;
20 use MediaWikiIntegrationTestCase;
21 use PHPUnit\Framework\Assert;
22 use ReflectionMethod;
23 use Wikimedia\TestingAccessWrapper;
24 use WikiPage;
26 // phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
28 /**
29 * @group Database
30 * @covers \MediaWiki\Actions\ActionEntryPoint
32 class ActionEntryPointTest extends MediaWikiIntegrationTestCase {
33 use TempUserTestTrait;
35 protected function setUp(): void {
36 parent::setUp();
38 $this->overrideConfigValues( [
39 MainConfigNames::Server => 'http://example.org',
40 MainConfigNames::ScriptPath => '/w',
41 MainConfigNames::Script => '/w/index.php',
42 MainConfigNames::LanguageCode => 'en',
43 ] );
45 // Needed to test redirects to My* special pages as an anonymous user.
46 $this->disableAutoCreateTempUser();
49 protected function tearDown(): void {
50 // Restore a scope stack that will run updates immediately
51 DeferredUpdates::setScopeStack( new DeferredUpdatesScopeMediaWikiStack() );
52 parent::tearDown();
55 /**
56 * @param MockEnvironment|WebRequest|array|null $environment
57 * @param RequestContext|null $context
59 * @return ActionEntryPoint
61 private function getEntryPoint( $environment = null, ?RequestContext $context = null ) {
62 if ( !$environment ) {
63 $environment = new MockEnvironment();
66 if ( is_array( $environment ) ) {
67 $environment = new FauxRequest( $environment );
70 if ( $environment instanceof WebRequest ) {
71 $environment = new MockEnvironment( $environment );
74 $entryPoint = new ActionEntryPoint(
75 $context ?? $environment->makeFauxContext(),
76 $environment,
77 $this->getServiceContainer()
79 $entryPoint->enableOutputCapture();
81 return $entryPoint;
84 public static function provideTryNormaliseRedirect() {
85 return [
87 // View: Canonical
88 'url' => 'http://example.org/wiki/Foo_Bar',
89 'query' => [],
90 'title' => 'Foo_Bar',
91 'redirect' => false,
94 // View: Escaped title
95 'url' => 'http://example.org/wiki/Foo%20Bar',
96 'query' => [],
97 'title' => 'Foo_Bar',
98 'redirect' => 'http://example.org/wiki/Foo_Bar',
101 // View: Script path
102 'url' => 'http://example.org/w/index.php?title=Foo_Bar',
103 'query' => [ 'title' => 'Foo_Bar' ],
104 'title' => 'Foo_Bar',
105 'redirect' => false,
108 // View: Script path with implicit title from page id
109 'url' => 'http://example.org/w/index.php?curid=123',
110 'query' => [ 'curid' => '123' ],
111 'title' => 'Foo_Bar',
112 'redirect' => false,
115 // View: Script path with implicit title from revision id
116 'url' => 'http://example.org/w/index.php?oldid=123',
117 'query' => [ 'oldid' => '123' ],
118 'title' => 'Foo_Bar',
119 'redirect' => false,
122 // View: Script path without title
123 'url' => 'http://example.org/w/index.php',
124 'query' => [],
125 'title' => 'Main_Page',
126 'redirect' => 'http://example.org/wiki/Main_Page',
129 // View: Script path with empty title
130 'url' => 'http://example.org/w/index.php?title=',
131 'query' => [ 'title' => '' ],
132 'title' => 'Main_Page',
133 'redirect' => 'http://example.org/wiki/Main_Page',
136 // View: Index with escaped title
137 'url' => 'http://example.org/w/index.php?title=Foo%20Bar',
138 'query' => [ 'title' => 'Foo Bar' ],
139 'title' => 'Foo_Bar',
140 'redirect' => 'http://example.org/wiki/Foo_Bar',
143 // View: Script path with escaped title
144 'url' => 'http://example.org/w/?title=Foo_Bar',
145 'query' => [ 'title' => 'Foo_Bar' ],
146 'title' => 'Foo_Bar',
147 'redirect' => false,
150 // View: Root path with escaped title
151 'url' => 'http://example.org/?title=Foo_Bar',
152 'query' => [ 'title' => 'Foo_Bar' ],
153 'title' => 'Foo_Bar',
154 'redirect' => false,
157 // View: Canonical with redundant query
158 'url' => 'http://example.org/wiki/Foo_Bar?action=view',
159 'query' => [ 'action' => 'view' ],
160 'title' => 'Foo_Bar',
161 'redirect' => false,
164 // Edit: Canonical view url with action query
165 'url' => 'http://example.org/wiki/Foo_Bar?action=edit',
166 'query' => [ 'action' => 'edit' ],
167 'title' => 'Foo_Bar',
168 'redirect' => false,
171 // View: Index with action query
172 'url' => 'http://example.org/w/index.php?title=Foo_Bar&action=view',
173 'query' => [ 'title' => 'Foo_Bar', 'action' => 'view' ],
174 'title' => 'Foo_Bar',
175 'redirect' => false,
178 // Edit: Index with action query
179 'url' => 'http://example.org/w/index.php?title=Foo_Bar&action=edit',
180 'query' => [ 'title' => 'Foo_Bar', 'action' => 'edit' ],
181 'title' => 'Foo_Bar',
182 'redirect' => false,
185 // Path with double slash prefix (T100782)
186 'url' => 'http://example.org//wiki/Double_slash',
187 'query' => [],
188 'title' => 'Double_slash',
189 'redirect' => false,
192 // View: Media namespace redirect (T203942)
193 'url' => 'http://example.org/w/index.php?title=Media:Foo_Bar',
194 'query' => [ 'title' => 'Foo_Bar' ],
195 'title' => 'File:Foo_Bar',
196 'redirect' => 'http://example.org/wiki/File:Foo_Bar',
202 * @dataProvider provideTryNormaliseRedirect
204 public function testTryNormaliseRedirect( $url, $query, $title, $expectedRedirect = false ) {
205 $environment = new MockEnvironment();
206 $environment->setRequestInfo( $url, $query );
208 $titleObj = Title::newFromText( $title );
210 // Set global context since some involved code paths don't yet have context
211 $context = $environment->makeFauxContext();
212 $context->setTitle( $titleObj );
214 $mw = $this->getEntryPoint( $environment, $context );
216 $method = new ReflectionMethod( $mw, 'tryNormaliseRedirect' );
217 $method->setAccessible( true );
218 $ret = $method->invoke( $mw, $titleObj );
220 $this->assertEquals(
221 $expectedRedirect !== false,
222 $ret,
223 'Return true only when redirecting'
226 $this->assertEquals(
227 $expectedRedirect ?: '',
228 $context->getOutput()->getRedirect()
232 public function testMainPageIsDomainRoot() {
233 $this->overrideConfigValue( MainConfigNames::MainPageIsDomainRoot, true );
235 $environment = new MockEnvironment();
236 $environment->setRequestInfo( '/' );
238 // Set global context since some involved code paths don't yet have context
239 $context = $environment->makeFauxContext();
241 $entryPoint = $this->getEntryPoint( $environment, $context );
242 $entryPoint->run();
244 $expected = '<title>(pagetitle: Main Page)';
245 Assert::assertStringContainsString( $expected, $entryPoint->getCapturedOutput() );
248 public static function provideParseTitle() {
249 return [
250 "No title means main page" => [
251 'query' => [],
252 'expected' => 'Main Page',
254 "Empty title also means main page" => [
255 'query' => wfCgiToArray( '?title=' ),
256 'expected' => 'Main Page',
258 "Valid title" => [
259 'query' => wfCgiToArray( '?title=Foo' ),
260 'expected' => 'Foo',
262 "Invalid title" => [
263 'query' => wfCgiToArray( '?title=[INVALID]' ),
264 'expected' => false,
266 "Invalid 'oldid'… means main page? (we show an error elsewhere)" => [
267 'query' => wfCgiToArray( '?oldid=9999999' ),
268 'expected' => 'Main Page',
270 "Invalid 'diff'… means main page? (we show an error elsewhere)" => [
271 'query' => wfCgiToArray( '?diff=9999999' ),
272 'expected' => 'Main Page',
274 "Invalid 'curid'" => [
275 'query' => wfCgiToArray( '?curid=9999999' ),
276 'expected' => false,
278 "'search' parameter with no title provided forces Special:Search" => [
279 'query' => wfCgiToArray( '?search=foo' ),
280 'expected' => 'Special:Search',
282 "'action=revisiondelete' forces Special:RevisionDelete even with title" => [
283 'query' => wfCgiToArray( '?action=revisiondelete&title=Unused' ),
284 'expected' => 'Special:RevisionDelete',
286 "'action=historysubmit&revisiondelete=1' forces Special:RevisionDelete even with title" => [
287 'query' => wfCgiToArray( '?action=historysubmit&revisiondelete=1&title=Unused' ),
288 'expected' => 'Special:RevisionDelete',
290 "'action=editchangetags' forces Special:EditTags even with title" => [
291 'query' => wfCgiToArray( '?action=editchangetags&title=Unused' ),
292 'expected' => 'Special:EditTags',
294 "'action=historysubmit&editchangetags=1' forces Special:EditTags even with title" => [
295 'query' => wfCgiToArray( '?action=historysubmit&editchangetags=1&title=Unused' ),
296 'expected' => 'Special:EditTags',
298 "No title with 'action' still means main page" => [
299 'query' => wfCgiToArray( '?action=history' ),
300 'expected' => 'Main Page',
302 "No title with 'action=delete' does not mean main page, because we want to discourage deleting it by accident :D" => [
303 'query' => wfCgiToArray( '?action=delete' ),
304 'expected' => false,
309 private function doTestParseTitle( array $query, $expected ): void {
310 if ( $expected === false ) {
311 $this->expectException( MalformedTitleException::class );
314 $req = new FauxRequest( $query );
315 $mw = $this->getEntryPoint( $req );
317 $method = new ReflectionMethod( $mw, 'parseTitle' );
318 $method->setAccessible( true );
319 $ret = $method->invoke( $mw, $req );
321 $this->assertEquals(
322 $expected,
323 $ret->getPrefixedText()
328 * @dataProvider provideParseTitle
330 public function testParseTitle( $query, $expected ) {
331 $this->doTestParseTitle( $query, $expected );
334 public static function provideParseTitleExistingPage(): array {
335 return [
336 "Valid 'oldid'" => [
337 static fn ( WikiPage $page ): array => wfCgiToArray( '?oldid=' . $page->getRevisionRecord()->getId() ),
339 "Valid 'diff'" => [
340 static fn ( WikiPage $page ): array => wfCgiToArray( '?diff=' . $page->getRevisionRecord()->getId() ),
342 "Valid 'curid'" => [
343 static fn ( WikiPage $page ): array => wfCgiToArray( '?curid=' . $page->getId() ),
349 * @dataProvider provideParseTitleExistingPage
351 public function testParseTitle__existingPage( callable $queryBuildCallback ) {
352 $pageTitle = 'TestParseTitle test page';
353 $page = $this->getExistingTestPage( $pageTitle );
354 $query = $queryBuildCallback( $page );
355 $this->doTestParseTitle( $query, $pageTitle );
359 * Test a post-send update cannot set cookies (T191537).
360 * @coversNothing
362 public function testPostSendJobDoesNotSetCookie() {
363 // Prevent updates from running immediately by setting
364 // a plain DeferredUpdatesScopeStack which doesn't allow
365 // opportunistic updates.
366 DeferredUpdates::setScopeStack( new DeferredUpdatesScopeStack() );
368 $mw = TestingAccessWrapper::newFromObject( $this->getEntryPoint() );
370 /** @var FauxResponse $response */
371 $response = $mw->getResponse();
373 // A update that attempts to set a cookie
374 $jobHasRun = false;
375 DeferredUpdates::addCallableUpdate( static function () use ( $response, &$jobHasRun ) {
376 $jobHasRun = true;
377 $response->setCookie( 'JobCookie', 'yes' );
378 $response->header( 'Foo: baz' );
379 } );
381 $mw->doPostOutputShutdown();
383 // restInPeace() might have been registered to a callback of
384 // register_postsend_function() and thus cannot be triggered from
385 // PHPUnit.
386 if ( $jobHasRun === false ) {
387 $mw->restInPeace();
390 $this->assertTrue( $jobHasRun, 'post-send job has run' );
391 $this->assertNull( $response->getCookie( 'JobCookie' ) );
392 $this->assertNull( $response->getHeader( 'Foo' ) );
395 public function testInvalidRedirectingOnSpecialPageWithPersonallyIdentifiableTarget() {
396 $this->overrideConfigValue( MainConfigNames::HideIdentifiableRedirects, true );
398 $specialTitle = SpecialPage::getTitleFor( 'Mypage', 'in<valid' );
399 $req = new FauxRequest( [
400 'title' => $specialTitle->getPrefixedDbKey(),
401 ] );
402 $req->setRequestURL( $specialTitle->getLinkURL() );
404 $env = new MockEnvironment( $req );
405 $context = $env->makeFauxContext();
406 $context->setTitle( $specialTitle );
408 $mw = TestingAccessWrapper::newFromObject( $this->getEntryPoint( $env, $context ) );
410 $this->expectException( BadTitleError::class );
411 $this->expectExceptionMessage( 'The requested page title contains invalid characters: "<".' );
412 $mw->performRequest();
415 public function testView() {
416 $page = $this->getExistingTestPage();
418 $request = new FauxRequest( [ 'title' => $page->getTitle()->getPrefixedDBkey() ] );
419 $env = new MockEnvironment( $request );
421 $entryPoint = $this->getEntryPoint( $env );
422 $entryPoint->run();
424 $expected = '<title>(pagetitle: ' . $page->getTitle()->getPrefixedText();
425 Assert::assertStringContainsString( $expected, $entryPoint->getCapturedOutput() );