813 lines
20 KiB
PHP
813 lines
20 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
use MediaWiki\FileRepo\ThumbnailEntryPoint;
|
||
|
|
use MediaWiki\MainConfigNames;
|
||
|
|
use MediaWiki\Permissions\SimpleAuthority;
|
||
|
|
use MediaWiki\Request\FauxRequest;
|
||
|
|
use MediaWiki\Tests\FileRepo\TestRepoTrait;
|
||
|
|
use MediaWiki\Tests\MockEnvironment;
|
||
|
|
use MediaWiki\User\UserIdentityValue;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @covers \MediaWiki\FileRepo\ThumbnailEntryPoint
|
||
|
|
* @group Database
|
||
|
|
*/
|
||
|
|
class ThumbnailEntryPointTest extends MediaWikiIntegrationTestCase {
|
||
|
|
|
||
|
|
use TestRepoTrait;
|
||
|
|
use MockHttpTrait;
|
||
|
|
|
||
|
|
private const PNG_MAGIC = "\x89\x50\x4e\x47";
|
||
|
|
private const JPEG_MAGIC = "\xff\xd8\xff\xe0";
|
||
|
|
|
||
|
|
private const IMAGES_DIR = __DIR__ . '/../../data/media';
|
||
|
|
|
||
|
|
// Counter for getting unique width values
|
||
|
|
private static $uniqueWidth = 20;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* will be called only once per test class
|
||
|
|
*/
|
||
|
|
public function addDBDataOnce() {
|
||
|
|
// 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
|
||
|
|
$this->importFileToTestRepo( self::IMAGES_DIR . '/greyscale-na-png.png', 'Test.png' );
|
||
|
|
$this->importFileToTestRepo( self::IMAGES_DIR . '/portrait-rotated.jpg', 'Icon.jpg' );
|
||
|
|
|
||
|
|
// 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->db->newUpdateQueryBuilder()
|
||
|
|
->table( 'oldimage' )
|
||
|
|
->set( [ 'oi_deleted' => 1 ] )
|
||
|
|
->where( [ 'oi_archive_name' => $oldFile->getArchiveName() ] )
|
||
|
|
->caller( __METHOD__ )
|
||
|
|
->execute();
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function tearDownAfterClass(): void {
|
||
|
|
self::destroyTestRepo();
|
||
|
|
parent::tearDownAfterClass();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function setUp(): void {
|
||
|
|
parent::setUp();
|
||
|
|
|
||
|
|
$this->overrideConfigValue( MainConfigNames::ThumbLimits, [ 16, 24 ] );
|
||
|
|
$this->installTestRepoGroup();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param FauxRequest|string|array|null $request
|
||
|
|
*
|
||
|
|
* @return MockEnvironment
|
||
|
|
*/
|
||
|
|
private function makeEnvironment( $request ): MockEnvironment {
|
||
|
|
if ( !$request ) {
|
||
|
|
$request = new FauxRequest();
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( is_string( $request ) ) {
|
||
|
|
$request = [ 'f' => $request, 'width' => self::$uniqueWidth++ ];
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( is_array( $request ) ) {
|
||
|
|
$request = new FauxRequest( $request );
|
||
|
|
$request->setRequestURL( '/w/img.php' );
|
||
|
|
}
|
||
|
|
|
||
|
|
return new MockEnvironment( $request );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param MockEnvironment|null $environment
|
||
|
|
* @param FauxRequest|RequestContext|string|array|null $request
|
||
|
|
*
|
||
|
|
* @return ThumbnailEntryPoint
|
||
|
|
*/
|
||
|
|
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 );
|
||
|
|
}
|
||
|
|
|
||
|
|
$context->setLanguage( 'qqx' );
|
||
|
|
|
||
|
|
$entryPoint = new ThumbnailEntryPoint(
|
||
|
|
$context,
|
||
|
|
$environment,
|
||
|
|
$this->getServiceContainer()
|
||
|
|
);
|
||
|
|
|
||
|
|
$entryPoint->enableOutputCapture();
|
||
|
|
return $entryPoint;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNotFound() {
|
||
|
|
$env = $this->makeEnvironment( 'Missing_puppy.jpeg' );
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 404 );
|
||
|
|
$env->assertHeaderValue(
|
||
|
|
'text/html; charset=utf-8',
|
||
|
|
'Content-Type'
|
||
|
|
);
|
||
|
|
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
$this->assertStringContainsString(
|
||
|
|
'<title>Error generating thumbnail</title>',
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testGenerateAndStreamThumbnail() {
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'width' => 12 // Must match the width in testStreamExistingThumbnail
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
// TODO: Assert Content-Type and Content-Length headers.
|
||
|
|
// Needs FileStreamer to use WebResponse.
|
||
|
|
|
||
|
|
$env->assertStatusCode( 200, $output );
|
||
|
|
|
||
|
|
$this->assertThumbnail(
|
||
|
|
[ 'magic' => self::PNG_MAGIC, 'width' => 12, ],
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
|
||
|
|
return [ 'data' => $output, 'width' => 12 ];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @depends testGenerateAndStreamThumbnail
|
||
|
|
*/
|
||
|
|
public function testStreamExistingThumbnail() {
|
||
|
|
// Sabotage transformations, so this test will fail if we do not
|
||
|
|
// use the existing thumbnail generated by testGenerateAndStreamThumbnail.
|
||
|
|
$handler = $this->getMockBuilder( BitmapHandler::class )
|
||
|
|
->onlyMethods( [ 'doTransform' ] )
|
||
|
|
->getMock();
|
||
|
|
|
||
|
|
$handler->expects( $this->never() )->method( 'doTransform' );
|
||
|
|
|
||
|
|
$factory = $this->createNoOpMock( MediaHandlerFactory::class, [ 'getHandler' ] );
|
||
|
|
$factory->method( 'getHandler' )->willReturn( $handler );
|
||
|
|
$this->setService( 'MediaHandlerFactory', $factory );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'width' => 12 // Must match the width in testGenerateAndStreamThumbnail
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 200 );
|
||
|
|
|
||
|
|
$this->assertThumbnail(
|
||
|
|
[ 'magic' => self::PNG_MAGIC, 'width' => 12, ],
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNoThumbName() {
|
||
|
|
// Make sure no handler is set, so that File::generateThumbName() returns null
|
||
|
|
$factory = $this->createNoOpMock( MediaHandlerFactory::class, [ 'getHandler' ] );
|
||
|
|
$factory->method( 'getHandler' )->willReturn( false );
|
||
|
|
$this->setService( 'MediaHandlerFactory', $factory );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'width' => self::$uniqueWidth++
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 400, $output );
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testTransformError() {
|
||
|
|
// Mock transformations to return an error
|
||
|
|
$handler = $this->getMockBuilder( BitmapHandler::class )
|
||
|
|
->onlyMethods( [ 'doTransform' ] )
|
||
|
|
->getMock();
|
||
|
|
|
||
|
|
$transformOutput = new MediaTransformError( 'testing', 200, 100 );
|
||
|
|
$handler->method( 'doTransform' )->willReturn( $transformOutput );
|
||
|
|
|
||
|
|
$factory = $this->createNoOpMock( MediaHandlerFactory::class, [ 'getHandler' ] );
|
||
|
|
$factory->method( 'getHandler' )->willReturn( $handler );
|
||
|
|
$this->setService( 'MediaHandlerFactory', $factory );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'width' => self::$uniqueWidth++
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 500, $output );
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testContentDisposition() {
|
||
|
|
// TODO...
|
||
|
|
$this->markTestSkipped( 'Needs refactoring of HTTPFileStreamer to capture headers' );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'width' => 12,
|
||
|
|
'download' => 1
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 200 );
|
||
|
|
$this->assertThumbnail( [ 'magic' => self::PNG_MAGIC, ], $output );
|
||
|
|
|
||
|
|
$env->assertHeaderValue( 'attachment', 'Content-Disposition' );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function provideThumbNameParam() {
|
||
|
|
yield [ '12px-Test.png' ];
|
||
|
|
yield [ 'page123456-12px-xyz' ];
|
||
|
|
yield [ '12px-xyz' ];
|
||
|
|
yield [ 'xyzzy', 400 ];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @dataProvider provideThumbNameParam
|
||
|
|
*/
|
||
|
|
public function testThumbNameParam( $thumbName, $expected = 200 ) {
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'thumbName' => $thumbName,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( $expected, $output );
|
||
|
|
|
||
|
|
if ( $expected < 300 ) {
|
||
|
|
$expectedProps = [ 'magic' => self::PNG_MAGIC ];
|
||
|
|
|
||
|
|
// get expected width
|
||
|
|
if ( preg_match( '/\b(\d+)px/', $thumbName, $matches ) ) {
|
||
|
|
$expectedProps['width'] = (int)$matches[1];
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->assertThumbnail(
|
||
|
|
$expectedProps,
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testAccessDenied() {
|
||
|
|
// Make the wiki non-public
|
||
|
|
$groupPermissions = $this->getConfVar( MainConfigNames::GroupPermissions );
|
||
|
|
$groupPermissions['*']['read'] = false;
|
||
|
|
|
||
|
|
$this->overrideConfigValue(
|
||
|
|
'GroupPermissions',
|
||
|
|
$groupPermissions
|
||
|
|
);
|
||
|
|
|
||
|
|
// Make the user have no rights
|
||
|
|
$authority = new SimpleAuthority(
|
||
|
|
new UserIdentityValue( 7, 'Heather' ),
|
||
|
|
[]
|
||
|
|
);
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment( 'Test.png' );
|
||
|
|
|
||
|
|
$context = $env->makeFauxContext();
|
||
|
|
$context->setAuthority( $authority );
|
||
|
|
|
||
|
|
$entryPoint = $this->getEntryPoint(
|
||
|
|
$env,
|
||
|
|
$context
|
||
|
|
);
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 403 );
|
||
|
|
$env->assertHeaderValue(
|
||
|
|
'text/html; charset=utf-8',
|
||
|
|
'Content-Type'
|
||
|
|
);
|
||
|
|
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
$this->assertStringContainsString(
|
||
|
|
'<title>Error generating thumbnail</title>',
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testAccessOnPrivateWiki() {
|
||
|
|
// Make the wiki non-public, so we don't use the short-circuit code
|
||
|
|
$groupPermissions = $this->getConfVar( MainConfigNames::GroupPermissions );
|
||
|
|
$groupPermissions['*']['read'] = false;
|
||
|
|
|
||
|
|
$this->overrideConfigValue(
|
||
|
|
'GroupPermissions',
|
||
|
|
$groupPermissions
|
||
|
|
);
|
||
|
|
|
||
|
|
// Make a user who is allowed to read
|
||
|
|
$authority = new SimpleAuthority(
|
||
|
|
new UserIdentityValue( 7, 'Heather' ),
|
||
|
|
[ 'read', 'renderfile', 'renderfile-nonstandard' ]
|
||
|
|
);
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment( 'Test.png' );
|
||
|
|
|
||
|
|
$context = $env->makeFauxContext();
|
||
|
|
$context->setAuthority( $authority );
|
||
|
|
|
||
|
|
$entryPoint = $this->getEntryPoint(
|
||
|
|
$env,
|
||
|
|
$context
|
||
|
|
);
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 200 );
|
||
|
|
$this->assertThumbnail( [ 'magic' => self::PNG_MAGIC, ], $output );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function provideRateLimit() {
|
||
|
|
// NOTE: The 12px thumbnail will have been generated at this point.
|
||
|
|
// We force 16 and 24 to be standard sizes during setup.
|
||
|
|
// Once the thumbnail is generated, the rate limit is no longer
|
||
|
|
// triggered.
|
||
|
|
yield [ '16', '24', 'renderfile' ];
|
||
|
|
yield [ self::$uniqueWidth++, self::$uniqueWidth++, 'renderfile-nonstandard' ];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @dataProvider provideRateLimit
|
||
|
|
*/
|
||
|
|
public function testRateLimited( $width1, $width2, $limit ) {
|
||
|
|
// Set up rate limit config
|
||
|
|
$rateLimits = $this->getConfVar( MainConfigNames::RateLimits );
|
||
|
|
$rateLimits[$limit] = [
|
||
|
|
'ip' => [ 1, 60 ],
|
||
|
|
'newbie' => [ 1, 60 ],
|
||
|
|
'user' => [ 1, 60 ],
|
||
|
|
];
|
||
|
|
|
||
|
|
$this->overrideConfigValue( MainConfigNames::RateLimits, $rateLimits );
|
||
|
|
|
||
|
|
// First run should pass
|
||
|
|
$env = $this->makeEnvironment( [ 'f' => 'Test.png', 'width' => $width1 ] );
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$entryPoint->getCapturedOutput();
|
||
|
|
$env->assertStatusCode( 200 );
|
||
|
|
|
||
|
|
// Second run should fail
|
||
|
|
$env = $this->makeEnvironment( [ 'f' => 'Test.png', 'width' => $width2 ] );
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$entryPoint->getCapturedOutput();
|
||
|
|
$env->assertStatusCode( 429 );
|
||
|
|
|
||
|
|
$env->assertHeaderValue(
|
||
|
|
'text/html; charset=utf-8',
|
||
|
|
'Content-Type'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @depends testGenerateAndStreamThumbnail
|
||
|
|
*/
|
||
|
|
public function testStreamOldFile( array $latestThumbnailInfo ) {
|
||
|
|
$file = $this->getTestRepo()->newFile( 'Test.png' );
|
||
|
|
$history = $file->getHistory();
|
||
|
|
$oldFile = $history[0];
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => $oldFile->getArchiveName(),
|
||
|
|
'width' => '12px', // use "px" suffix, just so we also cover that code path
|
||
|
|
'archived' => 1,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
$env->assertStatusCode( 200 );
|
||
|
|
|
||
|
|
$this->assertNotSame(
|
||
|
|
$latestThumbnailInfo['data'],
|
||
|
|
$output,
|
||
|
|
'Thumbnail for the old version should not be the same as the ' .
|
||
|
|
'thumbnail for the latest version'
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertThumbnail(
|
||
|
|
[ 'magic' => self::PNG_MAGIC, 'width' => 12, ],
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testOldDeletedFile() {
|
||
|
|
// Note that we manually set oi_deleted for this revision
|
||
|
|
// in addDBDataOnce().
|
||
|
|
$file = $this->getTestRepo()->newFile( 'Icon.jpg' );
|
||
|
|
$history = $file->getHistory();
|
||
|
|
$oldFile = $history[0];
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => $oldFile->getArchiveName(),
|
||
|
|
'width' => '12px', // use "px" suffix, just so we also cover that code path
|
||
|
|
'archived' => 1,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
$env->assertStatusCode( 404, $output );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @depends testGenerateAndStreamThumbnail
|
||
|
|
*/
|
||
|
|
public function testStreamOldFileRedirect( array $latestThumbnailInfo ) {
|
||
|
|
$file = $this->getTestRepo()->newFile( 'Test.png' );
|
||
|
|
$history = $file->getHistory();
|
||
|
|
$oldFile = $history[0];
|
||
|
|
|
||
|
|
// Try accessing the old revision using a redirected title
|
||
|
|
$archiveName = str_replace(
|
||
|
|
'Test.png',
|
||
|
|
'Redirect_to_Test.png',
|
||
|
|
$oldFile->getArchiveName()
|
||
|
|
);
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => $archiveName,
|
||
|
|
'width' => 12,
|
||
|
|
'archived' => 1,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$response = $env->getFauxResponse();
|
||
|
|
|
||
|
|
$this->assertSame( 302, $response->getStatusCode() );
|
||
|
|
|
||
|
|
$expected = '/' . urlencode( $oldFile->getArchiveName() ) . '/12px-Test.png';
|
||
|
|
$this->assertStringEndsWith(
|
||
|
|
$expected,
|
||
|
|
$response->getHeader( 'Location' )
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertSame( '', $output );
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testStreamTempFile() {
|
||
|
|
$user = $this->getTestUser()->getUser();
|
||
|
|
$stash = new UploadStash( $this->getTestRepo(), $user );
|
||
|
|
$file = $stash->stashFile( self::IMAGES_DIR . '/adobergb.jpg' );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => $file->getName(),
|
||
|
|
'width' => 12,
|
||
|
|
'temp' => 'yes',
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 200 );
|
||
|
|
$this->assertThumbnail(
|
||
|
|
[ 'magic' => self::JPEG_MAGIC, 'width' => 12, ],
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testRedirect() {
|
||
|
|
$this->overrideConfigValue( MainConfigNames::VaryOnXFP, true );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Redirect_to_Test.png',
|
||
|
|
'w' => 12
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$response = $env->getFauxResponse();
|
||
|
|
|
||
|
|
$this->assertSame( 302, $response->getStatusCode() );
|
||
|
|
$this->assertStringEndsWith(
|
||
|
|
'/Test.png/12px-Test.png',
|
||
|
|
$response->getHeader( 'Location' )
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertSame( '', $output );
|
||
|
|
$env->assertHeaderValue( 'X-Forwarded-Proto', 'Vary' );
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testBadTitle() {
|
||
|
|
$env = $this->makeEnvironment( '_/_' );
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 404 );
|
||
|
|
$env->assertHeaderValue(
|
||
|
|
'text/html; charset=utf-8',
|
||
|
|
'Content-Type'
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertStringContainsString(
|
||
|
|
'(badtitletext)',
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function provideOldFileWithBadTitle() {
|
||
|
|
yield 'invalid title' => [ '_/_' ];
|
||
|
|
yield 'valid title without timestamp' => [ 'Test.png' ];
|
||
|
|
yield 'invalid title with timestamp' => [ '20200101002233!_/_' ];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @dataProvider provideOldFileWithBadTitle
|
||
|
|
*/
|
||
|
|
public function testOldFileWithBadTitle( $badTitle ) {
|
||
|
|
$env = $this->makeEnvironment( [
|
||
|
|
'f' => $badTitle,
|
||
|
|
'width' => 12,
|
||
|
|
'archived' => 1
|
||
|
|
] );
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 404 );
|
||
|
|
$env->assertHeaderValue(
|
||
|
|
'text/html; charset=utf-8',
|
||
|
|
'Content-Type'
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertStringContainsString(
|
||
|
|
'(badtitletext)',
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testTooMuchWidth() {
|
||
|
|
// Set the width larger than the size of the image
|
||
|
|
$env = $this->makeEnvironment( [ 'f' => 'Test.png', 'width' => 1200 ] );
|
||
|
|
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 400 );
|
||
|
|
$env->assertHeaderValue(
|
||
|
|
'text/html; charset=utf-8',
|
||
|
|
'Content-Type'
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertStringContainsString(
|
||
|
|
'(thumbnail_error: ',
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
$this->assertStringContainsString(
|
||
|
|
'bigger than the source',
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testDeletedFile() {
|
||
|
|
// Delete Icon.jpg
|
||
|
|
$icon = $this->getTestRepo()->newFile( 'Icon.jpg' );
|
||
|
|
|
||
|
|
$this->assertTrue( $icon->exists() );// sanity
|
||
|
|
$icon->deleteFile( 'testing', new UserIdentityValue( 0, 'Test' ) );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment( 'Icon.jpg' );
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 404 );
|
||
|
|
$env->assertHeaderValue(
|
||
|
|
'text/html; charset=utf-8',
|
||
|
|
'Content-Type'
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->assertStringContainsString(
|
||
|
|
'<title>Error generating thumbnail</title>',
|
||
|
|
$output
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testNotModified() {
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'width' => 12
|
||
|
|
]
|
||
|
|
);
|
||
|
|
|
||
|
|
$env->setServerInfo( 'HTTP_IF_MODIFIED_SINCE', '25250101001122' );
|
||
|
|
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$response = $env->getFauxResponse();
|
||
|
|
$this->assertSame( 304, $response->getStatusCode() );
|
||
|
|
$this->assertSame( '', $output );
|
||
|
|
}
|
||
|
|
|
||
|
|
public function testProxy() {
|
||
|
|
$this->installTestRepoGroup( [ 'thumbProxyUrl' => 'https://images.acme.test/thumbnails/' ] );
|
||
|
|
$this->installMockHttp( 'PROXY RESPONSE' );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'width' => self::$uniqueWidth++
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$this->assertSame( 'PROXY RESPONSE', $output );
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function provideRepoCouldNotStreamFile() {
|
||
|
|
// The width must match the one generated by testGenerateAndStreamThumbnail
|
||
|
|
// This error comes from FileBackend::doStreamFile.
|
||
|
|
yield 'existing thumbnail' => [ 12, 'xyzzy-error' ];
|
||
|
|
|
||
|
|
// TODO: also test the case where we fail to stream a newly created
|
||
|
|
// thumbnail. In that case, the expected error comes from
|
||
|
|
// MediaTransformOutput::streamFileWithStatus, not FileBackend::doStreamFile.
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @dataProvider provideRepoCouldNotStreamFile
|
||
|
|
* @depends testGenerateAndStreamThumbnail
|
||
|
|
*/
|
||
|
|
public function testRepoCouldNotStreamFile( int $width, string $expectedError ) {
|
||
|
|
// Sabotage streaming in file backend
|
||
|
|
$backend = $this->createFileBackend( [
|
||
|
|
'overrides' => [
|
||
|
|
'doStreamFile' => Status::newFatal( 'xyzzy-error' )
|
||
|
|
]
|
||
|
|
] );
|
||
|
|
|
||
|
|
$this->installTestRepoGroup(
|
||
|
|
[ 'backend' => $backend ]
|
||
|
|
);
|
||
|
|
|
||
|
|
// TODO: figure out how to provoke an error in
|
||
|
|
// MediaTransformOutput::streamFileWithStatus.
|
||
|
|
// The below causes an error to be triggered too early.
|
||
|
|
// Since MediaTransformOutput uses StreamFile directly, we have to also
|
||
|
|
// sabotage transformations in the handler to return a ThumbnailImage
|
||
|
|
// with no path. This is unfortunately brittle to implementation changes.
|
||
|
|
$handler = $this->getMockBuilder( BitmapHandler::class )
|
||
|
|
->onlyMethods( [ 'doTransform' ] )
|
||
|
|
->getMock();
|
||
|
|
|
||
|
|
$file = $this->getTestRepo()->newFile( 'Test.png' );
|
||
|
|
$params = [ 'width' => $width, 'height' => $width ];
|
||
|
|
$handler->method( 'doTransform' )->willReturn(
|
||
|
|
new ThumbnailImage( $file, '', false, $params )
|
||
|
|
);
|
||
|
|
|
||
|
|
$factory = $this->createNoOpMock( MediaHandlerFactory::class, [ 'getHandler' ] );
|
||
|
|
$factory->method( 'getHandler' )->willReturn( $handler );
|
||
|
|
$this->setService( 'MediaHandlerFactory', $factory );
|
||
|
|
|
||
|
|
$env = $this->makeEnvironment(
|
||
|
|
[
|
||
|
|
'f' => 'Test.png',
|
||
|
|
'width' => $width
|
||
|
|
]
|
||
|
|
);
|
||
|
|
$entryPoint = $this->getEntryPoint( $env );
|
||
|
|
|
||
|
|
$entryPoint->run();
|
||
|
|
$output = $entryPoint->getCapturedOutput();
|
||
|
|
|
||
|
|
$env->assertStatusCode( 500, $output );
|
||
|
|
$env->assertHeaderValue(
|
||
|
|
'text/html; charset=utf-8',
|
||
|
|
'Content-Type'
|
||
|
|
);
|
||
|
|
|
||
|
|
// TODO: check the log for the specific error.
|
||
|
|
$this->assertStringContainsString( 'Could not stream the file', $output );
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param array $props
|
||
|
|
* @param string $output binary data
|
||
|
|
*/
|
||
|
|
private function assertThumbnail( array $props, string $output ): void {
|
||
|
|
if ( isset( $props['magic'] ) ) {
|
||
|
|
$this->assertStringStartsWith(
|
||
|
|
$props['magic'],
|
||
|
|
$output,
|
||
|
|
'Magic number should match'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ( isset( $props['width'] ) && function_exists( 'getimagesizefromstring' ) ) {
|
||
|
|
[ $width, ] = getimagesizefromstring( $output );
|
||
|
|
$this->assertSame(
|
||
|
|
$props['width'],
|
||
|
|
$width
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|