3 namespace MediaWiki\Tests\Action
;
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
;
23 use Wikimedia\TestingAccessWrapper
;
26 // phpcs:disable MediaWiki.Usage.SuperGlobalsUsage.SuperGlobals
30 * @covers \MediaWiki\Actions\ActionEntryPoint
32 class ActionEntryPointTest
extends MediaWikiIntegrationTestCase
{
33 use TempUserTestTrait
;
35 protected function setUp(): void
{
38 $this->overrideConfigValues( [
39 MainConfigNames
::Server
=> 'http://example.org',
40 MainConfigNames
::ScriptPath
=> '/w',
41 MainConfigNames
::Script
=> '/w/index.php',
42 MainConfigNames
::LanguageCode
=> 'en',
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() );
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(),
77 $this->getServiceContainer()
79 $entryPoint->enableOutputCapture();
84 public static function provideTryNormaliseRedirect() {
88 'url' => 'http://example.org/wiki/Foo_Bar',
94 // View: Escaped title
95 'url' => 'http://example.org/wiki/Foo%20Bar',
98 'redirect' => 'http://example.org/wiki/Foo_Bar',
102 'url' => 'http://example.org/w/index.php?title=Foo_Bar',
103 'query' => [ 'title' => 'Foo_Bar' ],
104 'title' => 'Foo_Bar',
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',
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',
122 // View: Script path without title
123 'url' => 'http://example.org/w/index.php',
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',
150 // View: Root path with escaped title
151 'url' => 'http://example.org/?title=Foo_Bar',
152 'query' => [ 'title' => 'Foo_Bar' ],
153 'title' => 'Foo_Bar',
157 // View: Canonical with redundant query
158 'url' => 'http://example.org/wiki/Foo_Bar?action=view',
159 'query' => [ 'action' => 'view' ],
160 'title' => 'Foo_Bar',
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',
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',
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',
185 // Path with double slash prefix (T100782)
186 'url' => 'http://example.org//wiki/Double_slash',
188 'title' => 'Double_slash',
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 );
221 $expectedRedirect !== false,
223 'Return true only when redirecting'
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 );
244 $expected = '<title>(pagetitle: Main Page)';
245 Assert
::assertStringContainsString( $expected, $entryPoint->getCapturedOutput() );
248 public static function provideParseTitle() {
250 "No title means main page" => [
252 'expected' => 'Main Page',
254 "Empty title also means main page" => [
255 'query' => wfCgiToArray( '?title=' ),
256 'expected' => 'Main Page',
259 'query' => wfCgiToArray( '?title=Foo' ),
263 'query' => wfCgiToArray( '?title=[INVALID]' ),
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' ),
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' ),
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 );
323 $ret->getPrefixedText()
328 * @dataProvider provideParseTitle
330 public function testParseTitle( $query, $expected ) {
331 $this->doTestParseTitle( $query, $expected );
334 public static function provideParseTitleExistingPage(): array {
337 static fn ( WikiPage
$page ): array => wfCgiToArray( '?oldid=' . $page->getRevisionRecord()->getId() ),
340 static fn ( WikiPage
$page ): array => wfCgiToArray( '?diff=' . $page->getRevisionRecord()->getId() ),
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).
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
375 DeferredUpdates
::addCallableUpdate( static function () use ( $response, &$jobHasRun ) {
377 $response->setCookie( 'JobCookie', 'yes' );
378 $response->header( 'Foo: baz' );
381 $mw->doPostOutputShutdown();
383 // restInPeace() might have been registered to a callback of
384 // register_postsend_function() and thus cannot be triggered from
386 if ( $jobHasRun === false ) {
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(),
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 );
424 $expected = '<title>(pagetitle: ' . $page->getTitle()->getPrefixedText();
425 Assert
::assertStringContainsString( $expected, $entryPoint->getCapturedOutput() );