103 $fileName =
$params[
'f'] ??
'';
110 if ( isset(
$params[
'width'] ) && substr(
$params[
'width'], -2 ) ==
'px' ) {
129 $fileName = strtr( $fileName,
'\\/',
'__' );
130 $localRepo = $services->getRepoGroup()->getLocalRepo();
131 $archiveTimestamp =
null;
135 $repo = $localRepo->getTempRepo();
137 # Temp files are hashed based on the name without the timestamp.
138 # The thumbnails will be hashed based on the entire name however.
139 # @todo fix
this convention to actually be reasonable.
140 $repo->getZonePath(
'public' ) .
'/' . $repo->getTempHashPath( $fileName ) . $fileName
142 } elseif ( $isOld ) {
144 $bits = explode(
'!', $fileName, 2 );
145 if ( count( $bits ) != 2 ) {
149 $archiveTimestamp = $bits[0];
150 $title = Title::makeTitleSafe(
NS_FILE, $bits[1] );
155 $img = $localRepo->newFromArchiveName( $title, $fileName );
157 $img = $localRepo->newFile( $fileName );
168 if ( !$services->getGroupPermissionsLookup()->groupHasPermission(
'*',
'read' ) ) {
169 $authority = $this->
getContext()->getAuthority();
170 $imgTitle = $img->getTitle();
172 if ( !$imgTitle || !$authority->authorizeRead(
'read', $imgTitle ) ) {
173 $this->
thumbErrorText( 403,
'Access denied. You do not have permission to access ' .
174 'the source file.' );
177 $headers[] =
'Cache-Control: private';
178 $varyHeader[] =
'Cookie';
182 if ( $img->isDeleted( File::DELETED_FILE ) ) {
183 $this->
thumbErrorText( 404,
"The source file '$fileName' does not exist." );
188 if ( isset(
$params[
'thumbName'] ) ) {
192 $this->
thumbErrorText( 400,
'The specified thumbnail parameters are not recognized.' );
197 if ( !$img->exists() ) {
198 $redirectedLocation =
false;
203 $possRedirFile = $localRepo->findFile( $img->getName() );
204 if ( $possRedirFile && $possRedirFile->getRedirected() !==
null ) {
205 $redirTarget = $possRedirFile->getName();
206 $targetFile = $localRepo->newFile( Title::makeTitleSafe(
NS_FILE, $redirTarget ) );
207 if ( $targetFile->exists() ) {
208 $newThumbName = $targetFile->thumbName(
$params );
210 $newThumbUrl = $targetFile->getArchiveThumbUrl(
211 $archiveTimestamp .
'!' . $targetFile->getName(), $newThumbName );
213 $newThumbUrl = $targetFile->getThumbUrl( $newThumbName );
220 if ( $redirectedLocation ) {
223 $response->statusHeader( 302 );
224 $response->header(
'Location: ' . $redirectedLocation );
225 $response->header(
'Expires: ' .
226 gmdate(
'D, d M Y H:i:s', time() + 12 * 3600 ) .
' GMT' );
228 $varyHeader[] =
'X-Forwarded-Proto';
230 if ( count( $varyHeader ) ) {
231 $response->header(
'Vary: ' . implode(
', ', $varyHeader ) );
233 $response->header(
'Content-Length: 0' );
238 $this->
thumbErrorText( 404,
"The source file '$fileName' does not exist." );
240 } elseif ( $img->getPath() ===
false ) {
241 $this->
thumbErrorText( 400,
"The source file '$fileName' is not locally accessible." );
247 if ( $this->
getServerInfo(
'HTTP_IF_MODIFIED_SINCE',
'' ) !==
'' ) {
249 $imsString = preg_replace(
255 AtEase::suppressWarnings();
256 $imsUnix = strtotime( $imsString );
257 AtEase::restoreWarnings();
258 if (
wfTimestamp( TS_UNIX, $img->getTimestamp() ) <= $imsUnix ) {
264 $rel404 =
$params[
'rel404'] ??
null;
271 $thumbName = $img->thumbName(
$params );
272 if ( !strlen( $thumbName ??
'' ) ) {
274 'Empty return from File::thumbName'
277 $thumbName2 = $img->thumbName(
$params, File::THUMB_FULL_NAME );
281 'The specified thumbnail parameters are not valid: ' . $e->getMessage()
290 if ( $rel404 !==
null ) {
291 if ( rawurldecode( $rel404 ) === $img->getThumbRel( $thumbName ) ) {
293 } elseif ( rawurldecode( $rel404 ) === $img->getThumbRel( $thumbName2 ) ) {
296 $this->
header(
'Location: ' .
298 $this->
header(
'Expires: ' .
299 gmdate(
'D, d M Y H:i:s', time() + 7 * 86400 ) .
' GMT' );
301 $varyHeader[] =
'X-Forwarded-Proto';
303 if ( count( $varyHeader ) ) {
304 $this->
header(
'Vary: ' . implode(
', ', $varyHeader ) );
308 $this->
thumbErrorText( 404,
"The given path of the specified thumbnail is incorrect;
309 expected '" . $img->getThumbRel( $thumbName ) .
"' but got '" .
310 rawurldecode( $rel404 ) .
"'." );
315 $dispositionType = isset(
$params[
'download'] ) ?
'attachment' :
'inline';
319 'Content-Disposition: ' . $img->getThumbDisposition( $thumbName, $dispositionType );
321 if ( count( $varyHeader ) ) {
322 $headers[] =
'Vary: ' . implode(
', ', $varyHeader );
326 $thumbPath = $img->getThumbPath( $thumbName );
327 if ( $img->getRepo()->fileExists( $thumbPath ) ) {
328 $starttime = microtime(
true );
329 $status = $img->getRepo()->streamFileWithStatus( $thumbPath, $headers );
330 $streamtime = microtime(
true ) - $starttime;
332 if ( $status->isOK() ) {
333 $services->getStatsdDataFactory()->timing(
334 'media.thumbnail.stream',
340 'Could not stream the file',
341 $status->getWikiText(
false,
false,
'en' ),
343 'file' => $thumbName,
344 'path' => $thumbPath,
345 'error' => $status->getWikiText(
false,
false,
'en' ),
352 $authority = $this->
getContext()->getAuthority();
353 $status = PermissionStatus::newEmpty();
355 && !$authority->authorizeAction(
'renderfile-nonstandard', $status )
357 $statusFormatter = $services->getFormatterFactory()
360 $this->
thumbError( 429, $statusFormatter->getHTML( $status ) );
362 } elseif ( !$authority->authorizeAction(
'renderfile', $status ) ) {
363 $statusFormatter = $services->getFormatterFactory()
366 $this->
thumbError( 429, $statusFormatter->getHTML( $status ) );
370 $thumbProxyUrl = $img->getRepo()->getThumbProxyUrl();
372 if ( strlen( $thumbProxyUrl ??
'' ) ) {
373 $this->proxyThumbnailRequest( $img, $thumbName );
384 $msg = $this->
getContext()->msg(
'thumbnail_error' );
388 $errorMsg = $errorMsg ?: $msg->rawParams(
'File::transform() returned false' )->escaped();
390 $errorMsg->
getKey() ===
'thumbnail_image-failure-limit'
394 } elseif ( $thumb->isError() ) {
395 $errorMsg = $thumb->getHtmlMsg();
396 $errorCode = $thumb->getHttpStatusCode();
397 } elseif ( !$thumb->hasFile() ) {
398 $errorMsg = $msg->rawParams(
'No path supplied in thumbnail object' )->escaped();
399 } elseif ( $thumb->fileIsSource() ) {
401 ->rawParams(
'Image was not scaled, is the requested width bigger than the source?' )
408 if ( $errorMsg !==
false ) {
409 $this->
thumbError( $errorCode, $errorMsg,
null, [
'file' => $thumbName,
'path' => $thumbPath ] );
412 '@phan-var MediaTransformOutput $thumb';
413 $status = $thumb->streamFileWithStatus( $headers );
414 if ( !$status->isOK() ) {
415 $this->
thumbError( 500,
'Could not stream the file', $status->getWikiText(
false,
false,
'en' ), [
416 'file' => $thumbName,
'path' => $thumbPath,
417 'error' => $status->getWikiText(
false,
false,
'en' ) ] );
473 $cache = ObjectCache::getLocalClusterInstance();
474 $key = $cache->makeKey(
476 $attemptFailureEpoch,
483 if ( $cache->get( $key ) >= 4 ) {
484 return [
false, $this->
getContext()->msg(
'thumbnail_image-failure-limit', 4 ) ];
489 register_shutdown_function(
static function () use ( $cache, &$done, $key ) {
492 $cache->incrWithInit( $key, $cache::TTL_HOUR + mt_rand( 0, 300 ) );
503 $poolCounterType =
'FileRenderExpensive';
505 $poolCounterType =
'FileRender';
512 'doWork' =>
static function () use ( $file,
$params ) {
515 'doCachedWork' =>
static function () use ( $file,
$params, $thumbPath ) {
518 return $file->
getRepo()->fileExists( $thumbPath )
522 'error' =>
function (
Status $status ) {
523 return $this->
getContext()->msg(
'generic-pool-error' )->parse() .
'<hr>' . $status->getHTML();
530 } elseif ( is_string( $result ) ) {
531 $errorHtml = $result;
533 }
catch ( Exception $e ) {
540 if ( !$thumb || $thumb->isError() ) {
542 $cache->incrWithInit( $key, $cache::TTL_HOUR + mt_rand( 0, 300 ) );
545 return [ $thumb, $errorHtml ];