MediaWiki 1.42.0
ThumbnailEntryPoint.php
Go to the documentation of this file.
1<?php
31namespace MediaWiki\FileRepo;
32
33use Exception;
34use File;
35use InvalidArgumentException;
49use MWException;
50use ObjectCache;
52use Wikimedia\AtEase\AtEase;
53
55
59 public function execute() {
61
64 'stream'
65 );
66
67 // Don't use fancy MIME detection, just check the file extension for jpg/gif/png.
68 // NOTE: This only works as long as to StreamFile::contentTypeFromPath
69 // get this setting from global state. When StreamFile gets refactored,
70 // we need to find a better way.
72
73 $this->handleRequest();
74 }
75
76 protected function doPrepareForOutput() {
77 // No-op.
78 // Do not call parent::doPrepareForOutput() to avoid
79 // commitMainTransaction() getting called.
80 }
81
82 protected function handleRequest() {
83 $this->streamThumb( $this->getRequest()->getQueryValuesOnly() );
84 }
85
99 protected function streamThumb( array $params ) {
100 $varyOnXFP = $this->getConfig( MainConfigNames::VaryOnXFP );
101
102 $headers = []; // HTTP headers to send
103
104 $fileName = $params['f'] ?? '';
105
106 // Backwards compatibility parameters
107 if ( isset( $params['w'] ) ) {
108 $params['width'] = $params['w'];
109 unset( $params['w'] );
110 }
111 if ( isset( $params['width'] ) && substr( $params['width'], -2 ) == 'px' ) {
112 // strip the px (pixel) suffix, if found
113 $params['width'] = substr( $params['width'], 0, -2 );
114 }
115 if ( isset( $params['p'] ) ) {
116 $params['page'] = $params['p'];
117 }
118
119 // Is this a thumb of an archived file?
120 $isOld = ( isset( $params['archived'] ) && $params['archived'] );
121 unset( $params['archived'] ); // handlers don't care
122
123 // Is this a thumb of a temp file?
124 $isTemp = ( isset( $params['temp'] ) && $params['temp'] );
125 unset( $params['temp'] ); // handlers don't care
126
127 $services = MediaWikiServices::getInstance();
128
129 // Some basic input validation
130 $fileName = strtr( $fileName, '\\/', '__' );
131 $localRepo = $services->getRepoGroup()->getLocalRepo();
132 $archiveTimestamp = null;
133
134 // Actually fetch the image. Method depends on whether it is archived or not.
135 if ( $isTemp ) {
136 $repo = $localRepo->getTempRepo();
137 $img = new UnregisteredLocalFile( false, $repo,
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
142 );
143 } elseif ( $isOld ) {
144 // Format is <timestamp>!<name>
145 $bits = explode( '!', $fileName, 2 );
146 if ( count( $bits ) != 2 ) {
147 $this->thumbError( 404, $this->getContext()->msg( 'badtitletext' )->parse() );
148 return;
149 }
150 $archiveTimestamp = $bits[0];
151 $title = Title::makeTitleSafe( NS_FILE, $bits[1] );
152 if ( !$title ) {
153 $this->thumbError( 404, $this->getContext()->msg( 'badtitletext' )->parse() );
154 return;
155 }
156 $img = $localRepo->newFromArchiveName( $title, $fileName );
157 } else {
158 $img = $localRepo->newFile( $fileName );
159 }
160
161 // Check the source file title
162 if ( !$img ) {
163 $this->thumbError( 404, $this->getContext()->msg( 'badtitletext' )->parse() );
164 return;
165 }
166
167 // Check permissions if there are read restrictions
168 $varyHeader = [];
169 if ( !$services->getGroupPermissionsLookup()->groupHasPermission( '*', 'read' ) ) {
170 $authority = $this->getContext()->getAuthority();
171 $imgTitle = $img->getTitle();
172
173 if ( !$imgTitle || !$authority->authorizeRead( 'read', $imgTitle ) ) {
174 $this->thumbErrorText( 403, 'Access denied. You do not have permission to access ' .
175 'the source file.' );
176 return;
177 }
178 $headers[] = 'Cache-Control: private';
179 $varyHeader[] = 'Cookie';
180 }
181
182 // Check if the file is hidden
183 if ( $img->isDeleted( File::DELETED_FILE ) ) {
184 $this->thumbErrorText( 404, "The source file '$fileName' does not exist." );
185 return;
186 }
187
188 // Do rendering parameters extraction from thumbnail name.
189 if ( isset( $params['thumbName'] ) ) {
190 $params = $this->extractThumbParams( $img, $params );
191 }
192 if ( $params == null ) {
193 $this->thumbErrorText( 400, 'The specified thumbnail parameters are not recognized.' );
194 return;
195 }
196
197 // Check the source file storage path
198 if ( !$img->exists() ) {
199 $redirectedLocation = false;
200 if ( !$isTemp ) {
201 // Check for file redirect
202 // Since redirects are associated with pages, not versions of files,
203 // we look for the most current version to see if its a redirect.
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 );
210 if ( $isOld ) {
211 $newThumbUrl = $targetFile->getArchiveThumbUrl(
212 $archiveTimestamp . '!' . $targetFile->getName(), $newThumbName );
213 } else {
214 $newThumbUrl = $targetFile->getThumbUrl( $newThumbName );
215 }
216 $redirectedLocation = wfExpandUrl( $newThumbUrl, PROTO_CURRENT );
217 }
218 }
219 }
220
221 if ( $redirectedLocation ) {
222 // File has been moved. Give redirect.
223 $response = $this->getResponse();
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' );
228 if ( $varyOnXFP ) {
229 $varyHeader[] = 'X-Forwarded-Proto';
230 }
231 if ( count( $varyHeader ) ) {
232 $response->header( 'Vary: ' . implode( ', ', $varyHeader ) );
233 }
234 $response->header( 'Content-Length: 0' );
235 return;
236 }
237
238 // If it's not a redirect that has a target as a local file, give 404.
239 $this->thumbErrorText( 404, "The source file '$fileName' does not exist." );
240 return;
241 } elseif ( $img->getPath() === false ) {
242 $this->thumbErrorText( 400, "The source file '$fileName' is not locally accessible." );
243 return;
244 }
245
246 // Check IMS against the source file
247 // This means that clients can keep a cached copy even after it has been deleted on the server
248 if ( $this->getServerInfo( 'HTTP_IF_MODIFIED_SINCE', '' ) !== '' ) {
249 // Fix IE brokenness
250 $imsString = preg_replace(
251 '/;.*$/',
252 '',
253 $this->getServerInfo( 'HTTP_IF_MODIFIED_SINCE' ) ?? ''
254 );
255 // Calculate time
256 AtEase::suppressWarnings();
257 $imsUnix = strtotime( $imsString );
258 AtEase::restoreWarnings();
259 if ( wfTimestamp( TS_UNIX, $img->getTimestamp() ) <= $imsUnix ) {
260 $this->status( 304 );
261 return;
262 }
263 }
264
265 $rel404 = $params['rel404'] ?? null;
266 unset( $params['r'] ); // ignore 'r' because we unconditionally pass File::RENDER
267 unset( $params['f'] ); // We're done with 'f' parameter.
268 unset( $params['rel404'] ); // moved to $rel404
269
270 // Get the normalized thumbnail name from the parameters...
271 try {
272 $thumbName = $img->thumbName( $params );
273 if ( !strlen( $thumbName ?? '' ) ) { // invalid params?
275 'Empty return from File::thumbName'
276 );
277 }
278 $thumbName2 = $img->thumbName( $params, File::THUMB_FULL_NAME ); // b/c; "long" style
280 $this->thumbErrorText(
281 400,
282 'The specified thumbnail parameters are not valid: ' . $e->getMessage()
283 );
284 return;
285 } catch ( MWException $e ) {
286 $this->thumbError( 500, $e->getHTML(), 'Exception caught while extracting thumb name',
287 [ 'exception' => $e ] );
288 return;
289 }
290
291 // For 404 handled thumbnails, we only use the base name of the URI
292 // for the thumb params and the parent directory for the source file name.
293 // Check that the zone relative path matches up so CDN caches won't pick
294 // up thumbs that would not be purged on source file deletion (T36231).
295 if ( $rel404 !== null ) { // thumbnail was handled via 404
296 if ( rawurldecode( $rel404 ) === $img->getThumbRel( $thumbName ) ) {
297 // Request for the canonical thumbnail name
298 } elseif ( rawurldecode( $rel404 ) === $img->getThumbRel( $thumbName2 ) ) {
299 // Request for the "long" thumbnail name; redirect to canonical name
300 $this->status( 301 );
301 $this->header( 'Location: ' .
302 wfExpandUrl( $img->getThumbUrl( $thumbName ), PROTO_CURRENT ) );
303 $this->header( 'Expires: ' .
304 gmdate( 'D, d M Y H:i:s', time() + 7 * 86400 ) . ' GMT' );
305 if ( $varyOnXFP ) {
306 $varyHeader[] = 'X-Forwarded-Proto';
307 }
308 if ( count( $varyHeader ) ) {
309 $this->header( 'Vary: ' . implode( ', ', $varyHeader ) );
310 }
311 return;
312 } else {
313 $this->thumbErrorText( 404, "The given path of the specified thumbnail is incorrect;
314 expected '" . $img->getThumbRel( $thumbName ) . "' but got '" .
315 rawurldecode( $rel404 ) . "'." );
316 return;
317 }
318 }
319
320 $dispositionType = isset( $params['download'] ) ? 'attachment' : 'inline';
321
322 // Suggest a good name for users downloading this thumbnail
323 $headers[] =
324 'Content-Disposition: ' . $img->getThumbDisposition( $thumbName, $dispositionType );
325
326 if ( count( $varyHeader ) ) {
327 $headers[] = 'Vary: ' . implode( ', ', $varyHeader );
328 }
329
330 // Stream the file if it exists already...
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;
336
337 if ( $status->isOK() ) {
338 $services->getStatsdDataFactory()->timing(
339 'media.thumbnail.stream',
340 $streamtime
341 );
342 } else {
343 $this->thumbError(
344 500,
345 'Could not stream the file',
346 $status->getWikiText( false, false, 'en' ),
347 [
348 'file' => $thumbName,
349 'path' => $thumbPath,
350 'error' => $status->getWikiText( false, false, 'en' ),
351 ]
352 );
353 }
354 return;
355 }
356
357 $authority = $this->getContext()->getAuthority();
358 $status = PermissionStatus::newEmpty();
359 if ( !wfThumbIsStandard( $img, $params )
360 && !$authority->authorizeAction( 'renderfile-nonstandard', $status )
361 ) {
362 $statusFormatter = $services->getFormatterFactory()
363 ->getStatusFormatter( $this->getContext() );
364
365 $this->thumbError( 429, $statusFormatter->getHTML( $status ) );
366 return;
367 } elseif ( !$authority->authorizeAction( 'renderfile', $status ) ) {
368 $statusFormatter = $services->getFormatterFactory()
369 ->getStatusFormatter( $this->getContext() );
370
371 $this->thumbError( 429, $statusFormatter->getHTML( $status ) );
372 return;
373 }
374
375 $thumbProxyUrl = $img->getRepo()->getThumbProxyUrl();
376
377 if ( strlen( $thumbProxyUrl ?? '' ) ) {
378 $this->proxyThumbnailRequest( $img, $thumbName );
379 // No local fallback when in proxy mode
380 return;
381 } else {
382 // Generate the thumbnail locally
383 [ $thumb, $errorMsg ] = $this->generateThumbnail( $img, $params, $thumbName, $thumbPath );
384 }
385
388 // Check for thumbnail generation errors...
389 $msg = $this->getContext()->msg( 'thumbnail_error' );
390 $errorCode = 500;
391
392 if ( !$thumb ) {
393 $errorMsg = $errorMsg ?: $msg->rawParams( 'File::transform() returned false' )->escaped();
394 if ( $errorMsg instanceof MessageSpecifier &&
395 $errorMsg->getKey() === 'thumbnail_image-failure-limit'
396 ) {
397 $errorCode = 429;
398 }
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() ) {
405 $errorMsg = $msg
406 ->rawParams( 'Image was not scaled, is the requested width bigger than the source?' )
407 ->escaped();
408 $errorCode = 400;
409 }
410
411 $this->prepareForOutput();
412
413 if ( $errorMsg !== false ) {
414 $this->thumbError( $errorCode, $errorMsg, null, [ 'file' => $thumbName, 'path' => $thumbPath ] );
415 } else {
416 // Stream the file if there were no errors
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' ) ] );
423 }
424 }
425 }
426
433 private function proxyThumbnailRequest( $img, $thumbName ) {
434 $thumbProxyUrl = $img->getRepo()->getThumbProxyUrl();
435
436 // Instead of generating the thumbnail ourselves, we proxy the request to another service
437 $thumbProxiedUrl = $thumbProxyUrl . $img->getThumbRel( $thumbName );
438
439 $req = MediaWikiServices::getInstance()->getHttpRequestFactory()->create( $thumbProxiedUrl );
440 $secret = $img->getRepo()->getThumbProxySecret();
441
442 // Pass a secret key shared with the proxied service if any
443 if ( strlen( $secret ?? '' ) ) {
444 $req->setHeader( 'X-Swift-Secret', $secret );
445 }
446
447 // Send request to proxied service
448 $req->execute();
449
451
452 // Simply serve the response from the proxied service as-is
453 $this->header( 'HTTP/1.1 ' . $req->getStatus() );
454
455 $headers = $req->getResponseHeaders();
456
457 foreach ( $headers as $key => $values ) {
458 foreach ( $values as $value ) {
459 $this->header( $key . ': ' . $value, false );
460 }
461 }
462
463 $this->print( $req->getContent() );
464 }
465
475 protected function generateThumbnail( File $file, array $params, $thumbName, $thumbPath ) {
476 $attemptFailureEpoch = $this->getConfig( MainConfigNames::AttemptFailureEpoch );
477
478 $cache = ObjectCache::getLocalClusterInstance();
479 $key = $cache->makeKey(
480 'attempt-failures',
481 $attemptFailureEpoch,
482 $file->getRepo()->getName(),
483 $file->getSha1(),
484 md5( $thumbName )
485 );
486
487 // Check if this file keeps failing to render
488 if ( $cache->get( $key ) >= 4 ) {
489 return [ false, $this->getContext()->msg( 'thumbnail_image-failure-limit', 4 ) ];
490 }
491
492 $done = false;
493 // Record failures on PHP fatals in addition to caching exceptions
494 register_shutdown_function( static function () use ( $cache, &$done, $key ) {
495 if ( !$done ) { // transform() gave a fatal
496 // Randomize TTL to reduce stampedes
497 $cache->incrWithInit( $key, $cache::TTL_HOUR + mt_rand( 0, 300 ) );
498 }
499 } );
500
501 $thumb = false;
502 $errorHtml = false;
503
504 // guard thumbnail rendering with PoolCounter to avoid stampedes
505 // expensive files use a separate PoolCounter config so it is possible
506 // to set up a global limit on them
507 if ( $file->isExpensiveToThumbnail() ) {
508 $poolCounterType = 'FileRenderExpensive';
509 } else {
510 $poolCounterType = 'FileRender';
511 }
512
513 // Thumbnail isn't already there, so create the new thumbnail...
514 try {
515 $work = new PoolCounterWorkViaCallback( $poolCounterType, sha1( $file->getName() ),
516 [
517 'doWork' => static function () use ( $file, $params ) {
518 return $file->transform( $params, File::RENDER_NOW );
519 },
520 'doCachedWork' => static function () use ( $file, $params, $thumbPath ) {
521 // If the worker that finished made this thumbnail then use it.
522 // Otherwise, it probably made a different thumbnail for this file.
523 return $file->getRepo()->fileExists( $thumbPath )
524 ? $file->transform( $params, File::RENDER_NOW )
525 : false; // retry once more in exclusive mode
526 },
527 'error' => function ( Status $status ) {
528 return $this->getContext()->msg( 'generic-pool-error' )->parse() . '<hr>' . $status->getHTML();
529 }
530 ]
531 );
532 $result = $work->execute();
533 if ( $result instanceof MediaTransformOutput ) {
534 $thumb = $result;
535 } elseif ( is_string( $result ) ) { // error
536 $errorHtml = $result;
537 }
538 } catch ( Exception $e ) {
539 // Tried to select a page on a non-paged file?
540 }
541
543 $done = true; // no PHP fatal occurred
544
545 if ( !$thumb || $thumb->isError() ) {
546 // Randomize TTL to reduce stampedes
547 $cache->incrWithInit( $key, $cache::TTL_HOUR + mt_rand( 0, 300 ) );
548 }
549
550 return [ $thumb, $errorHtml ];
551 }
552
561 private function extractThumbParams( $file, $params ) {
562 if ( !isset( $params['thumbName'] ) ) {
563 throw new InvalidArgumentException( "No thumbnail name passed to extractThumbParams" );
564 }
565
566 $thumbname = $params['thumbName'];
567 unset( $params['thumbName'] );
568
569 // FIXME: Files in the temp zone don't set a MIME type, which means
570 // they don't have a handler. Which means we can't parse the param
571 // string. However, not a big issue as what good is a param string
572 // if you have no handler to make use of the param string and
573 // actually generate the thumbnail.
574 $handler = $file->getHandler();
575
576 // Based on UploadStash::parseKey
577 $fileNamePos = strrpos( $thumbname, $params['f'] );
578 if ( $fileNamePos === false ) {
579 // Maybe using a short filename? (see FileRepo::nameForThumb)
580 $fileNamePos = strrpos( $thumbname, 'thumbnail' );
581 }
582
583 if ( $handler && $fileNamePos !== false ) {
584 $paramString = substr( $thumbname, 0, $fileNamePos - 1 );
585 $extraParams = $handler->parseParamString( $paramString );
586 if ( $extraParams !== false ) {
587 return $params + $extraParams;
588 }
589 }
590
591 // As a last ditch fallback, use the traditional common parameters
592 if ( preg_match( '!^(page(\d*)-)*(\d*)px-[^/]*$!', $thumbname, $matches ) ) {
593 [ /* all */, /* pagefull */, $pagenum, $size ] = $matches;
594 $params['width'] = $size;
595 if ( $pagenum ) {
596 $params['page'] = $pagenum;
597 }
598 return $params; // valid thumbnail URL
599 }
600 return null;
601 }
602
610 protected function thumbErrorText( $status, $msgText ) {
611 $this->thumbError( $status, htmlspecialchars( $msgText, ENT_NOQUOTES ) );
612 }
613
624 protected function thumbError( $status, $msgHtml, $msgText = null, $context = [] ) {
625 $showHostnames = $this->getConfig( MainConfigNames::ShowHostnames );
626
628
629 if ( $this->getResponse()->headersSent() ) {
630 LoggerFactory::getInstance( 'thumbnail' )->error(
631 'Error after output had been started. Output may be corrupt or truncated. ' .
632 'Original error: ' . ( $msgText ?: $msgHtml ) . " (Status $status)",
633 $context
634 );
635 return;
636 }
637
638 $this->header( 'Cache-Control: no-cache' );
639 $this->header( 'Content-Type: text/html; charset=utf-8' );
640 if ( $status == 400 || $status == 404 || $status == 429 ) {
641 $this->status( $status );
642 } elseif ( $status == 403 ) {
643 $this->status( 403 );
644 $this->header( 'Vary: Cookie' );
645 } else {
646 LoggerFactory::getInstance( 'thumbnail' )->error( $msgText ?: $msgHtml, $context );
647 $this->status( 500 );
648 }
649 if ( $showHostnames ) {
650 $this->header( 'X-MW-Thumbnail-Renderer: ' . wfHostname() );
651 $url = htmlspecialchars(
652 $this->getServerInfo( 'REQUEST_URI' ) ?? '',
653 ENT_NOQUOTES
654 );
655 $hostname = htmlspecialchars( wfHostname(), ENT_NOQUOTES );
656 $debug = "<!-- $url -->\n<!-- $hostname -->\n";
657 } else {
658 $debug = '';
659 }
660 $content = <<<EOT
661<!DOCTYPE html>
662<html><head>
663<meta charset="UTF-8" />
664<title>Error generating thumbnail</title>
665</head>
666<body>
667<h1>Error generating thumbnail</h1>
668<p>
669$msgHtml
670</p>
671$debug
672</body>
673</html>
674
675EOT;
676 $this->header( 'Content-Length: ' . strlen( $content ) );
677 $this->print( $content );
678 }
679
680}
const NS_FILE
Definition Defines.php:70
const PROTO_CURRENT
Definition Defines.php:207
wfThumbIsStandard(File $file, array $params)
Returns true if these thumbnail parameters match one that MediaWiki requests from file description pa...
wfHostname()
Get host name of the current machine, for use in error reporting.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL using $wgServer (or one of its alternatives).
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
array $params
The job parameters.
const MW_ENTRY_POINT
Definition api.php:35
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:73
isExpensiveToThumbnail()
True if creating thumbnails from the file is large or otherwise resource-intensive.
Definition File.php:2485
getName()
Return the name of this file.
Definition File.php:341
getRepo()
Returns the repository.
Definition File.php:2050
transform( $params, $flags=0)
Transform a media file.
Definition File.php:1177
getSha1()
Get the SHA-1 base 36 hash of the file.
Definition File.php:2331
MediaWiki exception.
MediaWiki exception thrown by some methods when the transform parameter array is invalid.
Base class for the output of MediaHandler::doTransform() and File::transform().
thumbErrorText( $status, $msgText)
Output a thumbnail generation error message.
generateThumbnail(File $file, array $params, $thumbName, $thumbPath)
Actually try to generate a new thumbnail.
thumbError( $status, $msgHtml, $msgText=null, $context=[])
Output a thumbnail generation error message.
doPrepareForOutput()
Prepare for sending the output.
streamThumb(array $params)
Stream a thumbnail specified by parameters.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const VaryOnXFP
Name constant for the VaryOnXFP setting, for use with Config::get()
const AttemptFailureEpoch
Name constant for the AttemptFailureEpoch setting, for use with Config::get()
const ShowHostnames
Name constant for the ShowHostnames setting, for use with Config::get()
Base class for entry point handlers.
prepareForOutput()
Prepare for sending the output.
getServerInfo(string $key, $default=null)
header(string $header, bool $replace=true, int $status=0)
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
A StatusValue for permission errors.
Convenience class for dealing with PoolCounter using callbacks.
execute( $skipcache=false)
Get the result of the work (whatever it is), or the result of the error() function.
Class for tracking request-level classification information for profiling/stats/logging.
static warnIfHeadersSent()
Log a warning message if headers have already been sent.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents a title within MediaWiki.
Definition Title.php:78
File without associated database record.
$wgTrivialMimeDetection
Config variable stub for the TrivialMimeDetection setting, for use by phpdoc and IDEs.
getKey()
Returns the message key.