104 $fileName =
$params[
'f'] ??
'';
111 if ( isset(
$params[
'width'] ) && substr(
$params[
'width'], -2 ) ==
'px' ) {
130 $fileName = strtr( $fileName,
'\\/',
'__' );
131 $localRepo = $services->getRepoGroup()->getLocalRepo();
132 $archiveTimestamp =
null;
136 $repo = $localRepo->getTempRepo();
138 # Temp files are hashed based on the name without the timestamp.
139 # The thumbnails will be hashed based on the entire name however.
140 # @todo fix
this convention to actually be reasonable.
141 $repo->getZonePath(
'public' ) .
'/' . $repo->getTempHashPath( $fileName ) . $fileName
143 } elseif ( $isOld ) {
145 $bits = explode(
'!', $fileName, 2 );
146 if ( count( $bits ) != 2 ) {
150 $archiveTimestamp = $bits[0];
151 $title = Title::makeTitleSafe(
NS_FILE, $bits[1] );
156 $img = $localRepo->newFromArchiveName( $title, $fileName );
158 $img = $localRepo->newFile( $fileName );
169 if ( !$services->getGroupPermissionsLookup()->groupHasPermission(
'*',
'read' ) ) {
170 $authority = $this->
getContext()->getAuthority();
171 $imgTitle = $img->getTitle();
173 if ( !$imgTitle || !$authority->authorizeRead(
'read', $imgTitle ) ) {
174 $this->
thumbErrorText( 403,
'Access denied. You do not have permission to access ' .
175 'the source file.' );
178 $headers[] =
'Cache-Control: private';
179 $varyHeader[] =
'Cookie';
183 if ( $img->isDeleted( File::DELETED_FILE ) ) {
184 $this->
thumbErrorText( 404,
"The source file '$fileName' does not exist." );
189 if ( isset(
$params[
'thumbName'] ) ) {
193 $this->
thumbErrorText( 400,
'The specified thumbnail parameters are not recognized.' );
198 if ( !$img->exists() ) {
199 $redirectedLocation =
false;
204 $possRedirFile = $localRepo->findFile( $img->getName() );
205 if ( $possRedirFile && $possRedirFile->getRedirected() !==
null ) {
206 $redirTarget = $possRedirFile->getName();
207 $targetFile = $localRepo->newFile( Title::makeTitleSafe(
NS_FILE, $redirTarget ) );
208 if ( $targetFile->exists() ) {
209 $newThumbName = $targetFile->thumbName(
$params );
211 $newThumbUrl = $targetFile->getArchiveThumbUrl(
212 $archiveTimestamp .
'!' . $targetFile->getName(), $newThumbName );
214 $newThumbUrl = $targetFile->getThumbUrl( $newThumbName );
221 if ( $redirectedLocation ) {
224 $response->statusHeader( 302 );
225 $response->header(
'Location: ' . $redirectedLocation );
226 $response->header(
'Expires: ' .
227 gmdate(
'D, d M Y H:i:s', time() + 12 * 3600 ) .
' GMT' );
229 $varyHeader[] =
'X-Forwarded-Proto';
231 if ( count( $varyHeader ) ) {
232 $response->header(
'Vary: ' . implode(
', ', $varyHeader ) );
234 $response->header(
'Content-Length: 0' );
239 $this->
thumbErrorText( 404,
"The source file '$fileName' does not exist." );
241 } elseif ( $img->getPath() ===
false ) {
242 $this->
thumbErrorText( 400,
"The source file '$fileName' is not locally accessible." );
248 if ( $this->
getServerInfo(
'HTTP_IF_MODIFIED_SINCE',
'' ) !==
'' ) {
250 $imsString = preg_replace(
256 AtEase::suppressWarnings();
257 $imsUnix = strtotime( $imsString );
258 AtEase::restoreWarnings();
259 if (
wfTimestamp( TS_UNIX, $img->getTimestamp() ) <= $imsUnix ) {
265 $rel404 =
$params[
'rel404'] ??
null;
272 $thumbName = $img->thumbName(
$params );
273 if ( !strlen( $thumbName ??
'' ) ) {
275 'Empty return from File::thumbName'
278 $thumbName2 = $img->thumbName(
$params, File::THUMB_FULL_NAME );
282 'The specified thumbnail parameters are not valid: ' . $e->getMessage()
286 $this->
thumbError( 500, $e->getHTML(),
'Exception caught while extracting thumb name',
287 [
'exception' => $e ] );
295 if ( $rel404 !==
null ) {
296 if ( rawurldecode( $rel404 ) === $img->getThumbRel( $thumbName ) ) {
298 } elseif ( rawurldecode( $rel404 ) === $img->getThumbRel( $thumbName2 ) ) {
301 $this->
header(
'Location: ' .
303 $this->
header(
'Expires: ' .
304 gmdate(
'D, d M Y H:i:s', time() + 7 * 86400 ) .
' GMT' );
306 $varyHeader[] =
'X-Forwarded-Proto';
308 if ( count( $varyHeader ) ) {
309 $this->
header(
'Vary: ' . implode(
', ', $varyHeader ) );
313 $this->
thumbErrorText( 404,
"The given path of the specified thumbnail is incorrect;
314 expected '" . $img->getThumbRel( $thumbName ) .
"' but got '" .
315 rawurldecode( $rel404 ) .
"'." );
320 $dispositionType = isset(
$params[
'download'] ) ?
'attachment' :
'inline';
324 'Content-Disposition: ' . $img->getThumbDisposition( $thumbName, $dispositionType );
326 if ( count( $varyHeader ) ) {
327 $headers[] =
'Vary: ' . implode(
', ', $varyHeader );
331 $thumbPath = $img->getThumbPath( $thumbName );
332 if ( $img->getRepo()->fileExists( $thumbPath ) ) {
333 $starttime = microtime(
true );
334 $status = $img->getRepo()->streamFileWithStatus( $thumbPath, $headers );
335 $streamtime = microtime(
true ) - $starttime;
337 if ( $status->isOK() ) {
338 $services->getStatsdDataFactory()->timing(
339 'media.thumbnail.stream',
345 'Could not stream the file',
346 $status->getWikiText(
false,
false,
'en' ),
348 'file' => $thumbName,
349 'path' => $thumbPath,
350 'error' => $status->getWikiText(
false,
false,
'en' ),
357 $authority = $this->
getContext()->getAuthority();
358 $status = PermissionStatus::newEmpty();
360 && !$authority->authorizeAction(
'renderfile-nonstandard', $status )
362 $statusFormatter = $services->getFormatterFactory()
365 $this->
thumbError( 429, $statusFormatter->getHTML( $status ) );
367 } elseif ( !$authority->authorizeAction(
'renderfile', $status ) ) {
368 $statusFormatter = $services->getFormatterFactory()
371 $this->
thumbError( 429, $statusFormatter->getHTML( $status ) );
375 $thumbProxyUrl = $img->getRepo()->getThumbProxyUrl();
377 if ( strlen( $thumbProxyUrl ??
'' ) ) {
378 $this->proxyThumbnailRequest( $img, $thumbName );
389 $msg = $this->
getContext()->msg(
'thumbnail_error' );
393 $errorMsg = $errorMsg ?: $msg->rawParams(
'File::transform() returned false' )->escaped();
395 $errorMsg->
getKey() ===
'thumbnail_image-failure-limit'
399 } elseif ( $thumb->isError() ) {
400 $errorMsg = $thumb->getHtmlMsg();
401 $errorCode = $thumb->getHttpStatusCode();
402 } elseif ( !$thumb->hasFile() ) {
403 $errorMsg = $msg->rawParams(
'No path supplied in thumbnail object' )->escaped();
404 } elseif ( $thumb->fileIsSource() ) {
406 ->rawParams(
'Image was not scaled, is the requested width bigger than the source?' )
413 if ( $errorMsg !==
false ) {
414 $this->
thumbError( $errorCode, $errorMsg,
null, [
'file' => $thumbName,
'path' => $thumbPath ] );
417 '@phan-var MediaTransformOutput $thumb';
418 $status = $thumb->streamFileWithStatus( $headers );
419 if ( !$status->isOK() ) {
420 $this->
thumbError( 500,
'Could not stream the file', $status->getWikiText(
false,
false,
'en' ), [
421 'file' => $thumbName,
'path' => $thumbPath,
422 'error' => $status->getWikiText(
false,
false,
'en' ) ] );
478 $cache = ObjectCache::getLocalClusterInstance();
479 $key = $cache->makeKey(
481 $attemptFailureEpoch,
488 if ( $cache->get( $key ) >= 4 ) {
489 return [
false, $this->
getContext()->msg(
'thumbnail_image-failure-limit', 4 ) ];
494 register_shutdown_function(
static function () use ( $cache, &$done, $key ) {
497 $cache->incrWithInit( $key, $cache::TTL_HOUR + mt_rand( 0, 300 ) );
508 $poolCounterType =
'FileRenderExpensive';
510 $poolCounterType =
'FileRender';
517 'doWork' =>
static function () use ( $file,
$params ) {
520 'doCachedWork' =>
static function () use ( $file,
$params, $thumbPath ) {
523 return $file->
getRepo()->fileExists( $thumbPath )
527 'error' =>
function (
Status $status ) {
528 return $this->
getContext()->msg(
'generic-pool-error' )->parse() .
'<hr>' . $status->getHTML();
535 } elseif ( is_string( $result ) ) {
536 $errorHtml = $result;
538 }
catch ( Exception $e ) {
545 if ( !$thumb || $thumb->isError() ) {
547 $cache->incrWithInit( $key, $cache::TTL_HOUR + mt_rand( 0, 300 ) );
550 return [ $thumb, $errorHtml ];