wiki.techinc.nl/tests/phpunit/includes/filerepo/AuthenticatedFileEntryPointTest.php
Kosta Harlan c9906b28cb
AuthenticatedFileEntryPointTest: Set named user in context
Why:

- The default uploader is the context user, which defaults to an
  anonymous user, and that will fail with temp accounts enabled

What:

- Set a default context user as a named test user account

Bug: T365669
Change-Id: Idfd6d8bc1f4726f5330329c0581f63f9b65dfa23
2024-06-27 08:47:40 +02:00

467 lines
12 KiB
PHP

<?php
use MediaWiki\Context\RequestContext;
use MediaWiki\FileRepo\AuthenticatedFileEntryPoint;
use MediaWiki\MainConfigNames;
use MediaWiki\Request\FauxRequest;
use MediaWiki\Tests\FileRepo\TestRepoTrait;
use MediaWiki\Tests\MockEnvironment;
use MediaWiki\Title\Title;
/**
* @covers \MediaWiki\FileRepo\AuthenticatedFileEntryPoint
* @group Database
*/
class AuthenticatedFileEntryPointTest extends MediaWikiIntegrationTestCase {
use TestRepoTrait;
private const PNG_MAGIC = "\x89\x50\x4e\x47";
private const JPEG_MAGIC = "\xff\xd8\xff\xe0";
private const IMAGES_DIR = __DIR__ . '/../../data/media';
private ?MockEnvironment $environment = null;
/**
* will be called only once per test class
*/
public function addDBDataOnce() {
// Set a named user account for the request context as the default,
// so that these tests do not fail with temp accounts enabled
RequestContext::getMain()->setUser( $this->getTestUser()->getUser() );
// Create mock repo with test files
$this->initTestRepoGroup();
$this->importFileToTestRepo( self::IMAGES_DIR . '/greyscale-png.png', 'Test.png' );
$this->importFileToTestRepo( self::IMAGES_DIR . '/test.jpg', 'Icon.jpg' );
// Create a second version of Test.png and Icon.jpg
$this->importFileToTestRepo( self::IMAGES_DIR . '/greyscale-na-png.png', 'Test.png' );
$this->importFileToTestRepo( self::IMAGES_DIR . '/portrait-rotated.jpg', 'Icon.jpg' );
// Create a thumbnail
$this->copyFileToTestBackend(
self::IMAGES_DIR . '/greyscale-na-png.png',
'/thumb/Test.png'
);
// Create a redirect
$title = Title::makeTitle( NS_FILE, 'Redirect_to_Test.png' );
$this->editPage( $title, '#REDIRECT [[File:Test.png]]' );
// Suppress the old version of Icon
$file = $this->getTestRepo()->newFile( 'Icon.jpg' );
$history = $file->getHistory();
$oldFile = $history[0];
$this->getDb()->newUpdateQueryBuilder()
->table( 'oldimage' )
->set( [ 'oi_deleted' => 1 ] )
->where( [ 'oi_archive_name' => $oldFile->getArchiveName() ] )
->caller( __METHOD__ )
->execute();
}
protected function setUp(): void {
parent::setUp();
$this->overrideConfigValue(
MainConfigNames::ImgAuthDetails,
true
);
$this->overrideConfigValue(
MainConfigNames::ForeignFileRepos,
[]
);
$this->overrideConfigValue(
MainConfigNames::UseInstantCommons,
false
);
$this->overrideConfigValue(
MainConfigNames::ImgAuthUrlPathMap,
[ '/testing' => 'mwstore://test/test-thumb/' ]
);
$this->overrideConfigValue(
MainConfigNames::ImgAuthPath,
'/img_auth/'
);
$this->installTestRepoGroup();
}
private function recordHeader( string $header ) {
$this->environment->getFauxResponse()->header( $header );
}
private function getFileUrlPath( string $name, string $prefix = '' ): string {
if ( $prefix !== '' && !str_ends_with( $prefix, '/' ) ) {
$prefix = $prefix . '/';
}
if ( !str_starts_with( $prefix, '/' ) ) {
// Unauthenticated path
$prefix = '/w/images/' . $prefix;
}
$file = $this->getTestRepo()->newFile( $name );
if ( $file ) {
$name = $file->getRel();
}
return $prefix . $name;
}
/**
* @param FauxRequest|string|array|null $request
*
* @return MockEnvironment
*/
private function makeEnvironment( $request ): MockEnvironment {
if ( !$request ) {
$request = new FauxRequest();
}
if ( is_string( $request ) ) {
$url = $request;
$request = new FauxRequest();
$request->setRequestURL( $url );
}
if ( is_array( $request ) ) {
$request = new FauxRequest( $request );
}
$this->environment = new MockEnvironment( $request );
return $this->environment;
}
/**
* @param MockEnvironment|null $environment
* @param FauxRequest|RequestContext|string|array|null $request
*
* @return AuthenticatedFileEntryPoint
*/
private function getEntryPoint( MockEnvironment $environment = null, $request = null ) {
if ( !$request && $environment ) {
$request = $environment->getFauxRequest();
}
if ( $request instanceof RequestContext ) {
$context = $request;
$request = $context->getRequest();
} else {
$context = new RequestContext();
$context->setRequest( $request );
$context->setUser( $this->getTestUser()->getUser() );
}
if ( !$environment ) {
$environment = $this->makeEnvironment( $request );
}
$entryPoint = new AuthenticatedFileEntryPoint(
$context,
$environment,
$this->getServiceContainer()
);
$entryPoint->enableOutputCapture();
return $entryPoint;
}
public static function provideGetRequestPathSuffix() {
yield [ '/upload', '/upload/file', 'file' ];
yield [ '/upload', '/upload/file?q=x', 'file' ];
yield [ '/upload', '/upload/x%25y', 'x%y' ];
yield [ '/foo', '/upload/file', false ];
}
/**
* @dataProvider provideGetRequestPathSuffix
*
* @param string $basePath
* @param string $requestURL
* @param string|false $expected
*
* @covers \MediaWiki\MediaWikiEntryPoint::getRequestPathSuffix
*/
public function testGetRequestPathSuffix( string $basePath, string $requestURL, $expected ) {
$entryPoint = $this->getEntryPoint( $this->makeEnvironment( $requestURL ) );
$this->assertSame( $expected, $entryPoint->getRequestPathSuffix( $basePath ) );
}
public static function provideStreamFile() {
yield 'public wiki' => [
'',
];
yield 'private wiki' => [
'',
[
'*' => [],
'user' => [ 'read' => true ],
],
[],
[],
[
'cache-control' => 'private',
'vary' => 'Cookie',
]
];
yield 'range' => [
'',
[],
[],
[ 'HTTP_RANGE' => 'bytes=0-99' ],
[ 'content-range' => 'bytes 0-99/365', 'content-length' => '100' ],
206
];
yield 'download' => [
'',
[],
[ 'download' => 1 ],
[],
[ 'content-disposition' => 'attachment' ]
];
yield 'thumb zone' => [
// Path under /w/images/
'thumb',
];
yield 'mapped prefix' => [
// Path under /w/images/
'testing', // per ImgAuthUrlPathMap
];
yield 'use ImgAuthPath' => [
// If the prefix starts with a "/" it's the full path.
'/img_auth/', // per ImgAuthPath
];
}
/**
* @dataProvider provideStreamFile
*
* @param string $prefix
* @param array $permissions
* @param array $requestData
* @param array $serverInfo
* @param array $expectedHeaders
* @param int $expectedCode
*
* @throws Exception
*/
public function testStreamFile(
string $prefix,
array $permissions = [],
array $requestData = [],
array $serverInfo = [],
array $expectedHeaders = [],
int $expectedCode = 200
) {
if ( !isset( $permissions['*'] ) ) {
// public wiki
$permissions['*'] = [ 'read' => true ];
}
$this->overrideConfigValue( MainConfigNames::GroupPermissions, $permissions );
$name = 'Test.png';
$url = $this->getFileUrlPath( $name, $prefix );
$request = new FauxRequest( $requestData );
$request->setRequestURL( $url );
$env = $this->makeEnvironment( $request );
foreach ( $serverInfo as $key => $value ) {
$env->setServerInfo( $key, $value );
}
$entryPoint = $this->getEntryPoint( $env );
$entryPoint->run();
$data = $entryPoint->getCapturedOutput();
$env->assertStatusCode( $expectedCode, $data );
$this->assertStringStartsWith(
self::PNG_MAGIC,
$data
);
$env->assertHeaderValue( 'image/png', 'Content-Type' );
foreach ( $expectedHeaders as $name => $exp ) {
$env->assertHeaderValue( $exp, $name );
}
}
public function testStreamFile_archive() {
$this->overrideConfigValue(
MainConfigNames::GroupPermissions,
[ '*' => [ 'read' => true ] ]
);
$name = 'Test.png';
$file = $this->getTestRepo()->newFile( $name );
$history = $file->getHistory();
$oldFile = $history[0];
$url = '/img_auth/' . $oldFile->getArchiveRel() . '/' . $oldFile->getArchiveName();
$env = $this->makeEnvironment( $url );
$entryPoint = $this->getEntryPoint( $env );
$entryPoint->run();
$data = $entryPoint->getCapturedOutput();
$env->assertStatusCode( 200, $data );
}
public function testNotModified() {
$this->overrideConfigValue(
MainConfigNames::GroupPermissions,
[ '*' => [ 'read' => true ] ]
);
$url = $this->getFileUrlPath( 'Test.png' );
$env = $this->makeEnvironment( $url );
$env->setServerInfo( 'HTTP_IF_MODIFIED_SINCE', '25250101001122' );
$entryPoint = $this->getEntryPoint( $env );
$entryPoint->run();
// Not modified
$env->assertStatusCode( 304 );
}
public function testAccessDenied_deleted() {
$this->overrideConfigValue(
MainConfigNames::GroupPermissions,
[ '*' => [ 'read' => true ] ]
);
$name = 'Icon.jpg';
$file = $this->getTestRepo()->newFile( $name );
$history = $file->getHistory();
// This old revision is marked as deleted (supressed) in the database
$oldFile = $history[0];
$url = '/img_auth/' . $oldFile->getArchiveRel() . '/' . $oldFile->getArchiveName();
$env = $this->makeEnvironment( $url );
$entryPoint = $this->getEntryPoint( $env );
$entryPoint->run();
$data = $entryPoint->getCapturedOutput();
$env->assertStatusCode( 403, $data );
}
public static function provideAccessDenied() {
yield 'no prefix' => [ '' ];
yield 'thumb zone' => [ 'thumb' ];
yield 'mapped prefix' => [ 'testing' ];
}
/**
* @dataProvider provideAccessDenied
*/
public function testAccessDenied(
string $prefix,
string $expected = 'User does not have access to read'
) {
$this->overrideConfigValue(
MainConfigNames::GroupPermissions,
[ '*' => [], 'user' => [], ]
);
$env = $this->makeEnvironment( $this->getFileUrlPath( 'Test.png', $prefix ) );
$entryPoint = $this->getEntryPoint( $env );
$entryPoint->run();
$output = $entryPoint->getCapturedOutput();
$env->assertStatusCode( 403 );
$env->assertHeaderValue( 'no-cache', 'cache-control' );
$this->assertStringContainsString( '<h1>Access denied</h1>', $output );
$this->assertStringContainsString( $expected, $output );
}
public function testAccessDenied_hook() {
$this->setUserLang( 'qqx' );
$this->overrideConfigValue(
MainConfigNames::GroupPermissions,
[ '*' => [], 'user' => [ 'read' => true ], ]
);
$this->setTemporaryHook(
'ImgAuthBeforeStream',
static function ( $title, $path, $name, ?array &$result ) {
$result = [ 'test-title', 'test-detail' ];
return false;
}
);
$env = $this->makeEnvironment( $this->getFileUrlPath( 'Test.png' ) );
$entryPoint = $this->getEntryPoint( $env );
$entryPoint->run();
$output = $entryPoint->getCapturedOutput();
$env->assertStatusCode( 403 );
$env->assertHeaderValue( 'no-cache', 'cache-control' );
$this->assertStringContainsString( '<h1>⧼test-title⧽</h1>', $output );
$this->assertStringContainsString( '<p>⧼test-detail⧽</p>', $output );
}
public static function provideNotFOund() {
yield 'no prefix, missing file' =>
[ 'No-such-file.png', '' ];
yield 'no prefix, bad title' =>
[ '_<>_', '' ];
yield 'thumb zone' =>
[ 'No-such-file.png', 'thumb' ];
yield 'mapped prefix' =>
[ 'No-such-file.png', 'testing' ];
yield 'unrecognized base path' =>
[
'No-such-file.png',
'/bad/base/path',
'Requested path is not in the configured',
];
}
/**
* @dataProvider provideNotFOund
*/
public function testNotFound( string $name, string $prefix, $expected = 'does not exist' ) {
$this->overrideConfigValue(
MainConfigNames::GroupPermissions,
[
'*' => [ 'read' => 'true' ],
]
);
$env = $this->makeEnvironment( $this->getFileUrlPath( $name, $prefix ) );
$entryPoint = $this->getEntryPoint( $env );
$entryPoint->run();
$output = $entryPoint->getCapturedOutput();
// Missing files are also "forbidden"
$env->assertStatusCode( 403 );
$env->assertHeaderValue( 'no-cache', 'cache-control' );
$this->assertStringContainsString(
'<h1>Access denied</h1>',
$output
);
$this->assertStringContainsString(
$expected,
$output
);
}
}