Merge "Improve docs for Title::getInternalURL/getCanonicalURL"
[lhc/web/wiklou.git] / includes / filerepo / file / File.php
1 <?php
2 /**
3 * @defgroup FileAbstraction File abstraction
4 * @ingroup FileRepo
5 *
6 * Represents files in a repository.
7 */
8 use MediaWiki\MediaWikiServices;
9
10 /**
11 * Base code for files.
12 *
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License as published by
15 * the Free Software Foundation; either version 2 of the License, or
16 * (at your option) any later version.
17 *
18 * This program is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * You should have received a copy of the GNU General Public License along
24 * with this program; if not, write to the Free Software Foundation, Inc.,
25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
26 * http://www.gnu.org/copyleft/gpl.html
27 *
28 * @file
29 * @ingroup FileAbstraction
30 */
31
32 // @phan-file-suppress PhanTypeMissingReturn false positives
33 /**
34 * Implements some public methods and some protected utility functions which
35 * are required by multiple child classes. Contains stub functionality for
36 * unimplemented public methods.
37 *
38 * Stub functions which should be overridden are marked with STUB. Some more
39 * concrete functions are also typically overridden by child classes.
40 *
41 * Note that only the repo object knows what its file class is called. You should
42 * never name a file class explictly outside of the repo class. Instead use the
43 * repo's factory functions to generate file objects, for example:
44 *
45 * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
46 *
47 * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
48 * in most cases.
49 *
50 * @ingroup FileAbstraction
51 */
52 abstract class File implements IDBAccessObject {
53 // Bitfield values akin to the Revision deletion constants
54 const DELETED_FILE = 1;
55 const DELETED_COMMENT = 2;
56 const DELETED_USER = 4;
57 const DELETED_RESTRICTED = 8;
58
59 /** Force rendering in the current process */
60 const RENDER_NOW = 1;
61 /**
62 * Force rendering even if thumbnail already exist and using RENDER_NOW
63 * I.e. you have to pass both flags: File::RENDER_NOW | File::RENDER_FORCE
64 */
65 const RENDER_FORCE = 2;
66
67 const DELETE_SOURCE = 1;
68
69 // Audience options for File::getDescription()
70 const FOR_PUBLIC = 1;
71 const FOR_THIS_USER = 2;
72 const RAW = 3;
73
74 // Options for File::thumbName()
75 const THUMB_FULL_NAME = 1;
76
77 /**
78 * Some member variables can be lazy-initialised using __get(). The
79 * initialisation function for these variables is always a function named
80 * like getVar(), where Var is the variable name with upper-case first
81 * letter.
82 *
83 * The following variables are initialised in this way in this base class:
84 * name, extension, handler, path, canRender, isSafeFile,
85 * transformScript, hashPath, pageCount, url
86 *
87 * Code within this class should generally use the accessor function
88 * directly, since __get() isn't re-entrant and therefore causes bugs that
89 * depend on initialisation order.
90 */
91
92 /**
93 * The following member variables are not lazy-initialised
94 */
95
96 /** @var FileRepo|LocalRepo|ForeignAPIRepo|bool */
97 public $repo;
98
99 /** @var Title|string|bool */
100 protected $title;
101
102 /** @var string Text of last error */
103 protected $lastError;
104
105 /** @var string Main part of the title, with underscores (Title::getDBkey) */
106 protected $redirected;
107
108 /** @var Title */
109 protected $redirectedTitle;
110
111 /** @var FSFile|bool False if undefined */
112 protected $fsFile;
113
114 /** @var MediaHandler */
115 protected $handler;
116
117 /** @var string The URL corresponding to one of the four basic zones */
118 protected $url;
119
120 /** @var string File extension */
121 protected $extension;
122
123 /** @var string The name of a file from its title object */
124 protected $name;
125
126 /** @var string The storage path corresponding to one of the zones */
127 protected $path;
128
129 /** @var string Relative path including trailing slash */
130 protected $hashPath;
131
132 /** @var string|false Number of pages of a multipage document, or false for
133 * documents which aren't multipage documents
134 */
135 protected $pageCount;
136
137 /** @var string URL of transformscript (for example thumb.php) */
138 protected $transformScript;
139
140 /** @var Title */
141 protected $redirectTitle;
142
143 /** @var bool Whether the output of transform() for this file is likely to be valid. */
144 protected $canRender;
145
146 /** @var bool Whether this media file is in a format that is unlikely to
147 * contain viruses or malicious content
148 */
149 protected $isSafeFile;
150
151 /** @var string Required Repository class type */
152 protected $repoClass = FileRepo::class;
153
154 /** @var array Cache of tmp filepaths pointing to generated bucket thumbnails, keyed by width */
155 protected $tmpBucketedThumbCache = [];
156
157 /**
158 * Call this constructor from child classes.
159 *
160 * Both $title and $repo are optional, though some functions
161 * may return false or throw exceptions if they are not set.
162 * Most subclasses will want to call assertRepoDefined() here.
163 *
164 * @param Title|string|bool $title
165 * @param FileRepo|bool $repo
166 */
167 function __construct( $title, $repo ) {
168 // Some subclasses do not use $title, but set name/title some other way
169 if ( $title !== false ) {
170 $title = self::normalizeTitle( $title, 'exception' );
171 }
172 $this->title = $title;
173 $this->repo = $repo;
174 }
175
176 /**
177 * Given a string or Title object return either a
178 * valid Title object with namespace NS_FILE or null
179 *
180 * @param Title|string $title
181 * @param string|bool $exception Use 'exception' to throw an error on bad titles
182 * @throws MWException
183 * @return Title|null
184 */
185 static function normalizeTitle( $title, $exception = false ) {
186 $ret = $title;
187 if ( $ret instanceof Title ) {
188 # Normalize NS_MEDIA -> NS_FILE
189 if ( $ret->getNamespace() == NS_MEDIA ) {
190 $ret = Title::makeTitleSafe( NS_FILE, $ret->getDBkey() );
191 # Sanity check the title namespace
192 } elseif ( $ret->getNamespace() !== NS_FILE ) {
193 $ret = null;
194 }
195 } else {
196 # Convert strings to Title objects
197 $ret = Title::makeTitleSafe( NS_FILE, (string)$ret );
198 }
199 if ( !$ret && $exception !== false ) {
200 throw new MWException( "`$title` is not a valid file title." );
201 }
202
203 return $ret;
204 }
205
206 function __get( $name ) {
207 $function = [ $this, 'get' . ucfirst( $name ) ];
208 if ( !is_callable( $function ) ) {
209 return null;
210 } else {
211 $this->$name = $function();
212
213 return $this->$name;
214 }
215 }
216
217 /**
218 * Normalize a file extension to the common form, making it lowercase and checking some synonyms,
219 * and ensure it's clean. Extensions with non-alphanumeric characters will be discarded.
220 * Keep in sync with mw.Title.normalizeExtension() in JS.
221 *
222 * @param string $extension File extension (without the leading dot)
223 * @return string File extension in canonical form
224 */
225 static function normalizeExtension( $extension ) {
226 $lower = strtolower( $extension );
227 $squish = [
228 'htm' => 'html',
229 'jpeg' => 'jpg',
230 'mpeg' => 'mpg',
231 'tiff' => 'tif',
232 'ogv' => 'ogg' ];
233 if ( isset( $squish[$lower] ) ) {
234 return $squish[$lower];
235 } elseif ( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
236 return $lower;
237 } else {
238 return '';
239 }
240 }
241
242 /**
243 * Checks if file extensions are compatible
244 *
245 * @param File $old Old file
246 * @param string $new New name
247 *
248 * @return bool|null
249 */
250 static function checkExtensionCompatibility( File $old, $new ) {
251 $oldMime = $old->getMimeType();
252 $n = strrpos( $new, '.' );
253 $newExt = self::normalizeExtension( $n ? substr( $new, $n + 1 ) : '' );
254 $mimeMagic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
255
256 return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
257 }
258
259 /**
260 * Upgrade the database row if there is one
261 * Called by ImagePage
262 * STUB
263 */
264 function upgradeRow() {
265 }
266
267 /**
268 * Split an internet media type into its two components; if not
269 * a two-part name, set the minor type to 'unknown'.
270 *
271 * @param string $mime "text/html" etc
272 * @return string[] ("text", "html") etc
273 */
274 public static function splitMime( $mime ) {
275 if ( strpos( $mime, '/' ) !== false ) {
276 return explode( '/', $mime, 2 );
277 } else {
278 return [ $mime, 'unknown' ];
279 }
280 }
281
282 /**
283 * Callback for usort() to do file sorts by name
284 *
285 * @param File $a
286 * @param File $b
287 * @return int Result of name comparison
288 */
289 public static function compare( File $a, File $b ) {
290 return strcmp( $a->getName(), $b->getName() );
291 }
292
293 /**
294 * Return the name of this file
295 *
296 * @return string
297 */
298 public function getName() {
299 if ( !isset( $this->name ) ) {
300 $this->assertRepoDefined();
301 $this->name = $this->repo->getNameFromTitle( $this->title );
302 }
303
304 return $this->name;
305 }
306
307 /**
308 * Get the file extension, e.g. "svg"
309 *
310 * @return string
311 */
312 function getExtension() {
313 if ( !isset( $this->extension ) ) {
314 $n = strrpos( $this->getName(), '.' );
315 $this->extension = self::normalizeExtension(
316 $n ? substr( $this->getName(), $n + 1 ) : '' );
317 }
318
319 return $this->extension;
320 }
321
322 /**
323 * Return the associated title object
324 *
325 * @return Title
326 */
327 public function getTitle() {
328 return $this->title;
329 }
330
331 /**
332 * Return the title used to find this file
333 *
334 * @return Title
335 */
336 public function getOriginalTitle() {
337 if ( $this->redirected ) {
338 return $this->getRedirectedTitle();
339 }
340
341 return $this->title;
342 }
343
344 /**
345 * Return the URL of the file
346 *
347 * @return string
348 */
349 public function getUrl() {
350 if ( !isset( $this->url ) ) {
351 $this->assertRepoDefined();
352 $ext = $this->getExtension();
353 $this->url = $this->repo->getZoneUrl( 'public', $ext ) . '/' . $this->getUrlRel();
354 }
355
356 return $this->url;
357 }
358
359 /**
360 * Get short description URL for a files based on the page ID
361 *
362 * @return string|null
363 * @since 1.27
364 */
365 public function getDescriptionShortUrl() {
366 return null;
367 }
368
369 /**
370 * Return a fully-qualified URL to the file.
371 * Upload URL paths _may or may not_ be fully qualified, so
372 * we check. Local paths are assumed to belong on $wgServer.
373 *
374 * @return string
375 */
376 public function getFullUrl() {
377 return wfExpandUrl( $this->getUrl(), PROTO_RELATIVE );
378 }
379
380 /**
381 * @return string
382 */
383 public function getCanonicalUrl() {
384 return wfExpandUrl( $this->getUrl(), PROTO_CANONICAL );
385 }
386
387 /**
388 * @return string
389 */
390 function getViewURL() {
391 if ( $this->mustRender() ) {
392 if ( $this->canRender() ) {
393 return $this->createThumb( $this->getWidth() );
394 } else {
395 wfDebug( __METHOD__ . ': supposed to render ' . $this->getName() .
396 ' (' . $this->getMimeType() . "), but can't!\n" );
397
398 return $this->getUrl(); # hm... return NULL?
399 }
400 } else {
401 return $this->getUrl();
402 }
403 }
404
405 /**
406 * Return the storage path to the file. Note that this does
407 * not mean that a file actually exists under that location.
408 *
409 * This path depends on whether directory hashing is active or not,
410 * i.e. whether the files are all found in the same directory,
411 * or in hashed paths like /images/3/3c.
412 *
413 * Most callers don't check the return value, but ForeignAPIFile::getPath
414 * returns false.
415 *
416 * @return string|bool ForeignAPIFile::getPath can return false
417 */
418 public function getPath() {
419 if ( !isset( $this->path ) ) {
420 $this->assertRepoDefined();
421 $this->path = $this->repo->getZonePath( 'public' ) . '/' . $this->getRel();
422 }
423
424 return $this->path;
425 }
426
427 /**
428 * Get an FS copy or original of this file and return the path.
429 * Returns false on failure. Callers must not alter the file.
430 * Temporary files are cleared automatically.
431 *
432 * @return string|bool False on failure
433 */
434 public function getLocalRefPath() {
435 $this->assertRepoDefined();
436 if ( !isset( $this->fsFile ) ) {
437 $starttime = microtime( true );
438 $this->fsFile = $this->repo->getLocalReference( $this->getPath() );
439
440 $statTiming = microtime( true ) - $starttime;
441 MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
442 'media.thumbnail.generate.fetchoriginal', 1000 * $statTiming );
443
444 if ( !$this->fsFile ) {
445 $this->fsFile = false; // null => false; cache negative hits
446 }
447 }
448
449 return ( $this->fsFile )
450 ? $this->fsFile->getPath()
451 : false;
452 }
453
454 /**
455 * Return the width of the image. Returns false if the width is unknown
456 * or undefined.
457 *
458 * STUB
459 * Overridden by LocalFile, UnregisteredLocalFile
460 *
461 * @param int $page
462 * @return int|bool
463 */
464 public function getWidth( $page = 1 ) {
465 return false;
466 }
467
468 /**
469 * Return the height of the image. Returns false if the height is unknown
470 * or undefined
471 *
472 * STUB
473 * Overridden by LocalFile, UnregisteredLocalFile
474 *
475 * @param int $page
476 * @return bool|int False on failure
477 */
478 public function getHeight( $page = 1 ) {
479 return false;
480 }
481
482 /**
483 * Return the smallest bucket from $wgThumbnailBuckets which is at least
484 * $wgThumbnailMinimumBucketDistance larger than $desiredWidth. The returned bucket, if any,
485 * will always be bigger than $desiredWidth.
486 *
487 * @param int $desiredWidth
488 * @param int $page
489 * @return bool|int
490 */
491 public function getThumbnailBucket( $desiredWidth, $page = 1 ) {
492 global $wgThumbnailBuckets, $wgThumbnailMinimumBucketDistance;
493
494 $imageWidth = $this->getWidth( $page );
495
496 if ( $imageWidth === false ) {
497 return false;
498 }
499
500 if ( $desiredWidth > $imageWidth ) {
501 return false;
502 }
503
504 if ( !$wgThumbnailBuckets ) {
505 return false;
506 }
507
508 $sortedBuckets = $wgThumbnailBuckets;
509
510 sort( $sortedBuckets );
511
512 foreach ( $sortedBuckets as $bucket ) {
513 if ( $bucket >= $imageWidth ) {
514 return false;
515 }
516
517 if ( $bucket - $wgThumbnailMinimumBucketDistance > $desiredWidth ) {
518 return $bucket;
519 }
520 }
521
522 // Image is bigger than any available bucket
523 return false;
524 }
525
526 /**
527 * Returns ID or name of user who uploaded the file
528 * STUB
529 *
530 * @param string $type 'text' or 'id'
531 * @return string|int
532 */
533 public function getUser( $type = 'text' ) {
534 return null;
535 }
536
537 /**
538 * Get the duration of a media file in seconds
539 *
540 * @return float|int
541 */
542 public function getLength() {
543 $handler = $this->getHandler();
544 if ( $handler ) {
545 return $handler->getLength( $this );
546 } else {
547 return 0;
548 }
549 }
550
551 /**
552 * Return true if the file is vectorized
553 *
554 * @return bool
555 */
556 public function isVectorized() {
557 $handler = $this->getHandler();
558 if ( $handler ) {
559 return $handler->isVectorized( $this );
560 } else {
561 return false;
562 }
563 }
564
565 /**
566 * Gives a (possibly empty) list of languages to render
567 * the file in.
568 *
569 * If the file doesn't have translations, or if the file
570 * format does not support that sort of thing, returns
571 * an empty array.
572 *
573 * @return string[]
574 * @since 1.23
575 */
576 public function getAvailableLanguages() {
577 $handler = $this->getHandler();
578 if ( $handler ) {
579 return $handler->getAvailableLanguages( $this );
580 } else {
581 return [];
582 }
583 }
584
585 /**
586 * Get the language code from the available languages for this file that matches the language
587 * requested by the user
588 *
589 * @param string $userPreferredLanguage
590 * @return string|null
591 */
592 public function getMatchedLanguage( $userPreferredLanguage ) {
593 $handler = $this->getHandler();
594 if ( $handler ) {
595 return $handler->getMatchedLanguage(
596 $userPreferredLanguage,
597 $handler->getAvailableLanguages( $this )
598 );
599 }
600
601 return null;
602 }
603
604 /**
605 * In files that support multiple language, what is the default language
606 * to use if none specified.
607 *
608 * @return string|null Lang code, or null if filetype doesn't support multiple languages.
609 * @since 1.23
610 */
611 public function getDefaultRenderLanguage() {
612 $handler = $this->getHandler();
613 if ( $handler ) {
614 return $handler->getDefaultRenderLanguage( $this );
615 } else {
616 return null;
617 }
618 }
619
620 /**
621 * Will the thumbnail be animated if one would expect it to be.
622 *
623 * Currently used to add a warning to the image description page
624 *
625 * @return bool False if the main image is both animated
626 * and the thumbnail is not. In all other cases must return
627 * true. If image is not renderable whatsoever, should
628 * return true.
629 */
630 public function canAnimateThumbIfAppropriate() {
631 $handler = $this->getHandler();
632 if ( !$handler ) {
633 // We cannot handle image whatsoever, thus
634 // one would not expect it to be animated
635 // so true.
636 return true;
637 }
638
639 return !$this->allowInlineDisplay()
640 // Image is not animated, so one would
641 // not expect thumb to be
642 || !$handler->isAnimatedImage( $this )
643 // Image is animated, but thumbnail isn't.
644 // This is unexpected to the user.
645 || $handler->canAnimateThumbnail( $this );
646 }
647
648 /**
649 * Get handler-specific metadata
650 * Overridden by LocalFile, UnregisteredLocalFile
651 * STUB
652 * @return bool|array
653 */
654 public function getMetadata() {
655 return false;
656 }
657
658 /**
659 * Like getMetadata but returns a handler independent array of common values.
660 * @see MediaHandler::getCommonMetaArray()
661 * @return array|bool Array or false if not supported
662 * @since 1.23
663 */
664 public function getCommonMetaArray() {
665 $handler = $this->getHandler();
666
667 if ( !$handler ) {
668 return false;
669 }
670
671 return $handler->getCommonMetaArray( $this );
672 }
673
674 /**
675 * get versioned metadata
676 *
677 * @param array|string $metadata Array or string of (serialized) metadata
678 * @param int $version Version number.
679 * @return array Array containing metadata, or what was passed to it on fail
680 * (unserializing if not array)
681 */
682 public function convertMetadataVersion( $metadata, $version ) {
683 $handler = $this->getHandler();
684 if ( !is_array( $metadata ) ) {
685 // Just to make the return type consistent
686 $metadata = unserialize( $metadata );
687 }
688 if ( $handler ) {
689 return $handler->convertMetadataVersion( $metadata, $version );
690 } else {
691 return $metadata;
692 }
693 }
694
695 /**
696 * Return the bit depth of the file
697 * Overridden by LocalFile
698 * STUB
699 * @return int
700 */
701 public function getBitDepth() {
702 return 0;
703 }
704
705 /**
706 * Return the size of the image file, in bytes
707 * Overridden by LocalFile, UnregisteredLocalFile
708 * STUB
709 * @return bool
710 */
711 public function getSize() {
712 return false;
713 }
714
715 /**
716 * Returns the MIME type of the file.
717 * Overridden by LocalFile, UnregisteredLocalFile
718 * STUB
719 *
720 * @return string
721 */
722 function getMimeType() {
723 return 'unknown/unknown';
724 }
725
726 /**
727 * Return the type of the media in the file.
728 * Use the value returned by this function with the MEDIATYPE_xxx constants.
729 * Overridden by LocalFile,
730 * STUB
731 * @return string
732 */
733 function getMediaType() {
734 return MEDIATYPE_UNKNOWN;
735 }
736
737 /**
738 * Checks if the output of transform() for this file is likely
739 * to be valid. If this is false, various user elements will
740 * display a placeholder instead.
741 *
742 * Currently, this checks if the file is an image format
743 * that can be converted to a format
744 * supported by all browsers (namely GIF, PNG and JPEG),
745 * or if it is an SVG image and SVG conversion is enabled.
746 *
747 * @return bool
748 */
749 function canRender() {
750 if ( !isset( $this->canRender ) ) {
751 $this->canRender = $this->getHandler() && $this->handler->canRender( $this ) && $this->exists();
752 }
753
754 return $this->canRender;
755 }
756
757 /**
758 * Accessor for __get()
759 * @return bool
760 */
761 protected function getCanRender() {
762 return $this->canRender();
763 }
764
765 /**
766 * Return true if the file is of a type that can't be directly
767 * rendered by typical browsers and needs to be re-rasterized.
768 *
769 * This returns true for everything but the bitmap types
770 * supported by all browsers, i.e. JPEG; GIF and PNG. It will
771 * also return true for any non-image formats.
772 *
773 * @return bool
774 */
775 function mustRender() {
776 return $this->getHandler() && $this->handler->mustRender( $this );
777 }
778
779 /**
780 * Alias for canRender()
781 *
782 * @return bool
783 */
784 function allowInlineDisplay() {
785 return $this->canRender();
786 }
787
788 /**
789 * Determines if this media file is in a format that is unlikely to
790 * contain viruses or malicious content. It uses the global
791 * $wgTrustedMediaFormats list to determine if the file is safe.
792 *
793 * This is used to show a warning on the description page of non-safe files.
794 * It may also be used to disallow direct [[media:...]] links to such files.
795 *
796 * Note that this function will always return true if allowInlineDisplay()
797 * or isTrustedFile() is true for this file.
798 *
799 * @return bool
800 */
801 function isSafeFile() {
802 if ( !isset( $this->isSafeFile ) ) {
803 $this->isSafeFile = $this->getIsSafeFileUncached();
804 }
805
806 return $this->isSafeFile;
807 }
808
809 /**
810 * Accessor for __get()
811 *
812 * @return bool
813 */
814 protected function getIsSafeFile() {
815 return $this->isSafeFile();
816 }
817
818 /**
819 * Uncached accessor
820 *
821 * @return bool
822 */
823 protected function getIsSafeFileUncached() {
824 global $wgTrustedMediaFormats;
825
826 if ( $this->allowInlineDisplay() ) {
827 return true;
828 }
829 if ( $this->isTrustedFile() ) {
830 return true;
831 }
832
833 $type = $this->getMediaType();
834 $mime = $this->getMimeType();
835 # wfDebug( "LocalFile::isSafeFile: type= $type, mime= $mime\n" );
836
837 if ( !$type || $type === MEDIATYPE_UNKNOWN ) {
838 return false; # unknown type, not trusted
839 }
840 if ( in_array( $type, $wgTrustedMediaFormats ) ) {
841 return true;
842 }
843
844 if ( $mime === "unknown/unknown" ) {
845 return false; # unknown type, not trusted
846 }
847 if ( in_array( $mime, $wgTrustedMediaFormats ) ) {
848 return true;
849 }
850
851 return false;
852 }
853
854 /**
855 * Returns true if the file is flagged as trusted. Files flagged that way
856 * can be linked to directly, even if that is not allowed for this type of
857 * file normally.
858 *
859 * This is a dummy function right now and always returns false. It could be
860 * implemented to extract a flag from the database. The trusted flag could be
861 * set on upload, if the user has sufficient privileges, to bypass script-
862 * and html-filters. It may even be coupled with cryptographics signatures
863 * or such.
864 *
865 * @return bool
866 */
867 function isTrustedFile() {
868 # this could be implemented to check a flag in the database,
869 # look for signatures, etc
870 return false;
871 }
872
873 /**
874 * Load any lazy-loaded file object fields from source
875 *
876 * This is only useful when setting $flags
877 *
878 * Overridden by LocalFile to actually query the DB
879 *
880 * @param int $flags Bitfield of File::READ_* constants
881 */
882 public function load( $flags = 0 ) {
883 }
884
885 /**
886 * Returns true if file exists in the repository.
887 *
888 * Overridden by LocalFile to avoid unnecessary stat calls.
889 *
890 * @return bool Whether file exists in the repository.
891 */
892 public function exists() {
893 return $this->getPath() && $this->repo->fileExists( $this->path );
894 }
895
896 /**
897 * Returns true if file exists in the repository and can be included in a page.
898 * It would be unsafe to include private images, making public thumbnails inadvertently
899 *
900 * @return bool Whether file exists in the repository and is includable.
901 */
902 public function isVisible() {
903 return $this->exists();
904 }
905
906 /**
907 * @return string
908 */
909 function getTransformScript() {
910 if ( !isset( $this->transformScript ) ) {
911 $this->transformScript = false;
912 if ( $this->repo ) {
913 $script = $this->repo->getThumbScriptUrl();
914 if ( $script ) {
915 $this->transformScript = wfAppendQuery( $script, [ 'f' => $this->getName() ] );
916 }
917 }
918 }
919
920 return $this->transformScript;
921 }
922
923 /**
924 * Get a ThumbnailImage which is the same size as the source
925 *
926 * @param array $handlerParams
927 *
928 * @return ThumbnailImage|MediaTransformOutput|bool False on failure
929 */
930 function getUnscaledThumb( $handlerParams = [] ) {
931 $hp =& $handlerParams;
932 $page = $hp['page'] ?? false;
933 $width = $this->getWidth( $page );
934 if ( !$width ) {
935 return $this->iconThumb();
936 }
937 $hp['width'] = $width;
938 // be sure to ignore any height specification as well (T64258)
939 unset( $hp['height'] );
940
941 return $this->transform( $hp );
942 }
943
944 /**
945 * Return the file name of a thumbnail with the specified parameters.
946 * Use File::THUMB_FULL_NAME to always get a name like "<params>-<source>".
947 * Otherwise, the format may be "<params>-<source>" or "<params>-thumbnail.<ext>".
948 *
949 * @param array $params Handler-specific parameters
950 * @param int $flags Bitfield that supports THUMB_* constants
951 * @return string|null
952 */
953 public function thumbName( $params, $flags = 0 ) {
954 $name = ( $this->repo && !( $flags & self::THUMB_FULL_NAME ) )
955 ? $this->repo->nameForThumb( $this->getName() )
956 : $this->getName();
957
958 return $this->generateThumbName( $name, $params );
959 }
960
961 /**
962 * Generate a thumbnail file name from a name and specified parameters
963 *
964 * @param string $name
965 * @param array $params Parameters which will be passed to MediaHandler::makeParamString
966 * @return string|null
967 */
968 public function generateThumbName( $name, $params ) {
969 if ( !$this->getHandler() ) {
970 return null;
971 }
972 $extension = $this->getExtension();
973 list( $thumbExt, ) = $this->getHandler()->getThumbType(
974 $extension, $this->getMimeType(), $params );
975 $thumbName = $this->getHandler()->makeParamString( $params );
976
977 if ( $this->repo->supportsSha1URLs() ) {
978 $thumbName .= '-' . $this->getSha1() . '.' . $thumbExt;
979 } else {
980 $thumbName .= '-' . $name;
981
982 if ( $thumbExt != $extension ) {
983 $thumbName .= ".$thumbExt";
984 }
985 }
986
987 return $thumbName;
988 }
989
990 /**
991 * Create a thumbnail of the image having the specified width/height.
992 * The thumbnail will not be created if the width is larger than the
993 * image's width. Let the browser do the scaling in this case.
994 * The thumbnail is stored on disk and is only computed if the thumbnail
995 * file does not exist OR if it is older than the image.
996 * Returns the URL.
997 *
998 * Keeps aspect ratio of original image. If both width and height are
999 * specified, the generated image will be no bigger than width x height,
1000 * and will also have correct aspect ratio.
1001 *
1002 * @param int $width Maximum width of the generated thumbnail
1003 * @param int $height Maximum height of the image (optional)
1004 *
1005 * @return string
1006 */
1007 public function createThumb( $width, $height = -1 ) {
1008 $params = [ 'width' => $width ];
1009 if ( $height != -1 ) {
1010 $params['height'] = $height;
1011 }
1012 $thumb = $this->transform( $params );
1013 if ( !$thumb || $thumb->isError() ) {
1014 return '';
1015 }
1016
1017 return $thumb->getUrl();
1018 }
1019
1020 /**
1021 * Return either a MediaTransformError or placeholder thumbnail (if $wgIgnoreImageErrors)
1022 *
1023 * @param string $thumbPath Thumbnail storage path
1024 * @param string $thumbUrl Thumbnail URL
1025 * @param array $params
1026 * @param int $flags
1027 * @return MediaTransformOutput
1028 */
1029 protected function transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags ) {
1030 global $wgIgnoreImageErrors;
1031
1032 $handler = $this->getHandler();
1033 if ( $handler && $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
1034 return $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
1035 } else {
1036 return new MediaTransformError( 'thumbnail_error',
1037 $params['width'], 0, wfMessage( 'thumbnail-dest-create' ) );
1038 }
1039 }
1040
1041 /**
1042 * Transform a media file
1043 *
1044 * @param array $params An associative array of handler-specific parameters.
1045 * Typical keys are width, height and page.
1046 * @param int $flags A bitfield, may contain self::RENDER_NOW to force rendering
1047 * @return ThumbnailImage|MediaTransformOutput|bool False on failure
1048 */
1049 function transform( $params, $flags = 0 ) {
1050 global $wgThumbnailEpoch;
1051
1052 do {
1053 if ( !$this->canRender() ) {
1054 $thumb = $this->iconThumb();
1055 break; // not a bitmap or renderable image, don't try
1056 }
1057
1058 // Get the descriptionUrl to embed it as comment into the thumbnail. T21791.
1059 $descriptionUrl = $this->getDescriptionUrl();
1060 if ( $descriptionUrl ) {
1061 $params['descriptionUrl'] = wfExpandUrl( $descriptionUrl, PROTO_CANONICAL );
1062 }
1063
1064 $handler = $this->getHandler();
1065 $script = $this->getTransformScript();
1066 if ( $script && !( $flags & self::RENDER_NOW ) ) {
1067 // Use a script to transform on client request, if possible
1068 $thumb = $handler->getScriptedTransform( $this, $script, $params );
1069 if ( $thumb ) {
1070 break;
1071 }
1072 }
1073
1074 $normalisedParams = $params;
1075 $handler->normaliseParams( $this, $normalisedParams );
1076
1077 $thumbName = $this->thumbName( $normalisedParams );
1078 $thumbUrl = $this->getThumbUrl( $thumbName );
1079 $thumbPath = $this->getThumbPath( $thumbName ); // final thumb path
1080
1081 if ( $this->repo ) {
1082 // Defer rendering if a 404 handler is set up...
1083 if ( $this->repo->canTransformVia404() && !( $flags & self::RENDER_NOW ) ) {
1084 // XXX: Pass in the storage path even though we are not rendering anything
1085 // and the path is supposed to be an FS path. This is due to getScalerType()
1086 // getting called on the path and clobbering $thumb->getUrl() if it's false.
1087 $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
1088 break;
1089 }
1090 // Check if an up-to-date thumbnail already exists...
1091 wfDebug( __METHOD__ . ": Doing stat for $thumbPath\n" );
1092 if ( !( $flags & self::RENDER_FORCE ) && $this->repo->fileExists( $thumbPath ) ) {
1093 $timestamp = $this->repo->getFileTimestamp( $thumbPath );
1094 if ( $timestamp !== false && $timestamp >= $wgThumbnailEpoch ) {
1095 // XXX: Pass in the storage path even though we are not rendering anything
1096 // and the path is supposed to be an FS path. This is due to getScalerType()
1097 // getting called on the path and clobbering $thumb->getUrl() if it's false.
1098 $thumb = $handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
1099 $thumb->setStoragePath( $thumbPath );
1100 break;
1101 }
1102 } elseif ( $flags & self::RENDER_FORCE ) {
1103 wfDebug( __METHOD__ . " forcing rendering per flag File::RENDER_FORCE\n" );
1104 }
1105
1106 // If the backend is ready-only, don't keep generating thumbnails
1107 // only to return transformation errors, just return the error now.
1108 if ( $this->repo->getReadOnlyReason() !== false ) {
1109 $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
1110 break;
1111 }
1112 }
1113
1114 $tmpFile = $this->makeTransformTmpFile( $thumbPath );
1115
1116 if ( !$tmpFile ) {
1117 $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $params, $flags );
1118 } else {
1119 $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags );
1120 }
1121 } while ( false );
1122
1123 return is_object( $thumb ) ? $thumb : false;
1124 }
1125
1126 /**
1127 * Generates a thumbnail according to the given parameters and saves it to storage
1128 * @param TempFSFile $tmpFile Temporary file where the rendered thumbnail will be saved
1129 * @param array $transformParams
1130 * @param int $flags
1131 * @return bool|MediaTransformOutput
1132 */
1133 public function generateAndSaveThumb( $tmpFile, $transformParams, $flags ) {
1134 global $wgIgnoreImageErrors;
1135
1136 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1137
1138 $handler = $this->getHandler();
1139
1140 $normalisedParams = $transformParams;
1141 $handler->normaliseParams( $this, $normalisedParams );
1142
1143 $thumbName = $this->thumbName( $normalisedParams );
1144 $thumbUrl = $this->getThumbUrl( $thumbName );
1145 $thumbPath = $this->getThumbPath( $thumbName ); // final thumb path
1146
1147 $tmpThumbPath = $tmpFile->getPath();
1148
1149 if ( $handler->supportsBucketing() ) {
1150 $this->generateBucketsIfNeeded( $normalisedParams, $flags );
1151 }
1152
1153 $starttime = microtime( true );
1154
1155 // Actually render the thumbnail...
1156 $thumb = $handler->doTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams );
1157 $tmpFile->bind( $thumb ); // keep alive with $thumb
1158
1159 $statTiming = microtime( true ) - $starttime;
1160 $stats->timing( 'media.thumbnail.generate.transform', 1000 * $statTiming );
1161
1162 if ( !$thumb ) { // bad params?
1163 $thumb = false;
1164 } elseif ( $thumb->isError() ) { // transform error
1165 /** @var MediaTransformError $thumb */
1166 $this->lastError = $thumb->toText();
1167 // Ignore errors if requested
1168 if ( $wgIgnoreImageErrors && !( $flags & self::RENDER_NOW ) ) {
1169 $thumb = $handler->getTransform( $this, $tmpThumbPath, $thumbUrl, $transformParams );
1170 }
1171 } elseif ( $this->repo && $thumb->hasFile() && !$thumb->fileIsSource() ) {
1172 // Copy the thumbnail from the file system into storage...
1173
1174 $starttime = microtime( true );
1175
1176 $disposition = $this->getThumbDisposition( $thumbName );
1177 $status = $this->repo->quickImport( $tmpThumbPath, $thumbPath, $disposition );
1178 if ( $status->isOK() ) {
1179 $thumb->setStoragePath( $thumbPath );
1180 } else {
1181 $thumb = $this->transformErrorOutput( $thumbPath, $thumbUrl, $transformParams, $flags );
1182 }
1183
1184 $statTiming = microtime( true ) - $starttime;
1185 $stats->timing( 'media.thumbnail.generate.store', 1000 * $statTiming );
1186
1187 // Give extensions a chance to do something with this thumbnail...
1188 Hooks::run( 'FileTransformed', [ $this, $thumb, $tmpThumbPath, $thumbPath ] );
1189 }
1190
1191 return $thumb;
1192 }
1193
1194 /**
1195 * Generates chained bucketed thumbnails if needed
1196 * @param array $params
1197 * @param int $flags
1198 * @return bool Whether at least one bucket was generated
1199 */
1200 protected function generateBucketsIfNeeded( $params, $flags = 0 ) {
1201 if ( !$this->repo
1202 || !isset( $params['physicalWidth'] )
1203 || !isset( $params['physicalHeight'] )
1204 ) {
1205 return false;
1206 }
1207
1208 $bucket = $this->getThumbnailBucket( $params['physicalWidth'] );
1209
1210 if ( !$bucket || $bucket == $params['physicalWidth'] ) {
1211 return false;
1212 }
1213
1214 $bucketPath = $this->getBucketThumbPath( $bucket );
1215
1216 if ( $this->repo->fileExists( $bucketPath ) ) {
1217 return false;
1218 }
1219
1220 $starttime = microtime( true );
1221
1222 $params['physicalWidth'] = $bucket;
1223 $params['width'] = $bucket;
1224
1225 $params = $this->getHandler()->sanitizeParamsForBucketing( $params );
1226
1227 $tmpFile = $this->makeTransformTmpFile( $bucketPath );
1228
1229 if ( !$tmpFile ) {
1230 return false;
1231 }
1232
1233 $thumb = $this->generateAndSaveThumb( $tmpFile, $params, $flags );
1234
1235 $buckettime = microtime( true ) - $starttime;
1236
1237 if ( !$thumb || $thumb->isError() ) {
1238 return false;
1239 }
1240
1241 $this->tmpBucketedThumbCache[$bucket] = $tmpFile->getPath();
1242 // For the caching to work, we need to make the tmp file survive as long as
1243 // this object exists
1244 $tmpFile->bind( $this );
1245
1246 MediaWikiServices::getInstance()->getStatsdDataFactory()->timing(
1247 'media.thumbnail.generate.bucket', 1000 * $buckettime );
1248
1249 return true;
1250 }
1251
1252 /**
1253 * Returns the most appropriate source image for the thumbnail, given a target thumbnail size
1254 * @param array $params
1255 * @return array Source path and width/height of the source
1256 */
1257 public function getThumbnailSource( $params ) {
1258 if ( $this->repo
1259 && $this->getHandler()->supportsBucketing()
1260 && isset( $params['physicalWidth'] )
1261 && $bucket = $this->getThumbnailBucket( $params['physicalWidth'] )
1262 ) {
1263 if ( $this->getWidth() != 0 ) {
1264 $bucketHeight = round( $this->getHeight() * ( $bucket / $this->getWidth() ) );
1265 } else {
1266 $bucketHeight = 0;
1267 }
1268
1269 // Try to avoid reading from storage if the file was generated by this script
1270 if ( isset( $this->tmpBucketedThumbCache[$bucket] ) ) {
1271 $tmpPath = $this->tmpBucketedThumbCache[$bucket];
1272
1273 if ( file_exists( $tmpPath ) ) {
1274 return [
1275 'path' => $tmpPath,
1276 'width' => $bucket,
1277 'height' => $bucketHeight
1278 ];
1279 }
1280 }
1281
1282 $bucketPath = $this->getBucketThumbPath( $bucket );
1283
1284 if ( $this->repo->fileExists( $bucketPath ) ) {
1285 $fsFile = $this->repo->getLocalReference( $bucketPath );
1286
1287 if ( $fsFile ) {
1288 return [
1289 'path' => $fsFile->getPath(),
1290 'width' => $bucket,
1291 'height' => $bucketHeight
1292 ];
1293 }
1294 }
1295 }
1296
1297 // Thumbnailing a very large file could result in network saturation if
1298 // everyone does it at once.
1299 if ( $this->getSize() >= 1e7 ) { // 10MB
1300 $work = new PoolCounterWorkViaCallback( 'GetLocalFileCopy', sha1( $this->getName() ),
1301 [
1302 'doWork' => function () {
1303 return $this->getLocalRefPath();
1304 }
1305 ]
1306 );
1307 $srcPath = $work->execute();
1308 } else {
1309 $srcPath = $this->getLocalRefPath();
1310 }
1311
1312 // Original file
1313 return [
1314 'path' => $srcPath,
1315 'width' => $this->getWidth(),
1316 'height' => $this->getHeight()
1317 ];
1318 }
1319
1320 /**
1321 * Returns the repo path of the thumb for a given bucket
1322 * @param int $bucket
1323 * @return string
1324 */
1325 protected function getBucketThumbPath( $bucket ) {
1326 $thumbName = $this->getBucketThumbName( $bucket );
1327 return $this->getThumbPath( $thumbName );
1328 }
1329
1330 /**
1331 * Returns the name of the thumb for a given bucket
1332 * @param int $bucket
1333 * @return string
1334 */
1335 protected function getBucketThumbName( $bucket ) {
1336 return $this->thumbName( [ 'physicalWidth' => $bucket ] );
1337 }
1338
1339 /**
1340 * Creates a temp FS file with the same extension and the thumbnail
1341 * @param string $thumbPath Thumbnail path
1342 * @return TempFSFile|null
1343 */
1344 protected function makeTransformTmpFile( $thumbPath ) {
1345 $thumbExt = FileBackend::extensionFromPath( $thumbPath );
1346 return TempFSFile::factory( 'transform_', $thumbExt, wfTempDir() );
1347 }
1348
1349 /**
1350 * @param string $thumbName Thumbnail name
1351 * @param string $dispositionType Type of disposition (either "attachment" or "inline")
1352 * @return string Content-Disposition header value
1353 */
1354 function getThumbDisposition( $thumbName, $dispositionType = 'inline' ) {
1355 $fileName = $this->name; // file name to suggest
1356 $thumbExt = FileBackend::extensionFromPath( $thumbName );
1357 if ( $thumbExt != '' && $thumbExt !== $this->getExtension() ) {
1358 $fileName .= ".$thumbExt";
1359 }
1360
1361 return FileBackend::makeContentDisposition( $dispositionType, $fileName );
1362 }
1363
1364 /**
1365 * Hook into transform() to allow migration of thumbnail files
1366 * STUB
1367 * Overridden by LocalFile
1368 * @param string $thumbName
1369 */
1370 function migrateThumbFile( $thumbName ) {
1371 }
1372
1373 /**
1374 * Get a MediaHandler instance for this file
1375 *
1376 * @return MediaHandler|bool Registered MediaHandler for file's MIME type
1377 * or false if none found
1378 */
1379 function getHandler() {
1380 if ( !isset( $this->handler ) ) {
1381 $this->handler = MediaHandler::getHandler( $this->getMimeType() );
1382 }
1383
1384 return $this->handler;
1385 }
1386
1387 /**
1388 * Get a ThumbnailImage representing a file type icon
1389 *
1390 * @return ThumbnailImage
1391 */
1392 function iconThumb() {
1393 global $wgResourceBasePath, $IP;
1394 $assetsPath = "$wgResourceBasePath/resources/assets/file-type-icons/";
1395 $assetsDirectory = "$IP/resources/assets/file-type-icons/";
1396
1397 $try = [ 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' ];
1398 foreach ( $try as $icon ) {
1399 if ( file_exists( $assetsDirectory . $icon ) ) { // always FS
1400 $params = [ 'width' => 120, 'height' => 120 ];
1401
1402 return new ThumbnailImage( $this, $assetsPath . $icon, false, $params );
1403 }
1404 }
1405
1406 return null;
1407 }
1408
1409 /**
1410 * Get last thumbnailing error.
1411 * Largely obsolete.
1412 * @return string
1413 */
1414 function getLastError() {
1415 return $this->lastError;
1416 }
1417
1418 /**
1419 * Get all thumbnail names previously generated for this file
1420 * STUB
1421 * Overridden by LocalFile
1422 * @return string[]
1423 */
1424 function getThumbnails() {
1425 return [];
1426 }
1427
1428 /**
1429 * Purge shared caches such as thumbnails and DB data caching
1430 * STUB
1431 * Overridden by LocalFile
1432 * @param array $options Options, which include:
1433 * 'forThumbRefresh' : The purging is only to refresh thumbnails
1434 */
1435 function purgeCache( $options = [] ) {
1436 }
1437
1438 /**
1439 * Purge the file description page, but don't go after
1440 * pages using the file. Use when modifying file history
1441 * but not the current data.
1442 */
1443 function purgeDescription() {
1444 $title = $this->getTitle();
1445 if ( $title ) {
1446 $title->invalidateCache();
1447 $title->purgeSquid();
1448 }
1449 }
1450
1451 /**
1452 * Purge metadata and all affected pages when the file is created,
1453 * deleted, or majorly updated.
1454 */
1455 function purgeEverything() {
1456 // Delete thumbnails and refresh file metadata cache
1457 $this->purgeCache();
1458 $this->purgeDescription();
1459
1460 // Purge cache of all pages using this file
1461 $title = $this->getTitle();
1462 if ( $title ) {
1463 DeferredUpdates::addUpdate(
1464 new HTMLCacheUpdate( $title, 'imagelinks', 'file-purge' )
1465 );
1466 }
1467 }
1468
1469 /**
1470 * Return a fragment of the history of file.
1471 *
1472 * STUB
1473 * @param int|null $limit Limit of rows to return
1474 * @param string|int|null $start Only revisions older than $start will be returned
1475 * @param string|int|null $end Only revisions newer than $end will be returned
1476 * @param bool $inc Include the endpoints of the time range
1477 *
1478 * @return File[]
1479 */
1480 function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1481 return [];
1482 }
1483
1484 /**
1485 * Return the history of this file, line by line. Starts with current version,
1486 * then old versions. Should return an object similar to an image/oldimage
1487 * database row.
1488 *
1489 * STUB
1490 * Overridden in LocalFile
1491 * @return bool
1492 */
1493 public function nextHistoryLine() {
1494 return false;
1495 }
1496
1497 /**
1498 * Reset the history pointer to the first element of the history.
1499 * Always call this function after using nextHistoryLine() to free db resources
1500 * STUB
1501 * Overridden in LocalFile.
1502 */
1503 public function resetHistory() {
1504 }
1505
1506 /**
1507 * Get the filename hash component of the directory including trailing slash,
1508 * e.g. f/fa/
1509 * If the repository is not hashed, returns an empty string.
1510 *
1511 * @return string
1512 */
1513 function getHashPath() {
1514 if ( !isset( $this->hashPath ) ) {
1515 $this->assertRepoDefined();
1516 $this->hashPath = $this->repo->getHashPath( $this->getName() );
1517 }
1518
1519 return $this->hashPath;
1520 }
1521
1522 /**
1523 * Get the path of the file relative to the public zone root.
1524 * This function is overridden in OldLocalFile to be like getArchiveRel().
1525 *
1526 * @return string
1527 */
1528 function getRel() {
1529 return $this->getHashPath() . $this->getName();
1530 }
1531
1532 /**
1533 * Get the path of an archived file relative to the public zone root
1534 *
1535 * @param bool|string $suffix If not false, the name of an archived thumbnail file
1536 *
1537 * @return string
1538 */
1539 function getArchiveRel( $suffix = false ) {
1540 $path = 'archive/' . $this->getHashPath();
1541 if ( $suffix === false ) {
1542 $path = rtrim( $path, '/' );
1543 } else {
1544 $path .= $suffix;
1545 }
1546
1547 return $path;
1548 }
1549
1550 /**
1551 * Get the path, relative to the thumbnail zone root, of the
1552 * thumbnail directory or a particular file if $suffix is specified
1553 *
1554 * @param bool|string $suffix If not false, the name of a thumbnail file
1555 * @return string
1556 */
1557 function getThumbRel( $suffix = false ) {
1558 $path = $this->getRel();
1559 if ( $suffix !== false ) {
1560 $path .= '/' . $suffix;
1561 }
1562
1563 return $path;
1564 }
1565
1566 /**
1567 * Get urlencoded path of the file relative to the public zone root.
1568 * This function is overridden in OldLocalFile to be like getArchiveUrl().
1569 *
1570 * @return string
1571 */
1572 function getUrlRel() {
1573 return $this->getHashPath() . rawurlencode( $this->getName() );
1574 }
1575
1576 /**
1577 * Get the path, relative to the thumbnail zone root, for an archived file's thumbs directory
1578 * or a specific thumb if the $suffix is given.
1579 *
1580 * @param string $archiveName The timestamped name of an archived image
1581 * @param bool|string $suffix If not false, the name of a thumbnail file
1582 * @return string
1583 */
1584 function getArchiveThumbRel( $archiveName, $suffix = false ) {
1585 $path = $this->getArchiveRel( $archiveName );
1586 if ( $suffix !== false ) {
1587 $path .= '/' . $suffix;
1588 }
1589
1590 return $path;
1591 }
1592
1593 /**
1594 * Get the path of the archived file.
1595 *
1596 * @param bool|string $suffix If not false, the name of an archived file.
1597 * @return string
1598 */
1599 function getArchivePath( $suffix = false ) {
1600 $this->assertRepoDefined();
1601
1602 return $this->repo->getZonePath( 'public' ) . '/' . $this->getArchiveRel( $suffix );
1603 }
1604
1605 /**
1606 * Get the path of an archived file's thumbs, or a particular thumb if $suffix is specified
1607 *
1608 * @param string $archiveName The timestamped name of an archived image
1609 * @param bool|string $suffix If not false, the name of a thumbnail file
1610 * @return string
1611 */
1612 function getArchiveThumbPath( $archiveName, $suffix = false ) {
1613 $this->assertRepoDefined();
1614
1615 return $this->repo->getZonePath( 'thumb' ) . '/' .
1616 $this->getArchiveThumbRel( $archiveName, $suffix );
1617 }
1618
1619 /**
1620 * Get the path of the thumbnail directory, or a particular file if $suffix is specified
1621 *
1622 * @param bool|string $suffix If not false, the name of a thumbnail file
1623 * @return string
1624 */
1625 function getThumbPath( $suffix = false ) {
1626 $this->assertRepoDefined();
1627
1628 return $this->repo->getZonePath( 'thumb' ) . '/' . $this->getThumbRel( $suffix );
1629 }
1630
1631 /**
1632 * Get the path of the transcoded directory, or a particular file if $suffix is specified
1633 *
1634 * @param bool|string $suffix If not false, the name of a media file
1635 * @return string
1636 */
1637 function getTranscodedPath( $suffix = false ) {
1638 $this->assertRepoDefined();
1639
1640 return $this->repo->getZonePath( 'transcoded' ) . '/' . $this->getThumbRel( $suffix );
1641 }
1642
1643 /**
1644 * Get the URL of the archive directory, or a particular file if $suffix is specified
1645 *
1646 * @param bool|string $suffix If not false, the name of an archived file
1647 * @return string
1648 */
1649 function getArchiveUrl( $suffix = false ) {
1650 $this->assertRepoDefined();
1651 $ext = $this->getExtension();
1652 $path = $this->repo->getZoneUrl( 'public', $ext ) . '/archive/' . $this->getHashPath();
1653 if ( $suffix === false ) {
1654 $path = rtrim( $path, '/' );
1655 } else {
1656 $path .= rawurlencode( $suffix );
1657 }
1658
1659 return $path;
1660 }
1661
1662 /**
1663 * Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified
1664 *
1665 * @param string $archiveName The timestamped name of an archived image
1666 * @param bool|string $suffix If not false, the name of a thumbnail file
1667 * @return string
1668 */
1669 function getArchiveThumbUrl( $archiveName, $suffix = false ) {
1670 $this->assertRepoDefined();
1671 $ext = $this->getExtension();
1672 $path = $this->repo->getZoneUrl( 'thumb', $ext ) . '/archive/' .
1673 $this->getHashPath() . rawurlencode( $archiveName );
1674 if ( $suffix !== false ) {
1675 $path .= '/' . rawurlencode( $suffix );
1676 }
1677
1678 return $path;
1679 }
1680
1681 /**
1682 * Get the URL of the zone directory, or a particular file if $suffix is specified
1683 *
1684 * @param string $zone Name of requested zone
1685 * @param bool|string $suffix If not false, the name of a file in zone
1686 * @return string Path
1687 */
1688 function getZoneUrl( $zone, $suffix = false ) {
1689 $this->assertRepoDefined();
1690 $ext = $this->getExtension();
1691 $path = $this->repo->getZoneUrl( $zone, $ext ) . '/' . $this->getUrlRel();
1692 if ( $suffix !== false ) {
1693 $path .= '/' . rawurlencode( $suffix );
1694 }
1695
1696 return $path;
1697 }
1698
1699 /**
1700 * Get the URL of the thumbnail directory, or a particular file if $suffix is specified
1701 *
1702 * @param bool|string $suffix If not false, the name of a thumbnail file
1703 * @return string Path
1704 */
1705 function getThumbUrl( $suffix = false ) {
1706 return $this->getZoneUrl( 'thumb', $suffix );
1707 }
1708
1709 /**
1710 * Get the URL of the transcoded directory, or a particular file if $suffix is specified
1711 *
1712 * @param bool|string $suffix If not false, the name of a media file
1713 * @return string Path
1714 */
1715 function getTranscodedUrl( $suffix = false ) {
1716 return $this->getZoneUrl( 'transcoded', $suffix );
1717 }
1718
1719 /**
1720 * Get the public zone virtual URL for a current version source file
1721 *
1722 * @param bool|string $suffix If not false, the name of a thumbnail file
1723 * @return string
1724 */
1725 function getVirtualUrl( $suffix = false ) {
1726 $this->assertRepoDefined();
1727 $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel();
1728 if ( $suffix !== false ) {
1729 $path .= '/' . rawurlencode( $suffix );
1730 }
1731
1732 return $path;
1733 }
1734
1735 /**
1736 * Get the public zone virtual URL for an archived version source file
1737 *
1738 * @param bool|string $suffix If not false, the name of a thumbnail file
1739 * @return string
1740 */
1741 function getArchiveVirtualUrl( $suffix = false ) {
1742 $this->assertRepoDefined();
1743 $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath();
1744 if ( $suffix === false ) {
1745 $path = rtrim( $path, '/' );
1746 } else {
1747 $path .= rawurlencode( $suffix );
1748 }
1749
1750 return $path;
1751 }
1752
1753 /**
1754 * Get the virtual URL for a thumbnail file or directory
1755 *
1756 * @param bool|string $suffix If not false, the name of a thumbnail file
1757 * @return string
1758 */
1759 function getThumbVirtualUrl( $suffix = false ) {
1760 $this->assertRepoDefined();
1761 $path = $this->repo->getVirtualUrl() . '/thumb/' . $this->getUrlRel();
1762 if ( $suffix !== false ) {
1763 $path .= '/' . rawurlencode( $suffix );
1764 }
1765
1766 return $path;
1767 }
1768
1769 /**
1770 * @return bool
1771 */
1772 function isHashed() {
1773 $this->assertRepoDefined();
1774
1775 return (bool)$this->repo->getHashLevels();
1776 }
1777
1778 /**
1779 * @throws MWException
1780 */
1781 function readOnlyError() {
1782 throw new MWException( static::class . ': write operations are not supported' );
1783 }
1784
1785 /**
1786 * Record a file upload in the upload log and the image table
1787 * STUB
1788 * Overridden by LocalFile
1789 * @param string $oldver
1790 * @param string $desc
1791 * @param string $license
1792 * @param string $copyStatus
1793 * @param string $source
1794 * @param bool $watch
1795 * @param string|bool $timestamp
1796 * @param null|User $user User object or null to use $wgUser
1797 * @return bool
1798 * @throws MWException
1799 */
1800 function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1801 $watch = false, $timestamp = false, User $user = null
1802 ) {
1803 $this->readOnlyError();
1804 }
1805
1806 /**
1807 * Move or copy a file to its public location. If a file exists at the
1808 * destination, move it to an archive. Returns a Status object with
1809 * the archive name in the "value" member on success.
1810 *
1811 * The archive name should be passed through to recordUpload for database
1812 * registration.
1813 *
1814 * Options to $options include:
1815 * - headers : name/value map of HTTP headers to use in response to GET/HEAD requests
1816 *
1817 * @param string|FSFile $src Local filesystem path to the source image
1818 * @param int $flags A bitwise combination of:
1819 * File::DELETE_SOURCE Delete the source file, i.e. move rather than copy
1820 * @param array $options Optional additional parameters
1821 * @return Status On success, the value member contains the
1822 * archive name, or an empty string if it was a new file.
1823 *
1824 * STUB
1825 * Overridden by LocalFile
1826 */
1827 function publish( $src, $flags = 0, array $options = [] ) {
1828 $this->readOnlyError();
1829 }
1830
1831 /**
1832 * @param bool|IContextSource $context Context to use (optional)
1833 * @return bool
1834 */
1835 function formatMetadata( $context = false ) {
1836 if ( !$this->getHandler() ) {
1837 return false;
1838 }
1839
1840 return $this->getHandler()->formatMetadata( $this, $context );
1841 }
1842
1843 /**
1844 * Returns true if the file comes from the local file repository.
1845 *
1846 * @return bool
1847 */
1848 function isLocal() {
1849 return $this->repo && $this->repo->isLocal();
1850 }
1851
1852 /**
1853 * Returns the name of the repository.
1854 *
1855 * @return string
1856 */
1857 function getRepoName() {
1858 return $this->repo ? $this->repo->getName() : 'unknown';
1859 }
1860
1861 /**
1862 * Returns the repository
1863 *
1864 * @return FileRepo|LocalRepo|bool
1865 */
1866 function getRepo() {
1867 return $this->repo;
1868 }
1869
1870 /**
1871 * Returns true if the image is an old version
1872 * STUB
1873 *
1874 * @return bool
1875 */
1876 function isOld() {
1877 return false;
1878 }
1879
1880 /**
1881 * Is this file a "deleted" file in a private archive?
1882 * STUB
1883 *
1884 * @param int $field One of DELETED_* bitfield constants
1885 * @return bool
1886 */
1887 function isDeleted( $field ) {
1888 return false;
1889 }
1890
1891 /**
1892 * Return the deletion bitfield
1893 * STUB
1894 * @return int
1895 */
1896 function getVisibility() {
1897 return 0;
1898 }
1899
1900 /**
1901 * Was this file ever deleted from the wiki?
1902 *
1903 * @return bool
1904 */
1905 function wasDeleted() {
1906 $title = $this->getTitle();
1907
1908 return $title && $title->isDeletedQuick();
1909 }
1910
1911 /**
1912 * Move file to the new title
1913 *
1914 * Move current, old version and all thumbnails
1915 * to the new filename. Old file is deleted.
1916 *
1917 * Cache purging is done; checks for validity
1918 * and logging are caller's responsibility
1919 *
1920 * @param Title $target New file name
1921 * @return Status
1922 */
1923 function move( $target ) {
1924 $this->readOnlyError();
1925 }
1926
1927 /**
1928 * Delete all versions of the file.
1929 *
1930 * Moves the files into an archive directory (or deletes them)
1931 * and removes the database rows.
1932 *
1933 * Cache purging is done; logging is caller's responsibility.
1934 *
1935 * @param string $reason
1936 * @param bool $suppress Hide content from sysops?
1937 * @param User|null $user
1938 * @return Status
1939 * STUB
1940 * Overridden by LocalFile
1941 */
1942 function delete( $reason, $suppress = false, $user = null ) {
1943 $this->readOnlyError();
1944 }
1945
1946 /**
1947 * Restore all or specified deleted revisions to the given file.
1948 * Permissions and logging are left to the caller.
1949 *
1950 * May throw database exceptions on error.
1951 *
1952 * @param array $versions Set of record ids of deleted items to restore,
1953 * or empty to restore all revisions.
1954 * @param bool $unsuppress Remove restrictions on content upon restoration?
1955 * @return int|bool The number of file revisions restored if successful,
1956 * or false on failure
1957 * STUB
1958 * Overridden by LocalFile
1959 */
1960 function restore( $versions = [], $unsuppress = false ) {
1961 $this->readOnlyError();
1962 }
1963
1964 /**
1965 * Returns 'true' if this file is a type which supports multiple pages,
1966 * e.g. DJVU or PDF. Note that this may be true even if the file in
1967 * question only has a single page.
1968 *
1969 * @return bool
1970 */
1971 function isMultipage() {
1972 return $this->getHandler() && $this->handler->isMultiPage( $this );
1973 }
1974
1975 /**
1976 * Returns the number of pages of a multipage document, or false for
1977 * documents which aren't multipage documents
1978 *
1979 * @return string|bool|int
1980 */
1981 function pageCount() {
1982 if ( !isset( $this->pageCount ) ) {
1983 if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
1984 $this->pageCount = $this->handler->pageCount( $this );
1985 } else {
1986 $this->pageCount = false;
1987 }
1988 }
1989
1990 return $this->pageCount;
1991 }
1992
1993 /**
1994 * Calculate the height of a thumbnail using the source and destination width
1995 *
1996 * @param int $srcWidth
1997 * @param int $srcHeight
1998 * @param int $dstWidth
1999 *
2000 * @return int
2001 */
2002 static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) {
2003 // Exact integer multiply followed by division
2004 if ( $srcWidth == 0 ) {
2005 return 0;
2006 } else {
2007 return (int)round( $srcHeight * $dstWidth / $srcWidth );
2008 }
2009 }
2010
2011 /**
2012 * Get an image size array like that returned by getImageSize(), or false if it
2013 * can't be determined. Loads the image size directly from the file ignoring caches.
2014 *
2015 * @note Use getWidth()/getHeight() instead of this method unless you have a
2016 * a good reason. This method skips all caches.
2017 *
2018 * @param string $filePath The path to the file (e.g. From getLocalRefPath() )
2019 * @return array|false The width, followed by height, with optionally more things after
2020 */
2021 function getImageSize( $filePath ) {
2022 if ( !$this->getHandler() ) {
2023 return false;
2024 }
2025
2026 return $this->getHandler()->getImageSize( $this, $filePath );
2027 }
2028
2029 /**
2030 * Get the URL of the image description page. May return false if it is
2031 * unknown or not applicable.
2032 *
2033 * @return string
2034 */
2035 function getDescriptionUrl() {
2036 if ( $this->repo ) {
2037 return $this->repo->getDescriptionUrl( $this->getName() );
2038 } else {
2039 return false;
2040 }
2041 }
2042
2043 /**
2044 * Get the HTML text of the description page, if available
2045 *
2046 * @param Language|null $lang Optional language to fetch description in
2047 * @return string|false
2048 */
2049 function getDescriptionText( Language $lang = null ) {
2050 global $wgLang;
2051
2052 if ( !$this->repo || !$this->repo->fetchDescription ) {
2053 return false;
2054 }
2055
2056 $lang = $lang ?? $wgLang;
2057
2058 $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName(), $lang->getCode() );
2059 if ( $renderUrl ) {
2060 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2061 $key = $this->repo->getLocalCacheKey(
2062 'RemoteFileDescription',
2063 $lang->getCode(),
2064 md5( $this->getName() )
2065 );
2066 $fname = __METHOD__;
2067
2068 return $cache->getWithSetCallback(
2069 $key,
2070 $this->repo->descriptionCacheExpiry ?: $cache::TTL_UNCACHEABLE,
2071 function ( $oldValue, &$ttl, array &$setOpts ) use ( $renderUrl, $fname ) {
2072 wfDebug( "Fetching shared description from $renderUrl\n" );
2073 $res = Http::get( $renderUrl, [], $fname );
2074 if ( !$res ) {
2075 $ttl = WANObjectCache::TTL_UNCACHEABLE;
2076 }
2077
2078 return $res;
2079 }
2080 );
2081 }
2082
2083 return false;
2084 }
2085
2086 /**
2087 * Get description of file revision
2088 * STUB
2089 *
2090 * @param int $audience One of:
2091 * File::FOR_PUBLIC to be displayed to all users
2092 * File::FOR_THIS_USER to be displayed to the given user
2093 * File::RAW get the description regardless of permissions
2094 * @param User|null $user User object to check for, only if FOR_THIS_USER is
2095 * passed to the $audience parameter
2096 * @return null|string
2097 */
2098 function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
2099 return null;
2100 }
2101
2102 /**
2103 * Get the 14-character timestamp of the file upload
2104 *
2105 * @return string|bool TS_MW timestamp or false on failure
2106 */
2107 function getTimestamp() {
2108 $this->assertRepoDefined();
2109
2110 return $this->repo->getFileTimestamp( $this->getPath() );
2111 }
2112
2113 /**
2114 * Returns the timestamp (in TS_MW format) of the last change of the description page.
2115 * Returns false if the file does not have a description page, or retrieving the timestamp
2116 * would be expensive.
2117 * @since 1.25
2118 * @return string|bool
2119 */
2120 public function getDescriptionTouched() {
2121 return false;
2122 }
2123
2124 /**
2125 * Get the SHA-1 base 36 hash of the file
2126 *
2127 * @return string
2128 */
2129 function getSha1() {
2130 $this->assertRepoDefined();
2131
2132 return $this->repo->getFileSha1( $this->getPath() );
2133 }
2134
2135 /**
2136 * Get the deletion archive key, "<sha1>.<ext>"
2137 *
2138 * @return string|false
2139 */
2140 function getStorageKey() {
2141 $hash = $this->getSha1();
2142 if ( !$hash ) {
2143 return false;
2144 }
2145 $ext = $this->getExtension();
2146 $dotExt = $ext === '' ? '' : ".$ext";
2147
2148 return $hash . $dotExt;
2149 }
2150
2151 /**
2152 * Determine if the current user is allowed to view a particular
2153 * field of this file, if it's marked as deleted.
2154 * STUB
2155 * @param int $field
2156 * @param User|null $user User object to check, or null to use $wgUser
2157 * @return bool
2158 */
2159 function userCan( $field, User $user = null ) {
2160 return true;
2161 }
2162
2163 /**
2164 * @return string[] HTTP header name/value map to use for HEAD/GET request responses
2165 * @since 1.30
2166 */
2167 function getContentHeaders() {
2168 $handler = $this->getHandler();
2169 if ( $handler ) {
2170 $metadata = $this->getMetadata();
2171
2172 if ( is_string( $metadata ) ) {
2173 $metadata = Wikimedia\quietCall( 'unserialize', $metadata );
2174 }
2175
2176 if ( !is_array( $metadata ) ) {
2177 $metadata = [];
2178 }
2179
2180 return $handler->getContentHeaders( $metadata );
2181 }
2182
2183 return [];
2184 }
2185
2186 /**
2187 * @return string
2188 */
2189 function getLongDesc() {
2190 $handler = $this->getHandler();
2191 if ( $handler ) {
2192 return $handler->getLongDesc( $this );
2193 } else {
2194 return MediaHandler::getGeneralLongDesc( $this );
2195 }
2196 }
2197
2198 /**
2199 * @return string
2200 */
2201 function getShortDesc() {
2202 $handler = $this->getHandler();
2203 if ( $handler ) {
2204 return $handler->getShortDesc( $this );
2205 } else {
2206 return MediaHandler::getGeneralShortDesc( $this );
2207 }
2208 }
2209
2210 /**
2211 * @return string
2212 */
2213 function getDimensionsString() {
2214 $handler = $this->getHandler();
2215 if ( $handler ) {
2216 return $handler->getDimensionsString( $this );
2217 } else {
2218 return '';
2219 }
2220 }
2221
2222 /**
2223 * @return string
2224 */
2225 function getRedirected() {
2226 return $this->redirected;
2227 }
2228
2229 /**
2230 * @return Title|null
2231 */
2232 function getRedirectedTitle() {
2233 if ( $this->redirected ) {
2234 if ( !$this->redirectTitle ) {
2235 $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected );
2236 }
2237
2238 return $this->redirectTitle;
2239 }
2240
2241 return null;
2242 }
2243
2244 /**
2245 * @param string $from
2246 * @return void
2247 */
2248 function redirectedFrom( $from ) {
2249 $this->redirected = $from;
2250 }
2251
2252 /**
2253 * @return bool
2254 */
2255 function isMissing() {
2256 return false;
2257 }
2258
2259 /**
2260 * Check if this file object is small and can be cached
2261 * @return bool
2262 */
2263 public function isCacheable() {
2264 return true;
2265 }
2266
2267 /**
2268 * Assert that $this->repo is set to a valid FileRepo instance
2269 * @throws MWException
2270 */
2271 protected function assertRepoDefined() {
2272 if ( !( $this->repo instanceof $this->repoClass ) ) {
2273 throw new MWException( "A {$this->repoClass} object is not set for this File.\n" );
2274 }
2275 }
2276
2277 /**
2278 * Assert that $this->title is set to a Title
2279 * @throws MWException
2280 */
2281 protected function assertTitleDefined() {
2282 if ( !( $this->title instanceof Title ) ) {
2283 throw new MWException( "A Title object is not set for this File.\n" );
2284 }
2285 }
2286
2287 /**
2288 * True if creating thumbnails from the file is large or otherwise resource-intensive.
2289 * @return bool
2290 */
2291 public function isExpensiveToThumbnail() {
2292 $handler = $this->getHandler();
2293 return $handler ? $handler->isExpensiveToThumbnail( $this ) : false;
2294 }
2295
2296 /**
2297 * Whether the thumbnails created on the same server as this code is running.
2298 * @since 1.25
2299 * @return bool
2300 */
2301 public function isTransformedLocally() {
2302 return true;
2303 }
2304 }