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