3 use MediaWiki\Context\RequestContext
;
4 use MediaWiki\FileRepo\Thumbnail404EntryPoint
;
5 use MediaWiki\MainConfigNames
;
6 use MediaWiki\Request\FauxRequest
;
7 use MediaWiki\Tests\FileRepo\TestRepoTrait
;
8 use MediaWiki\Tests\MockEnvironment
;
9 use MediaWiki\Title\Title
;
12 * @covers \MediaWiki\FileRepo\Thumbnail404EntryPoint
15 class Thumbnail404EntryPointTest
extends MediaWikiIntegrationTestCase
{
20 private const PNG_MAGIC
= "\x89\x50\x4e\x47";
21 private const JPEG_MAGIC
= "\xff\xd8\xff\xe0";
23 private const IMAGES_DIR
= __DIR__
. '/../../data/media';
26 * will be called only once per test class
28 public function addDBDataOnce() {
29 // Set a named user account for the request context as the default,
30 // so that these tests do not fail with temp accounts enabled
31 RequestContext
::getMain()->setUser( $this->getTestUser()->getUser() );
32 // Create mock repo with test files
33 $this->initTestRepoGroup();
35 $this->importFileToTestRepo( self
::IMAGES_DIR
. '/greyscale-png.png', 'Test.png' );
36 $this->importFileToTestRepo( self
::IMAGES_DIR
. '/Animated_PNG_example_bouncing_beach_ball.png' );
37 $this->importFileToTestRepo( self
::IMAGES_DIR
. '/test.jpg', 'Icon.jpg' );
39 // Create a second version of Test.png
40 $this->importFileToTestRepo( self
::IMAGES_DIR
. '/greyscale-dot-na-png.png', 'Test.png' );
43 $title = Title
::makeTitle( NS_FILE
, 'Redirect_to_Test.png' );
44 $this->editPage( $title, '#REDIRECT [[File:Test.png]]' );
47 public static function tearDownAfterClass(): void
{
48 self
::destroyTestRepo();
49 parent
::tearDownAfterClass();
52 public function setUp(): void
{
55 $this->installTestRepoGroup();
59 * @param FauxRequest|string|null $request
61 * @return MockEnvironment
63 private function makeEnvironment( $request ): MockEnvironment
{
65 $request = new FauxRequest();
68 if ( is_string( $request ) ) {
69 $req = new FauxRequest( [] );
70 $req->setRequestURL( $request );
74 return new MockEnvironment( $request );
78 * @param MockEnvironment|null $environment
79 * @param FauxRequest|RequestContext|string|array|null $request
81 * @return Thumbnail404EntryPoint
83 private function getEntryPoint(
84 ?MockEnvironment
$environment = null,
87 if ( !$request && $environment ) {
88 $request = $environment->getFauxRequest();
91 if ( $request instanceof RequestContext
) {
93 $request = $context->getRequest();
95 $context = new RequestContext();
96 $context->setRequest( $request );
97 $context->setUser( $this->getTestUser()->getUser() );
100 if ( !$environment ) {
101 $environment = $this->makeEnvironment( $request );
104 $entryPoint = new Thumbnail404EntryPoint(
107 $this->getServiceContainer()
110 $entryPoint->enableOutputCapture();
114 public static function provideNotFound() {
115 yield
'non-existing image' => [
116 '/w/images/thumb/a/aa/Xyzzy.png/13px-Xyzzy.png',
119 yield
'malformed name' => [
120 '/w/images/thumb/x/xx/XyzzyXyzzy',
126 * @dataProvider provideNotFound
128 public function testNotFound( $req, $expectedStatus ) {
129 $env = $this->makeEnvironment( $req );
130 $entryPoint = $this->getEntryPoint( $env );
133 $output = $entryPoint->getCapturedOutput();
135 $env->assertStatusCode( $expectedStatus );
136 $env->assertHeaderValue(
137 'text/html; charset=utf-8',
141 $this->assertStringContainsString(
142 '<title>Error generating thumbnail</title>',
147 public function testStreamFile() {
148 $file = $this->getTestRepo()->newFile( 'Test.png' );
149 $rel = $file->getRel();
150 $name = $file->getName();
152 $env = $this->makeEnvironment( "/w/images/thumb/$rel/13px-$name" );
153 $entryPoint = $this->getEntryPoint( $env );
156 $output = $entryPoint->getCapturedOutput();
158 $env->assertStatusCode( 200 );
160 $this->assertThumbnail(
161 [ 'magic' => self
::PNG_MAGIC
, 'width' => 13, ],
165 return [ 'data' => $output, 'width' => 13 ];
168 public function testStreamFileWithThumbPath() {
169 $this->overrideConfigValue( MainConfigNames
::ThumbPath
, '/thumbnails/' );
171 $file = $this->getTestRepo()->newFile( 'Test.png' );
172 $rel = $file->getRel();
174 $env = $this->makeEnvironment( "/thumbnails/$rel/13px-Test.png" );
175 $entryPoint = $this->getEntryPoint( $env );
178 $output = $entryPoint->getCapturedOutput();
180 $env->assertStatusCode( 200 );
182 $this->assertThumbnail(
183 [ 'magic' => self
::PNG_MAGIC
, 'width' => 13, ],
188 public function testStreamFileWithLongName() {
189 $this->overrideConfigValue( MainConfigNames
::VaryOnXFP
, true );
191 // Note that abbrvThreshold is 16 per MockRepTrait
192 $file = $this->getTestRepo()->newFile( 'Animated_PNG_example_bouncing_beach_ball.png' );
193 $rel = $file->getRel();
194 $name = $file->getName();
196 // use abbreviated name
197 $env = $this->makeEnvironment( "/w/images/thumb/$rel/13px-thumbnail.png" );
198 $entryPoint = $this->getEntryPoint( $env );
201 $output = $entryPoint->getCapturedOutput();
203 $env->assertStatusCode( 200, $output );
204 $env->assertHeaderValue( null, 'Vary' );
207 $env = $this->makeEnvironment( "/w/images/thumb/$rel/13px-$name" );
208 $entryPoint = $this->getEntryPoint( $env );
211 $output = $entryPoint->getCapturedOutput();
213 $env->assertStatusCode( 301, $output );
214 $env->assertHeaderValue( 'X-Forwarded-Proto', 'Vary' );
216 $this->assertStringEndsWith(
217 "/w/images/thumb/$rel/13px-thumbnail.png",
218 $env->getFauxResponse()->getHeader( 'Location' )
223 * @depends testStreamFile
225 public function testStreamOldFile( array $latestThumbnailInfo ) {
226 $file = $this->getTestRepo()->newFile( 'Test.png' );
227 $history = $file->getHistory();
228 $oldFile = $history[0];
230 $this->assertNotSame(
233 'Old and latest file version should not have the same size'
236 $curThumbPath = $file->getThumbPath( '13px-Test.png' );
237 $oldThumbPath = $oldFile->getThumbPath( '13px-Test.png' );
239 $file->getRepo()->getBackend()->fileExists( [ 'src' => $oldThumbPath ] ),
240 'Thumbnail for old file version does not exist'
243 $uri = '/w/images/thumb/' . $oldFile->getArchiveRel()
244 . '/' . $oldFile->getArchiveName() . '/13px-Test.png';
246 $env = $this->makeEnvironment( $uri );
247 $entryPoint = $this->getEntryPoint( $env );
250 $output = $entryPoint->getCapturedOutput();
251 $env->assertStatusCode( 200 );
253 $this->assertNotSame(
254 $file->getRepo()->getBackend()->getFileSha1Base36( [ 'src' => $oldThumbPath ] ),
255 $file->getRepo()->getBackend()->getFileSha1Base36( [ 'src' => $curThumbPath ] ),
256 "Thumbnails at $oldThumbPath and $curThumbPath should have different hashes"
259 $this->assertNotSame(
260 $latestThumbnailInfo['data'],
262 'Thumbnail for the old version should not be the same as the ' .
263 'thumbnail for the latest version'
266 $this->assertThumbnail(
267 [ 'magic' => self
::PNG_MAGIC
, 'width' => 13, ],
272 public function testStreamTempFile() {
273 $user = $this->getTestUser()->getUser();
274 $stash = new UploadStash( $this->getTestRepo(), $user );
275 $file = $stash->stashFile( self
::IMAGES_DIR
. '/adobergb.jpg' );
277 $uri = '/w/images/thumb/temp/' . $file->getRel()
278 . '/13px-' . $file->getName();
280 $env = $this->makeEnvironment( $uri );
281 $entryPoint = $this->getEntryPoint( $env );
284 $output = $entryPoint->getCapturedOutput();
286 $env->assertStatusCode( 200 );
287 $this->assertThumbnail(
288 [ 'magic' => self
::JPEG_MAGIC
, 'width' => 13, ],
293 public function testBadPath() {
294 $file = $this->getTestRepo()->newFile( 'Test.png' );
295 $rel = $file->getRel();
297 $uri = "/w/images/thumb/$rel/148px-XYZZY";
299 $env = $this->makeEnvironment( $uri );
300 $entryPoint = $this->getEntryPoint( $env );
303 $entryPoint->getCapturedOutput();
305 $env->assertStatusCode( 404 );
309 * @param array $props
310 * @param string $output binary data
312 private function assertThumbnail( array $props, string $output ): void
{
313 if ( isset( $props['magic'] ) ) {
314 $this->assertStringStartsWith(
317 'Magic number should match'
321 if ( isset( $props['width'] ) && function_exists( 'getimagesizefromstring' ) ) {
322 [ $width, ] = getimagesizefromstring( $output );