]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/filerepo/File.php
MediaWiki 1.14.0
[autoinstallsdev/mediawiki.git] / includes / filerepo / File.php
1 <?php
2
3 /**
4  * Implements some public methods and some protected utility functions which
5  * are required by multiple child classes. Contains stub functionality for
6  * unimplemented public methods.
7  *
8  * Stub functions which should be overridden are marked with STUB. Some more
9  * concrete functions are also typically overridden by child classes.
10  *
11  * Note that only the repo object knows what its file class is called. You should
12  * never name a file class explictly outside of the repo class. Instead use the
13  * repo's factory functions to generate file objects, for example:
14  *
15  * RepoGroup::singleton()->getLocalRepo()->newFile($title);
16  *
17  * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
18  * in most cases.
19  *
20  * @ingroup FileRepo
21  */
22 abstract class File {
23         const DELETED_FILE = 1;
24         const DELETED_COMMENT = 2;
25         const DELETED_USER = 4;
26         const DELETED_RESTRICTED = 8;
27         const RENDER_NOW = 1;
28
29         const DELETE_SOURCE = 1;
30
31         /**
32          * Some member variables can be lazy-initialised using __get(). The
33          * initialisation function for these variables is always a function named
34          * like getVar(), where Var is the variable name with upper-case first
35          * letter.
36          *
37          * The following variables are initialised in this way in this base class:
38          *    name, extension, handler, path, canRender, isSafeFile,
39          *    transformScript, hashPath, pageCount, url
40          *
41          * Code within this class should generally use the accessor function
42          * directly, since __get() isn't re-entrant and therefore causes bugs that
43          * depend on initialisation order.
44          */
45
46         /**
47          * The following member variables are not lazy-initialised
48          */
49         var $repo, $title, $lastError, $redirected, $redirectedTitle;
50
51         /**
52          * Call this constructor from child classes
53          */
54         function __construct( $title, $repo ) {
55                 $this->title = $title;
56                 $this->repo = $repo;
57         }
58
59         function __get( $name ) {
60                 $function = array( $this, 'get' . ucfirst( $name ) );
61                 if ( !is_callable( $function ) ) {
62                         return null;
63                 } else {
64                         $this->$name = call_user_func( $function );
65                         return $this->$name;
66                 }
67         }
68
69         /**
70          * Normalize a file extension to the common form, and ensure it's clean.
71          * Extensions with non-alphanumeric characters will be discarded.
72          *
73          * @param $ext string (without the .)
74          * @return string
75          */
76         static function normalizeExtension( $ext ) {
77                 $lower = strtolower( $ext );
78                 $squish = array(
79                         'htm' => 'html',
80                         'jpeg' => 'jpg',
81                         'mpeg' => 'mpg',
82                         'tiff' => 'tif',
83                         'ogv' => 'ogg' );
84                 if( isset( $squish[$lower] ) ) {
85                         return $squish[$lower];
86                 } elseif( preg_match( '/^[0-9a-z]+$/', $lower ) ) {
87                         return $lower;
88                 } else {
89                         return '';
90                 }
91         }
92
93         /**
94          * Checks if file extensions are compatible
95          *
96          * @param $old File Old file
97          * @param $new string New name
98          */
99         static function checkExtensionCompatibility( File $old, $new ) {
100                 $oldMime = $old->getMimeType();
101                 $n = strrpos( $new, '.' );
102                 $newExt = self::normalizeExtension(
103                         $n ? substr( $new, $n + 1 ) : '' );
104                 $mimeMagic = MimeMagic::singleton();
105                 return $mimeMagic->isMatchingExtension( $newExt, $oldMime );
106         }
107
108         /**
109          * Upgrade the database row if there is one
110          * Called by ImagePage
111          * STUB
112          */
113         function upgradeRow() {}
114
115         /**
116          * Split an internet media type into its two components; if not
117          * a two-part name, set the minor type to 'unknown'.
118          *
119          * @param $mime "text/html" etc
120          * @return array ("text", "html") etc
121          */
122         static function splitMime( $mime ) {
123                 if( strpos( $mime, '/' ) !== false ) {
124                         return explode( '/', $mime, 2 );
125                 } else {
126                         return array( $mime, 'unknown' );
127                 }
128         }
129
130         /**
131          * Return the name of this file
132          */
133         public function getName() {
134                 if ( !isset( $this->name ) ) {
135                         $this->name = $this->repo->getNameFromTitle( $this->title );
136                 }
137                 return $this->name;
138         }
139
140         /**
141          * Get the file extension, e.g. "svg"
142          */
143         function getExtension() {
144                 if ( !isset( $this->extension ) ) {
145                         $n = strrpos( $this->getName(), '.' );
146                         $this->extension = self::normalizeExtension(
147                                 $n ? substr( $this->getName(), $n + 1 ) : '' );
148                 }
149                 return $this->extension;
150         }
151
152         /**
153          * Return the associated title object
154          */
155         public function getTitle() { return $this->title; }
156         
157         /**
158          * Return the title used to find this file
159          */
160         public function getOriginalTitle() {
161                 if ( $this->redirected )
162                         return $this->getRedirectedTitle();
163                 return $this->title;
164         }
165
166         /**
167          * Return the URL of the file
168          */
169         public function getUrl() {
170                 if ( !isset( $this->url ) ) {
171                         $this->url = $this->repo->getZoneUrl( 'public' ) . '/' . $this->getUrlRel();
172                 }
173                 return $this->url;
174         }
175
176         /**
177          * Return a fully-qualified URL to the file.
178          * Upload URL paths _may or may not_ be fully qualified, so
179          * we check. Local paths are assumed to belong on $wgServer.
180          * @return string
181          */
182         public function getFullUrl() {
183                 return wfExpandUrl( $this->getUrl() );
184         }
185
186         function getViewURL() {
187                 if( $this->mustRender()) {
188                         if( $this->canRender() ) {
189                                 return $this->createThumb( $this->getWidth() );
190                         }
191                         else {
192                                 wfDebug(__METHOD__.': supposed to render '.$this->getName().' ('.$this->getMimeType()."), but can't!\n");
193                                 return $this->getURL(); #hm... return NULL?
194                         }
195                 } else {
196                         return $this->getURL();
197                 }
198         }
199
200         /**
201         * Return the full filesystem path to the file. Note that this does
202         * not mean that a file actually exists under that location.
203         *
204         * This path depends on whether directory hashing is active or not,
205         * i.e. whether the files are all found in the same directory,
206         * or in hashed paths like /images/3/3c.
207         *
208         * May return false if the file is not locally accessible.
209         */
210         public function getPath() {
211                 if ( !isset( $this->path ) ) {
212                         $this->path = $this->repo->getZonePath('public') . '/' . $this->getRel();
213                 }
214                 return $this->path;
215         }
216
217         /**
218         * Alias for getPath()
219         */
220         public function getFullPath() {
221                 return $this->getPath();
222         }
223
224         /**
225          * Return the width of the image. Returns false if the width is unknown
226          * or undefined.
227          *
228          * STUB
229          * Overridden by LocalFile, UnregisteredLocalFile
230          */
231         public function getWidth( $page = 1 ) { return false; }
232
233         /**
234          * Return the height of the image. Returns false if the height is unknown
235          * or undefined
236          *
237          * STUB
238          * Overridden by LocalFile, UnregisteredLocalFile
239          */
240         public function getHeight( $page = 1 ) { return false; }
241
242         /**
243          * Returns ID or name of user who uploaded the file
244          * STUB
245          *
246          * @param $type string 'text' or 'id'
247          */
248         public function getUser( $type='text' ) { return null; }
249
250         /**
251          * Get the duration of a media file in seconds
252          */
253         public function getLength() {
254                 $handler = $this->getHandler();
255                 if ( $handler ) {
256                         return $handler->getLength( $this );
257                 } else {
258                         return 0;
259                 }
260         }
261
262         /**
263          * Get handler-specific metadata
264          * Overridden by LocalFile, UnregisteredLocalFile
265          * STUB
266          */
267         public function getMetadata() { return false; }
268
269         /**
270          * Return the bit depth of the file
271          * Overridden by LocalFile
272          * STUB
273          */
274         public function getBitDepth() { return 0; }
275
276         /**
277          * Return the size of the image file, in bytes
278          * Overridden by LocalFile, UnregisteredLocalFile
279          * STUB
280          */
281         public function getSize() { return false; }
282
283         /**
284          * Returns the mime type of the file.
285          * Overridden by LocalFile, UnregisteredLocalFile
286          * STUB
287          */
288         function getMimeType() { return 'unknown/unknown'; }
289
290         /**
291          * Return the type of the media in the file.
292          * Use the value returned by this function with the MEDIATYPE_xxx constants.
293          * Overridden by LocalFile,
294          * STUB
295          */
296         function getMediaType() { return MEDIATYPE_UNKNOWN; }
297
298         /**
299          * Checks if the output of transform() for this file is likely
300          * to be valid. If this is false, various user elements will
301          * display a placeholder instead.
302          *
303          * Currently, this checks if the file is an image format
304          * that can be converted to a format
305          * supported by all browsers (namely GIF, PNG and JPEG),
306          * or if it is an SVG image and SVG conversion is enabled.
307          */
308         function canRender() {
309                 if ( !isset( $this->canRender ) ) {
310                         $this->canRender = $this->getHandler() && $this->handler->canRender( $this );
311                 }
312                 return $this->canRender;
313         }
314
315         /**
316          * Accessor for __get()
317          */
318         protected function getCanRender() {
319                 return $this->canRender();
320         }
321
322         /**
323          * Return true if the file is of a type that can't be directly
324          * rendered by typical browsers and needs to be re-rasterized.
325          *
326          * This returns true for everything but the bitmap types
327          * supported by all browsers, i.e. JPEG; GIF and PNG. It will
328          * also return true for any non-image formats.
329          *
330          * @return bool
331          */
332         function mustRender() {
333                 return $this->getHandler() && $this->handler->mustRender( $this );
334         }
335
336         /**
337          * Alias for canRender()
338          */
339         function allowInlineDisplay() {
340                 return $this->canRender();
341         }
342
343         /**
344          * Determines if this media file is in a format that is unlikely to
345          * contain viruses or malicious content. It uses the global
346          * $wgTrustedMediaFormats list to determine if the file is safe.
347          *
348          * This is used to show a warning on the description page of non-safe files.
349          * It may also be used to disallow direct [[media:...]] links to such files.
350          *
351          * Note that this function will always return true if allowInlineDisplay()
352          * or isTrustedFile() is true for this file.
353          */
354         function isSafeFile() {
355                 if ( !isset( $this->isSafeFile ) ) {
356                         $this->isSafeFile = $this->_getIsSafeFile();
357                 }
358                 return $this->isSafeFile;
359         }
360
361         /** Accessor for __get() */
362         protected function getIsSafeFile() {
363                 return $this->isSafeFile();
364         }
365
366         /** Uncached accessor */
367         protected function _getIsSafeFile() {
368                 if ($this->allowInlineDisplay()) return true;
369                 if ($this->isTrustedFile()) return true;
370
371                 global $wgTrustedMediaFormats;
372
373                 $type= $this->getMediaType();
374                 $mime= $this->getMimeType();
375                 #wfDebug("LocalFile::isSafeFile: type= $type, mime= $mime\n");
376
377                 if (!$type || $type===MEDIATYPE_UNKNOWN) return false; #unknown type, not trusted
378                 if ( in_array( $type, $wgTrustedMediaFormats) ) return true;
379
380                 if ($mime==="unknown/unknown") return false; #unknown type, not trusted
381                 if ( in_array( $mime, $wgTrustedMediaFormats) ) return true;
382
383                 return false;
384         }
385
386         /** Returns true if the file is flagged as trusted. Files flagged that way
387         * can be linked to directly, even if that is not allowed for this type of
388         * file normally.
389         *
390         * This is a dummy function right now and always returns false. It could be
391         * implemented to extract a flag from the database. The trusted flag could be
392         * set on upload, if the user has sufficient privileges, to bypass script-
393         * and html-filters. It may even be coupled with cryptographics signatures
394         * or such.
395         */
396         function isTrustedFile() {
397                 #this could be implemented to check a flag in the databas,
398                 #look for signatures, etc
399                 return false;
400         }
401
402         /**
403          * Returns true if file exists in the repository.
404          *
405          * Overridden by LocalFile to avoid unnecessary stat calls.
406          *
407          * @return boolean Whether file exists in the repository.
408          */
409         public function exists() {
410                 return $this->getPath() && file_exists( $this->path );
411         }
412
413         /**
414          * Returns true if file exists in the repository and can be included in a page.
415          * It would be unsafe to include private images, making public thumbnails inadvertently
416          *
417          * @return boolean Whether file exists in the repository and is includable.
418          * @public
419          */
420         function isVisible() {
421                 return $this->exists();
422         }
423
424         function getTransformScript() {
425                 if ( !isset( $this->transformScript ) ) {
426                         $this->transformScript = false;
427                         if ( $this->repo ) {
428                                 $script = $this->repo->getThumbScriptUrl();
429                                 if ( $script ) {
430                                         $this->transformScript = "$script?f=" . urlencode( $this->getName() );
431                                 }
432                         }
433                 }
434                 return $this->transformScript;
435         }
436
437         /**
438          * Get a ThumbnailImage which is the same size as the source
439          */
440         function getUnscaledThumb( $page = false ) {
441                 $width = $this->getWidth( $page );
442                 if ( !$width ) {
443                         return $this->iconThumb();
444                 }
445                 if ( $page ) {
446                         $params = array(
447                                 'page' => $page,
448                                 'width' => $this->getWidth( $page )
449                         );
450                 } else {
451                         $params = array( 'width' => $this->getWidth() );
452                 }
453                 return $this->transform( $params );
454         }
455
456         /**
457          * Return the file name of a thumbnail with the specified parameters
458          *
459          * @param array $params Handler-specific parameters
460          * @private -ish
461          */
462         function thumbName( $params ) {
463                 if ( !$this->getHandler() ) {
464                         return null;
465                 }
466                 $extension = $this->getExtension();
467                 list( $thumbExt, $thumbMime ) = $this->handler->getThumbType( $extension, $this->getMimeType() );
468                 $thumbName = $this->handler->makeParamString( $params ) . '-' . $this->getName();
469                 if ( $thumbExt != $extension ) {
470                         $thumbName .= ".$thumbExt";
471                 }
472                 return $thumbName;
473         }
474
475         /**
476          * Create a thumbnail of the image having the specified width/height.
477          * The thumbnail will not be created if the width is larger than the
478          * image's width. Let the browser do the scaling in this case.
479          * The thumbnail is stored on disk and is only computed if the thumbnail
480          * file does not exist OR if it is older than the image.
481          * Returns the URL.
482          *
483          * Keeps aspect ratio of original image. If both width and height are
484          * specified, the generated image will be no bigger than width x height,
485          * and will also have correct aspect ratio.
486          *
487          * @param integer $width        maximum width of the generated thumbnail
488          * @param integer $height       maximum height of the image (optional)
489          */
490         public function createThumb( $width, $height = -1 ) {
491                 $params = array( 'width' => $width );
492                 if ( $height != -1 ) {
493                         $params['height'] = $height;
494                 }
495                 $thumb = $this->transform( $params );
496                 if( is_null( $thumb ) || $thumb->isError() ) return '';
497                 return $thumb->getUrl();
498         }
499
500         /**
501          * As createThumb, but returns a ThumbnailImage object. This can
502          * provide access to the actual file, the real size of the thumb,
503          * and can produce a convenient <img> tag for you.
504          *
505          * For non-image formats, this may return a filetype-specific icon.
506          *
507          * @param integer $width        maximum width of the generated thumbnail
508          * @param integer $height       maximum height of the image (optional)
509          * @param boolean $render       Deprecated
510          *
511          * @return ThumbnailImage or null on failure
512          *
513          * @deprecated use transform()
514          */
515         public function getThumbnail( $width, $height=-1, $render = true ) {
516                 $params = array( 'width' => $width );
517                 if ( $height != -1 ) {
518                         $params['height'] = $height;
519                 }
520                 return $this->transform( $params, 0 );
521         }
522
523         /**
524          * Transform a media file
525          *
526          * @param array $params An associative array of handler-specific parameters. Typical
527          *                      keys are width, height and page.
528          * @param integer $flags A bitfield, may contain self::RENDER_NOW to force rendering
529          * @return MediaTransformOutput
530          */
531         function transform( $params, $flags = 0 ) {
532                 global $wgUseSquid, $wgIgnoreImageErrors;
533
534                 wfProfileIn( __METHOD__ );
535                 do {
536                         if ( !$this->canRender() ) {
537                                 // not a bitmap or renderable image, don't try.
538                                 $thumb = $this->iconThumb();
539                                 break;
540                         }
541
542                         $script = $this->getTransformScript();
543                         if ( $script && !($flags & self::RENDER_NOW) ) {
544                                 // Use a script to transform on client request, if possible
545                                 $thumb = $this->handler->getScriptedTransform( $this, $script, $params );
546                                 if( $thumb ) {
547                                         break;
548                                 }
549                         }
550
551                         $normalisedParams = $params;
552                         $this->handler->normaliseParams( $this, $normalisedParams );
553                         $thumbName = $this->thumbName( $normalisedParams );
554                         $thumbPath = $this->getThumbPath( $thumbName );
555                         $thumbUrl = $this->getThumbUrl( $thumbName );
556
557                         if ( $this->repo->canTransformVia404() && !($flags & self::RENDER_NOW ) ) {
558                                 $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
559                                 break;
560                         }
561
562                         wfDebug( __METHOD__.": Doing stat for $thumbPath\n" );
563                         $this->migrateThumbFile( $thumbName );
564                         if ( file_exists( $thumbPath ) ) {
565                                 $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
566                                 break;
567                         }
568                         $thumb = $this->handler->doTransform( $this, $thumbPath, $thumbUrl, $params );
569
570                         // Ignore errors if requested
571                         if ( !$thumb ) {
572                                 $thumb = null;
573                         } elseif ( $thumb->isError() ) {
574                                 $this->lastError = $thumb->toText();
575                                 if ( $wgIgnoreImageErrors && !($flags & self::RENDER_NOW) ) {
576                                         $thumb = $this->handler->getTransform( $this, $thumbPath, $thumbUrl, $params );
577                                 }
578                         }
579                         
580                         // Purge. Useful in the event of Core -> Squid connection failure or squid 
581                         // purge collisions from elsewhere during failure. Don't keep triggering for 
582                         // "thumbs" which have the main image URL though (bug 13776)
583                         if ( $wgUseSquid && ( !$thumb || $thumb->isError() || $thumb->getUrl() != $this->getURL()) ) {
584                                 SquidUpdate::purge( array( $thumbUrl ) );
585                         }
586                 } while (false);
587
588                 wfProfileOut( __METHOD__ );
589                 return $thumb;
590         }
591
592         /**
593          * Hook into transform() to allow migration of thumbnail files
594          * STUB
595          * Overridden by LocalFile
596          */
597         function migrateThumbFile( $thumbName ) {}
598
599         /**
600          * Get a MediaHandler instance for this file
601          */
602         function getHandler() {
603                 if ( !isset( $this->handler ) ) {
604                         $this->handler = MediaHandler::getHandler( $this->getMimeType() );
605                 }
606                 return $this->handler;
607         }
608
609         /**
610          * Get a ThumbnailImage representing a file type icon
611          * @return ThumbnailImage
612          */
613         function iconThumb() {
614                 global $wgStylePath, $wgStyleDirectory;
615
616                 $try = array( 'fileicon-' . $this->getExtension() . '.png', 'fileicon.png' );
617                 foreach( $try as $icon ) {
618                         $path = '/common/images/icons/' . $icon;
619                         $filepath = $wgStyleDirectory . $path;
620                         if( file_exists( $filepath ) ) {
621                                 return new ThumbnailImage( $this, $wgStylePath . $path, 120, 120 );
622                         }
623                 }
624                 return null;
625         }
626
627         /**
628          * Get last thumbnailing error.
629          * Largely obsolete.
630          */
631         function getLastError() {
632                 return $this->lastError;
633         }
634
635         /**
636          * Get all thumbnail names previously generated for this file
637          * STUB
638          * Overridden by LocalFile
639          */
640         function getThumbnails() { return array(); }
641
642         /**
643          * Purge shared caches such as thumbnails and DB data caching
644          * STUB
645          * Overridden by LocalFile
646          */
647         function purgeCache() {}
648
649         /**
650          * Purge the file description page, but don't go after
651          * pages using the file. Use when modifying file history
652          * but not the current data.
653          */
654         function purgeDescription() {
655                 $title = $this->getTitle();
656                 if ( $title ) {
657                         $title->invalidateCache();
658                         $title->purgeSquid();
659                 }
660         }
661
662         /**
663          * Purge metadata and all affected pages when the file is created,
664          * deleted, or majorly updated.
665          */
666         function purgeEverything() {
667                 // Delete thumbnails and refresh file metadata cache
668                 $this->purgeCache();
669                 $this->purgeDescription();
670
671                 // Purge cache of all pages using this file
672                 $title = $this->getTitle();
673                 if ( $title ) {
674                         $update = new HTMLCacheUpdate( $title, 'imagelinks' );
675                         $update->doUpdate();
676                 }
677         }
678
679         /**
680          * Return a fragment of the history of file.
681          *
682          * STUB
683          * @param $limit integer Limit of rows to return
684          * @param $start timestamp Only revisions older than $start will be returned
685          * @param $end timestamp Only revisions newer than $end will be returned
686          * @param $inc bool Include the endpoints of the time range
687          */
688         function getHistory($limit = null, $start = null, $end = null, $inc=true) {
689                 return array();
690         }
691
692         /**
693          * Return the history of this file, line by line. Starts with current version,
694          * then old versions. Should return an object similar to an image/oldimage
695          * database row.
696          *
697          * STUB
698          * Overridden in LocalFile
699          */
700         public function nextHistoryLine() {
701                 return false;
702         }
703
704         /**
705          * Reset the history pointer to the first element of the history.
706          * Always call this function after using nextHistoryLine() to free db resources
707          * STUB
708          * Overridden in LocalFile.
709          */
710         public function resetHistory() {}
711
712         /**
713          * Get the filename hash component of the directory including trailing slash,
714          * e.g. f/fa/
715          * If the repository is not hashed, returns an empty string.
716          */
717         function getHashPath() {
718                 if ( !isset( $this->hashPath ) ) {
719                         $this->hashPath = $this->repo->getHashPath( $this->getName() );
720                 }
721                 return $this->hashPath;
722         }
723
724         /**
725          * Get the path of the file relative to the public zone root
726          */
727         function getRel() {
728                 return $this->getHashPath() . $this->getName();
729         }
730
731         /**
732          * Get urlencoded relative path of the file
733          */
734         function getUrlRel() {
735                 return $this->getHashPath() . rawurlencode( $this->getName() );
736         }
737
738         /** Get the relative path for an archive file */
739         function getArchiveRel( $suffix = false ) {
740                 $path = 'archive/' . $this->getHashPath();
741                 if ( $suffix === false ) {
742                         $path = substr( $path, 0, -1 );
743                 } else {
744                         $path .= $suffix;
745                 }
746                 return $path;
747         }
748
749         /** Get relative path for a thumbnail file */
750         function getThumbRel( $suffix = false ) {
751                 $path = 'thumb/' . $this->getRel();
752                 if ( $suffix !== false ) {
753                         $path .= '/' . $suffix;
754                 }
755                 return $path;
756         }
757
758         /** Get the path of the archive directory, or a particular file if $suffix is specified */
759         function getArchivePath( $suffix = false ) {
760                 return $this->repo->getZonePath('public') . '/' . $this->getArchiveRel( $suffix );
761         }
762
763         /** Get the path of the thumbnail directory, or a particular file if $suffix is specified */
764         function getThumbPath( $suffix = false ) {
765                 return $this->repo->getZonePath('public') . '/' . $this->getThumbRel( $suffix );
766         }
767
768         /** Get the URL of the archive directory, or a particular file if $suffix is specified */
769         function getArchiveUrl( $suffix = false ) {
770                 $path = $this->repo->getZoneUrl('public') . '/archive/' . $this->getHashPath();
771                 if ( $suffix === false ) {
772                         $path = substr( $path, 0, -1 );
773                 } else {
774                         $path .= rawurlencode( $suffix );
775                 }
776                 return $path;
777         }
778
779         /** Get the URL of the thumbnail directory, or a particular file if $suffix is specified */
780         function getThumbUrl( $suffix = false ) {
781                 $path = $this->repo->getZoneUrl('public') . '/thumb/' . $this->getUrlRel();
782                 if ( $suffix !== false ) {
783                         $path .= '/' . rawurlencode( $suffix );
784                 }
785                 return $path;
786         }
787
788         /** Get the virtual URL for an archive file or directory */
789         function getArchiveVirtualUrl( $suffix = false ) {
790                 $path = $this->repo->getVirtualUrl() . '/public/archive/' . $this->getHashPath();
791                 if ( $suffix === false ) {
792                         $path = substr( $path, 0, -1 );
793                 } else {
794                         $path .= rawurlencode( $suffix );
795                 }
796                 return $path;
797         }
798
799         /** Get the virtual URL for a thumbnail file or directory */
800         function getThumbVirtualUrl( $suffix = false ) {
801                 $path = $this->repo->getVirtualUrl() . '/public/thumb/' . $this->getUrlRel();
802                 if ( $suffix !== false ) {
803                         $path .= '/' . rawurlencode( $suffix );
804                 }
805                 return $path;
806         }
807
808         /** Get the virtual URL for the file itself */
809         function getVirtualUrl( $suffix = false ) {
810                 $path = $this->repo->getVirtualUrl() . '/public/' . $this->getUrlRel();
811                 if ( $suffix !== false ) {
812                         $path .= '/' . rawurlencode( $suffix );
813                 }
814                 return $path;
815         }
816
817         /**
818          * @return bool
819          */
820         function isHashed() {
821                 return $this->repo->isHashed();
822         }
823
824         function readOnlyError() {
825                 throw new MWException( get_class($this) . ': write operations are not supported' );
826         }
827
828         /**
829          * Record a file upload in the upload log and the image table
830          * STUB
831          * Overridden by LocalFile
832          */
833         function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '', $watch = false ) {
834                 $this->readOnlyError();
835         }
836
837         /**
838          * Move or copy a file to its public location. If a file exists at the
839          * destination, move it to an archive. Returns the archive name on success
840          * or an empty string if it was a new file, and a wikitext-formatted
841          * WikiError object on failure.
842          *
843          * The archive name should be passed through to recordUpload for database
844          * registration.
845          *
846          * @param string $sourcePath Local filesystem path to the source image
847          * @param integer $flags A bitwise combination of:
848          *     File::DELETE_SOURCE    Delete the source file, i.e. move
849          *         rather than copy
850          * @return The archive name on success or an empty string if it was a new
851          *     file, and a wikitext-formatted WikiError object on failure.
852          *
853          * STUB
854          * Overridden by LocalFile
855          */
856         function publish( $srcPath, $flags = 0 ) {
857                 $this->readOnlyError();
858         }
859
860         /**
861          * Get an array of Title objects which are articles which use this file
862          * Also adds their IDs to the link cache
863          *
864          * This is mostly copied from Title::getLinksTo()
865          *
866          * @deprecated Use HTMLCacheUpdate, this function uses too much memory
867          */
868         function getLinksTo( $options = '' ) {
869                 wfProfileIn( __METHOD__ );
870
871                 // Note: use local DB not repo DB, we want to know local links
872                 if ( $options ) {
873                         $db = wfGetDB( DB_MASTER );
874                 } else {
875                         $db = wfGetDB( DB_SLAVE );
876                 }
877                 $linkCache = LinkCache::singleton();
878
879                 list( $page, $imagelinks ) = $db->tableNamesN( 'page', 'imagelinks' );
880                 $encName = $db->addQuotes( $this->getName() );
881                 $sql = "SELECT page_namespace,page_title,page_id,page_len,page_is_redirect,
882                         FROM $page,$imagelinks WHERE page_id=il_from AND il_to=$encName $options";
883                 $res = $db->query( $sql, __METHOD__ );
884
885                 $retVal = array();
886                 if ( $db->numRows( $res ) ) {
887                         while ( $row = $db->fetchObject( $res ) ) {
888                                 if ( $titleObj = Title::newFromRow( $row ) ) {
889                                         $linkCache->addGoodLinkObj( $row->page_id, $titleObj, $row->page_len, $row->page_is_redirect );
890                                         $retVal[] = $titleObj;
891                                 }
892                         }
893                 }
894                 $db->freeResult( $res );
895                 wfProfileOut( __METHOD__ );
896                 return $retVal;
897         }
898
899         function formatMetadata() {
900                 if ( !$this->getHandler() ) {
901                         return false;
902                 }
903                 return $this->getHandler()->formatMetadata( $this, $this->getMetadata() );
904         }
905
906         /**
907          * Returns true if the file comes from the local file repository.
908          *
909          * @return bool
910          */
911         function isLocal() {
912                 return $this->getRepoName() == 'local';
913         }
914
915         /**
916          * Returns the name of the repository.
917          *
918          * @return string
919          */
920         function getRepoName() {
921                 return $this->repo ? $this->repo->getName() : 'unknown';
922         }
923         /*
924          * Returns the repository
925          */
926         function getRepo() {
927                 return $this->repo;
928         }
929
930         /**
931          * Returns true if the image is an old version
932          * STUB
933          */
934         function isOld() {
935                 return false;
936         }
937
938         /**
939          * Is this file a "deleted" file in a private archive?
940          * STUB
941          */
942         function isDeleted( $field ) {
943                 return false;
944         }
945
946         /**
947          * Was this file ever deleted from the wiki?
948          *
949          * @return bool
950          */
951         function wasDeleted() {
952                 $title = $this->getTitle();
953                 return $title && $title->isDeleted() > 0;
954         }
955
956         /**
957          * Move file to the new title
958          *
959          * Move current, old version and all thumbnails
960          * to the new filename. Old file is deleted.
961          *
962          * Cache purging is done; checks for validity
963          * and logging are caller's responsibility
964          *
965          * @param $target Title New file name
966          * @return FileRepoStatus object.
967          */
968          function move( $target ) {
969                 $this->readOnlyError();
970          }
971
972         /**
973          * Delete all versions of the file.
974          *
975          * Moves the files into an archive directory (or deletes them)
976          * and removes the database rows.
977          *
978          * Cache purging is done; logging is caller's responsibility.
979          *
980          * @param $reason
981          * @param $suppress, hide content from sysops?
982          * @return true on success, false on some kind of failure
983          * STUB
984          * Overridden by LocalFile
985          */
986         function delete( $reason, $suppress = false ) {
987                 $this->readOnlyError();
988         }
989
990         /**
991          * Restore all or specified deleted revisions to the given file.
992          * Permissions and logging are left to the caller.
993          *
994          * May throw database exceptions on error.
995          *
996          * @param $versions set of record ids of deleted items to restore,
997          *                    or empty to restore all revisions.
998          * @param $unsuppress, remove restrictions on content upon restoration?
999          * @return the number of file revisions restored if successful,
1000          *         or false on failure
1001          * STUB
1002          * Overridden by LocalFile
1003          */
1004         function restore( $versions=array(), $unsuppress=false ) {
1005                 $this->readOnlyError();
1006         }
1007
1008         /**
1009          * Returns 'true' if this image is a multipage document, e.g. a DJVU
1010          * document.
1011          *
1012          * @return Bool
1013          */
1014         function isMultipage() {
1015                 return $this->getHandler() && $this->handler->isMultiPage( $this );
1016         }
1017
1018         /**
1019          * Returns the number of pages of a multipage document, or NULL for
1020          * documents which aren't multipage documents
1021          */
1022         function pageCount() {
1023                 if ( !isset( $this->pageCount ) ) {
1024                         if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
1025                                 $this->pageCount = $this->handler->pageCount( $this );
1026                         } else {
1027                                 $this->pageCount = false;
1028                         }
1029                 }
1030                 return $this->pageCount;
1031         }
1032
1033         /**
1034          * Calculate the height of a thumbnail using the source and destination width
1035          */
1036         static function scaleHeight( $srcWidth, $srcHeight, $dstWidth ) {
1037                 // Exact integer multiply followed by division
1038                 if ( $srcWidth == 0 ) {
1039                         return 0;
1040                 } else {
1041                         return round( $srcHeight * $dstWidth / $srcWidth );
1042                 }
1043         }
1044
1045         /**
1046          * Get an image size array like that returned by getimagesize(), or false if it
1047          * can't be determined.
1048          *
1049          * @param string $fileName The filename
1050          * @return array
1051          */
1052         function getImageSize( $fileName ) {
1053                 if ( !$this->getHandler() ) {
1054                         return false;
1055                 }
1056                 return $this->handler->getImageSize( $this, $fileName );
1057         }
1058
1059         /**
1060          * Get the URL of the image description page. May return false if it is
1061          * unknown or not applicable.
1062          */
1063         function getDescriptionUrl() {
1064                 return $this->repo->getDescriptionUrl( $this->getName() );
1065         }
1066
1067         /**
1068          * Get the HTML text of the description page, if available
1069          */
1070         function getDescriptionText() {
1071                 global $wgMemc;
1072                 if ( !$this->repo->fetchDescription ) {
1073                         return false;
1074                 }
1075                 $renderUrl = $this->repo->getDescriptionRenderUrl( $this->getName() );
1076                 if ( $renderUrl ) {
1077                         if ( $this->repo->descriptionCacheExpiry > 0 ) {
1078                                 wfDebug("Attempting to get the description from cache...");
1079                                 $key = wfMemcKey( 'RemoteFileDescription', 'url', md5($renderUrl) );
1080                                 $obj = $wgMemc->get($key);
1081                                 if ($obj) {
1082                                         wfDebug("success!\n");
1083                                         return $obj;
1084                                 }
1085                                 wfDebug("miss\n");
1086                         }
1087                         wfDebug( "Fetching shared description from $renderUrl\n" );
1088                         $res = Http::get( $renderUrl );
1089                         if ( $res && $this->repo->descriptionCacheExpiry > 0 ) $wgMemc->set( $key, $res, $this->repo->descriptionCacheExpiry );
1090                         return $res;
1091                 } else {
1092                         return false;
1093                 }
1094         }
1095
1096         /**
1097          * Get discription of file revision
1098          * STUB
1099          */
1100         function getDescription() {
1101                 return null;
1102         }
1103
1104         /**
1105          * Get the 14-character timestamp of the file upload, or false if
1106          * it doesn't exist
1107          */
1108         function getTimestamp() {
1109                 $path = $this->getPath();
1110                 if ( !file_exists( $path ) ) {
1111                         return false;
1112                 }
1113                 return wfTimestamp( TS_MW, filemtime( $path ) );
1114         }
1115
1116         /**
1117          * Get the SHA-1 base 36 hash of the file
1118          */
1119         function getSha1() {
1120                 return self::sha1Base36( $this->getPath() );
1121         }
1122
1123         /**
1124          * Determine if the current user is allowed to view a particular
1125          * field of this file, if it's marked as deleted.
1126          * STUB
1127          * @param int $field
1128          * @return bool
1129          */
1130         function userCan( $field ) {
1131                 return true;
1132         }
1133
1134         /**
1135          * Get an associative array containing information about a file in the local filesystem.
1136          *
1137          * @param string $path Absolute local filesystem path
1138          * @param mixed $ext The file extension, or true to extract it from the filename.
1139          *                   Set it to false to ignore the extension.
1140          */
1141         static function getPropsFromPath( $path, $ext = true ) {
1142                 wfProfileIn( __METHOD__ );
1143                 wfDebug( __METHOD__.": Getting file info for $path\n" );
1144                 $info = array(
1145                         'fileExists' => file_exists( $path ) && !is_dir( $path )
1146                 );
1147                 $gis = false;
1148                 if ( $info['fileExists'] ) {
1149                         $magic = MimeMagic::singleton();
1150
1151                         $info['mime'] = $magic->guessMimeType( $path, $ext );
1152                         list( $info['major_mime'], $info['minor_mime'] ) = self::splitMime( $info['mime'] );
1153                         $info['media_type'] = $magic->getMediaType( $path, $info['mime'] );
1154
1155                         # Get size in bytes
1156                         $info['size'] = filesize( $path );
1157
1158                         # Height, width and metadata
1159                         $handler = MediaHandler::getHandler( $info['mime'] );
1160                         if ( $handler ) {
1161                                 $tempImage = (object)array();
1162                                 $info['metadata'] = $handler->getMetadata( $tempImage, $path );
1163                                 $gis = $handler->getImageSize( $tempImage, $path, $info['metadata'] );
1164                         } else {
1165                                 $gis = false;
1166                                 $info['metadata'] = '';
1167                         }
1168                         $info['sha1'] = self::sha1Base36( $path );
1169
1170                         wfDebug(__METHOD__.": $path loaded, {$info['size']} bytes, {$info['mime']}.\n");
1171                 } else {
1172                         $info['mime'] = NULL;
1173                         $info['media_type'] = MEDIATYPE_UNKNOWN;
1174                         $info['metadata'] = '';
1175                         $info['sha1'] = '';
1176                         wfDebug(__METHOD__.": $path NOT FOUND!\n");
1177                 }
1178                 if( $gis ) {
1179                         # NOTE: $gis[2] contains a code for the image type. This is no longer used.
1180                         $info['width'] = $gis[0];
1181                         $info['height'] = $gis[1];
1182                         if ( isset( $gis['bits'] ) ) {
1183                                 $info['bits'] = $gis['bits'];
1184                         } else {
1185                                 $info['bits'] = 0;
1186                         }
1187                 } else {
1188                         $info['width'] = 0;
1189                         $info['height'] = 0;
1190                         $info['bits'] = 0;
1191                 }
1192                 wfProfileOut( __METHOD__ );
1193                 return $info;
1194         }
1195
1196         /**
1197          * Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case
1198          * encoding, zero padded to 31 digits.
1199          *
1200          * 160 log 2 / log 36 = 30.95, so the 160-bit hash fills 31 digits in base 36
1201          * fairly neatly.
1202          *
1203          * Returns false on failure
1204          */
1205         static function sha1Base36( $path ) {
1206                 wfSuppressWarnings();
1207                 $hash = sha1_file( $path );
1208                 wfRestoreWarnings();
1209                 if ( $hash === false ) {
1210                         return false;
1211                 } else {
1212                         return wfBaseConvert( $hash, 16, 36, 31 );
1213                 }
1214         }
1215
1216         function getLongDesc() {
1217                 $handler = $this->getHandler();
1218                 if ( $handler ) {
1219                         return $handler->getLongDesc( $this );
1220                 } else {
1221                         return MediaHandler::getGeneralLongDesc( $this );
1222                 }
1223         }
1224
1225         function getShortDesc() {
1226                 $handler = $this->getHandler();
1227                 if ( $handler ) {
1228                         return $handler->getShortDesc( $this );
1229                 } else {
1230                         return MediaHandler::getGeneralShortDesc( $this );
1231                 }
1232         }
1233
1234         function getDimensionsString() {
1235                 $handler = $this->getHandler();
1236                 if ( $handler ) {
1237                         return $handler->getDimensionsString( $this );
1238                 } else {
1239                         return '';
1240                 }
1241         }
1242
1243         function getRedirected() {
1244                 return $this->redirected;
1245         }
1246         
1247         function getRedirectedTitle() {
1248                 if ( $this->redirected ) {
1249                         if ( !$this->redirectTitle )
1250                                 $this->redirectTitle = Title::makeTitle( NS_FILE, $this->redirected );
1251                         return $this->redirectTitle;
1252                 }
1253         }
1254
1255         function redirectedFrom( $from ) {
1256                 $this->redirected = $from;
1257         }
1258 }
1259 /**
1260  * Aliases for backwards compatibility with 1.6
1261  */
1262 define( 'MW_IMG_DELETED_FILE', File::DELETED_FILE );
1263 define( 'MW_IMG_DELETED_COMMENT', File::DELETED_COMMENT );
1264 define( 'MW_IMG_DELETED_USER', File::DELETED_USER );
1265 define( 'MW_IMG_DELETED_RESTRICTED', File::DELETED_RESTRICTED );