]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/filerepo/FileRepo.php
MediaWiki 1.15.4-scripts
[autoinstalls/mediawiki.git] / includes / filerepo / FileRepo.php
1 <?php
2
3 /**
4  * Base class for file repositories
5  * Do not instantiate, use a derived class.
6  * @ingroup FileRepo
7  */
8 abstract class FileRepo {
9         const DELETE_SOURCE = 1;
10         const FIND_PRIVATE = 1;
11         const FIND_IGNORE_REDIRECT = 2;
12         const OVERWRITE = 2;
13         const OVERWRITE_SAME = 4;
14
15         var $thumbScriptUrl, $transformVia404;
16         var $descBaseUrl, $scriptDirUrl, $articleUrl, $fetchDescription, $initialCapital;
17         var $pathDisclosureProtection = 'paranoid';
18         var $descriptionCacheExpiry, $apiThumbCacheExpiry, $hashLevels;
19
20         /**
21          * Factory functions for creating new files
22          * Override these in the base class
23          */
24         var $fileFactory = false, $oldFileFactory = false;
25         var $fileFactoryKey = false, $oldFileFactoryKey = false;
26
27         function __construct( $info ) {
28                 // Required settings
29                 $this->name = $info['name'];
30
31                 // Optional settings
32                 $this->initialCapital = true; // by default
33                 foreach ( array( 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
34                         'thumbScriptUrl', 'initialCapital', 'pathDisclosureProtection', 
35                         'descriptionCacheExpiry', 'apiThumbCacheExpiry', 'hashLevels' ) as $var )
36                 {
37                         if ( isset( $info[$var] ) ) {
38                                 $this->$var = $info[$var];
39                         }
40                 }
41                 $this->transformVia404 = !empty( $info['transformVia404'] );
42         }
43
44         /**
45          * Determine if a string is an mwrepo:// URL
46          */
47         static function isVirtualUrl( $url ) {
48                 return substr( $url, 0, 9 ) == 'mwrepo://';
49         }
50
51         /**
52          * Create a new File object from the local repository
53          * @param mixed $title Title object or string
54          * @param mixed $time Time at which the image was uploaded.
55          *                    If this is specified, the returned object will be an
56          *                    instance of the repository's old file class instead of
57          *                    a current file. Repositories not supporting version
58          *                    control should return false if this parameter is set.
59          */
60         function newFile( $title, $time = false ) {
61                 if ( !($title instanceof Title) ) {
62                         $title = Title::makeTitleSafe( NS_FILE, $title );
63                         if ( !is_object( $title ) ) {
64                                 return null;
65                         }
66                 }
67                 if ( $time ) {
68                         if ( $this->oldFileFactory ) {
69                                 return call_user_func( $this->oldFileFactory, $title, $this, $time );
70                         } else {
71                                 return false;
72                         }
73                 } else {
74                         return call_user_func( $this->fileFactory, $title, $this );
75                 }
76         }
77
78         /**
79          * Find an instance of the named file created at the specified time
80          * Returns false if the file does not exist. Repositories not supporting
81          * version control should return false if the time is specified.
82          *
83          * @param mixed $title Title object or string
84          * @param mixed $time 14-character timestamp, or false for the current version
85          */
86         function findFile( $title, $time = false, $flags = 0 ) {
87                 if ( !($title instanceof Title) ) {
88                         $title = Title::makeTitleSafe( NS_FILE, $title );
89                         if ( !is_object( $title ) ) {
90                                 return false;
91                         }
92                 }
93                 # First try the current version of the file to see if it precedes the timestamp
94                 $img = $this->newFile( $title );
95                 if ( !$img ) {
96                         return false;
97                 }
98                 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
99                         return $img;
100                 }
101                 # Now try an old version of the file
102                 if ( $time !== false ) {
103                         $img = $this->newFile( $title, $time );
104                         if ( $img && $img->exists() ) {
105                                 if ( !$img->isDeleted(File::DELETED_FILE) ) {
106                                         return $img;
107                                 } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
108                                         return $img;
109                                 }
110                         }
111                 }
112                                 
113                 # Now try redirects
114                 if ( $flags & FileRepo::FIND_IGNORE_REDIRECT ) {
115                         return false;
116                 }
117                 $redir = $this->checkRedirect( $title );                
118                 if( $redir && $redir->getNamespace() == NS_FILE) {
119                         $img = $this->newFile( $redir );
120                         if( !$img ) {
121                                 return false;
122                         }
123                         if( $img->exists() ) {
124                                 $img->redirectedFrom( $title->getDBkey() );
125                                 return $img;
126                         }
127                 }
128                 return false;
129         }
130         
131         /*
132          * Find many files at once. 
133          * @param array $titles, an array of titles
134          * @todo Think of a good way to optionally pass timestamps to this function.
135          */
136         function findFiles( $titles ) {
137                 $result = array();
138                 foreach ( $titles as $index => $title ) {
139                         $file = $this->findFile( $title );
140                         if ( $file )
141                                 $result[$file->getTitle()->getDBkey()] = $file;
142                 }
143                 return $result;
144         }
145         
146         /**
147          * Create a new File object from the local repository
148          * @param mixed $sha1 SHA-1 key
149          * @param mixed $time Time at which the image was uploaded.
150          *                    If this is specified, the returned object will be an
151          *                    instance of the repository's old file class instead of
152          *                    a current file. Repositories not supporting version
153          *                    control should return false if this parameter is set.
154          */
155         function newFileFromKey( $sha1, $time = false ) {
156                 if ( $time ) {
157                         if ( $this->oldFileFactoryKey ) {
158                                 return call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
159                         } else {
160                                 return false;
161                         }
162                 } else {
163                         return call_user_func( $this->fileFactoryKey, $sha1, $this );
164                 }
165         }
166         
167         /**
168          * Find an instance of the file with this key, created at the specified time
169          * Returns false if the file does not exist. Repositories not supporting
170          * version control should return false if the time is specified.
171          *
172          * @param string $sha1 string
173          * @param mixed $time 14-character timestamp, or false for the current version
174          */
175         function findFileFromKey( $sha1, $time = false, $flags = 0 ) {
176                 # First try the current version of the file to see if it precedes the timestamp
177                 $img = $this->newFileFromKey( $sha1 );
178                 if ( !$img ) {
179                         return false;
180                 }
181                 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
182                         return $img;
183                 }
184                 # Now try an old version of the file
185                 if ( $time !== false ) {
186                         $img = $this->newFileFromKey( $sha1, $time );
187                         if ( $img->exists() ) {
188                                 if ( !$img->isDeleted(File::DELETED_FILE) ) {
189                                         return $img;
190                                 } else if ( ($flags & FileRepo::FIND_PRIVATE) && $img->userCan(File::DELETED_FILE) ) {
191                                         return $img;
192                                 }
193                         }
194                 }
195                 return false;
196         }
197
198         /**
199          * Get the URL of thumb.php
200          */
201         function getThumbScriptUrl() {
202                 return $this->thumbScriptUrl;
203         }
204
205         /**
206          * Returns true if the repository can transform files via a 404 handler
207          */
208         function canTransformVia404() {
209                 return $this->transformVia404;
210         }
211
212         /**
213          * Get the name of an image from its title object
214          */
215         function getNameFromTitle( $title ) {
216                 global $wgCapitalLinks;
217                 if ( $this->initialCapital != $wgCapitalLinks ) {
218                         global $wgContLang;
219                         $name = $title->getUserCaseDBKey();
220                         if ( $this->initialCapital ) {
221                                 $name = $wgContLang->ucfirst( $name );
222                         }
223                 } else {
224                         $name = $title->getDBkey();
225                 }
226                 return $name;
227         }
228
229         static function getHashPathForLevel( $name, $levels ) {
230                 if ( $levels == 0 ) {
231                         return '';
232                 } else {
233                         $hash = md5( $name );
234                         $path = '';
235                         for ( $i = 1; $i <= $levels; $i++ ) {
236                                 $path .= substr( $hash, 0, $i ) . '/';
237                         }
238                         return $path;
239                 }
240         }
241         
242         /**
243          * Get a relative path including trailing slash, e.g. f/fa/
244          * If the repo is not hashed, returns an empty string
245          */
246         function getHashPath( $name ) {
247                 return self::getHashPathForLevel( $name, $this->hashLevels );
248         }
249
250         /**
251          * Get the name of this repository, as specified by $info['name]' to the constructor
252          */
253         function getName() {
254                 return $this->name;
255         }
256
257         /**
258          * Get the URL of an image description page. May return false if it is
259          * unknown or not applicable. In general this should only be called by the
260          * File class, since it may return invalid results for certain kinds of
261          * repositories. Use File::getDescriptionUrl() in user code.
262          *
263          * In particular, it uses the article paths as specified to the repository
264          * constructor, whereas local repositories use the local Title functions.
265          */
266         function getDescriptionUrl( $name ) {
267                 $encName = wfUrlencode( $name );
268                 if ( !is_null( $this->descBaseUrl ) ) {
269                         # "http://example.com/wiki/Image:"
270                         return $this->descBaseUrl . $encName;
271                 }
272                 if ( !is_null( $this->articleUrl ) ) {
273                         # "http://example.com/wiki/$1"
274                         #
275                         # We use "Image:" as the canonical namespace for
276                         # compatibility across all MediaWiki versions.
277                         return str_replace( '$1',
278                                 "Image:$encName", $this->articleUrl );
279                 }
280                 if ( !is_null( $this->scriptDirUrl ) ) {
281                         # "http://example.com/w"
282                         #
283                         # We use "Image:" as the canonical namespace for
284                         # compatibility across all MediaWiki versions,
285                         # and just sort of hope index.php is right. ;)
286                         return $this->scriptDirUrl .
287                                 "/index.php?title=Image:$encName";
288                 }
289                 return false;
290         }
291
292         /**
293          * Get the URL of the content-only fragment of the description page. For
294          * MediaWiki this means action=render. This should only be called by the
295          * repository's file class, since it may return invalid results. User code
296          * should use File::getDescriptionText().
297          * @param string $name Name of image to fetch
298          * @param string $lang Language to fetch it in, if any.
299          */
300         function getDescriptionRenderUrl( $name, $lang = null ) {
301                 $query = 'action=render';
302                 if ( !is_null( $lang ) ) {
303                         $query .= '&uselang=' . $lang;
304                 }
305                 if ( isset( $this->scriptDirUrl ) ) {
306                         return $this->scriptDirUrl . '/index.php?title=' .
307                                 wfUrlencode( 'Image:' . $name ) .
308                                 "&$query";
309                 } else {
310                         $descUrl = $this->getDescriptionUrl( $name );
311                         if ( $descUrl ) {
312                                 return wfAppendQuery( $descUrl, $query );
313                         } else {
314                                 return false;
315                         }
316                 }
317         }
318
319         /**
320          * Store a file to a given destination.
321          *
322          * @param string $srcPath Source path or virtual URL
323          * @param string $dstZone Destination zone
324          * @param string $dstRel Destination relative path
325          * @param integer $flags Bitwise combination of the following flags:
326          *     self::DELETE_SOURCE     Delete the source file after upload
327          *     self::OVERWRITE         Overwrite an existing destination file instead of failing
328          *     self::OVERWRITE_SAME    Overwrite the file if the destination exists and has the
329          *                             same contents as the source
330          * @return FileRepoStatus
331          */
332         function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
333                 $status = $this->storeBatch( array( array( $srcPath, $dstZone, $dstRel ) ), $flags );
334                 if ( $status->successCount == 0 ) {
335                         $status->ok = false;
336                 }
337                 return $status;
338         }
339
340         /**
341          * Store a batch of files
342          *
343          * @param array $triplets (src,zone,dest) triplets as per store()
344          * @param integer $flags Flags as per store
345          */
346         abstract function storeBatch( $triplets, $flags = 0 );
347
348         /**
349          * Pick a random name in the temp zone and store a file to it.
350          * Returns a FileRepoStatus object with the URL in the value.
351          *
352          * @param string $originalName The base name of the file as specified
353          *     by the user. The file extension will be maintained.
354          * @param string $srcPath The current location of the file.
355          */
356         abstract function storeTemp( $originalName, $srcPath );
357
358         /**
359          * Remove a temporary file or mark it for garbage collection
360          * @param string $virtualUrl The virtual URL returned by storeTemp
361          * @return boolean True on success, false on failure
362          * STUB
363          */
364         function freeTemp( $virtualUrl ) {
365                 return true;
366         }
367
368         /**
369          * Copy or move a file either from the local filesystem or from an mwrepo://
370          * virtual URL, into this repository at the specified destination location.
371          *
372          * Returns a FileRepoStatus object. On success, the value contains "new" or
373          * "archived", to indicate whether the file was new with that name.
374          *
375          * @param string $srcPath The source path or URL
376          * @param string $dstRel The destination relative path
377          * @param string $archiveRel The relative path where the existing file is to
378          *        be archived, if there is one. Relative to the public zone root.
379          * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
380          *        that the source file should be deleted if possible
381          */
382         function publish( $srcPath, $dstRel, $archiveRel, $flags = 0 ) {
383                 $status = $this->publishBatch( array( array( $srcPath, $dstRel, $archiveRel ) ), $flags );
384                 if ( $status->successCount == 0 ) {
385                         $status->ok = false;
386                 }
387                 if ( isset( $status->value[0] ) ) {
388                         $status->value = $status->value[0];
389                 } else {
390                         $status->value = false;
391                 }
392                 return $status;
393         }
394
395         /**
396          * Publish a batch of files
397          * @param array $triplets (source,dest,archive) triplets as per publish()
398          * @param integer $flags Bitfield, may be FileRepo::DELETE_SOURCE to indicate
399          *        that the source files should be deleted if possible
400          */
401         abstract function publishBatch( $triplets, $flags = 0 );
402
403         /**
404          * Move a group of files to the deletion archive.
405          *
406          * If no valid deletion archive is configured, this may either delete the
407          * file or throw an exception, depending on the preference of the repository.
408          *
409          * The overwrite policy is determined by the repository -- currently FSRepo
410          * assumes a naming scheme in the deleted zone based on content hash, as
411          * opposed to the public zone which is assumed to be unique.
412          *
413          * @param array $sourceDestPairs Array of source/destination pairs. Each element
414          *        is a two-element array containing the source file path relative to the
415          *        public root in the first element, and the archive file path relative
416          *        to the deleted zone root in the second element.
417          * @return FileRepoStatus
418          */
419         abstract function deleteBatch( $sourceDestPairs );
420
421         /**
422          * Move a file to the deletion archive.
423          * If no valid deletion archive exists, this may either delete the file
424          * or throw an exception, depending on the preference of the repository
425          * @param mixed $srcRel Relative path for the file to be deleted
426          * @param mixed $archiveRel Relative path for the archive location.
427          *        Relative to a private archive directory.
428          * @return WikiError object (wikitext-formatted), or true for success
429          */
430         function delete( $srcRel, $archiveRel ) {
431                 return $this->deleteBatch( array( array( $srcRel, $archiveRel ) ) );
432         }
433
434         /**
435          * Get properties of a file with a given virtual URL
436          * The virtual URL must refer to this repo
437          * Properties should ultimately be obtained via File::getPropsFromPath()
438          */
439         abstract function getFileProps( $virtualUrl );
440
441         /**
442          * Call a callback function for every file in the repository
443          * May use either the database or the filesystem
444          * STUB
445          */
446         function enumFiles( $callback ) {
447                 throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) );
448         }
449
450         /**
451          * Determine if a relative path is valid, i.e. not blank or involving directory traveral
452          */
453         function validateFilename( $filename ) {
454                 if ( strval( $filename ) == '' ) {
455                         return false;
456                 }
457                 if ( wfIsWindows() ) {
458                         $filename = strtr( $filename, '\\', '/' );
459                 }
460                 /**
461                  * Use the same traversal protection as Title::secureAndSplit()
462                  */
463                 if ( strpos( $filename, '.' ) !== false &&
464                      ( $filename === '.' || $filename === '..' ||
465                        strpos( $filename, './' ) === 0  ||
466                        strpos( $filename, '../' ) === 0 ||
467                        strpos( $filename, '/./' ) !== false ||
468                        strpos( $filename, '/../' ) !== false ) )
469                 {
470                         return false;
471                 } else {
472                         return true;
473                 }
474         }
475
476         /**#@+
477          * Path disclosure protection functions
478          */
479         function paranoidClean( $param ) { return '[hidden]'; }
480         function passThrough( $param ) { return $param; }
481
482         /**
483          * Get a callback function to use for cleaning error message parameters
484          */
485         function getErrorCleanupFunction() {
486                 switch ( $this->pathDisclosureProtection ) {
487                         case 'none':
488                                 $callback = array( $this, 'passThrough' );
489                                 break;
490                         default: // 'paranoid'
491                                 $callback = array( $this, 'paranoidClean' );
492                 }
493                 return $callback;
494         }
495         /**#@-*/
496
497         /**
498          * Create a new fatal error
499          */
500         function newFatal( $message /*, parameters...*/ ) {
501                 $params = func_get_args();
502                 array_unshift( $params, $this );
503                 return call_user_func_array( array( 'FileRepoStatus', 'newFatal' ), $params );
504         }
505
506         /**
507          * Create a new good result
508          */
509         function newGood( $value = null ) {
510                 return FileRepoStatus::newGood( $this, $value );
511         }
512
513         /**
514          * Delete files in the deleted directory if they are not referenced in the filearchive table
515          * STUB
516          */
517         function cleanupDeletedBatch( $storageKeys ) {}
518
519         /**
520          * Checks if there is a redirect named as $title. If there is, return the
521          * title object. If not, return false.
522          * STUB
523          *
524          * @param Title $title Title of image
525          */
526         function checkRedirect( $title ) {
527                 return false;
528         }
529
530         /**
531          * Invalidates image redirect cache related to that image
532          *
533          * @param Title $title Title of image
534          */     
535         function invalidateImageRedirect( $title ) {
536                 global $wgMemc;
537                 $memcKey = $this->getMemcKey( "image_redirect:" . md5( $title->getPrefixedDBkey() ) );
538                 $wgMemc->delete( $memcKey );
539         }
540         
541         function findBySha1( $hash ) {
542                 return array();
543         }
544         
545         /**
546          * Get the human-readable name of the repo. 
547          * @return string
548          */
549         public function getDisplayName() {
550                 // We don't name our own repo, return nothing
551                 if ( $this->name == 'local' ) {
552                         return null;
553                 }
554                 $repoName = wfMsg( 'shared-repo-name-' . $this->name );
555                 if ( !wfEmptyMsg( 'shared-repo-name-' . $this->name, $repoName ) ) {
556                         return $repoName;
557                 }
558                 return wfMsg( 'shared-repo' ); 
559         }
560         
561         function getSlaveDB() {
562                 return wfGetDB( DB_SLAVE );
563         }
564
565         function getMasterDB() {
566                 return wfGetDB( DB_MASTER );
567         }
568         
569         function getMemcKey( $key ) {
570                 return wfWikiID( $this->getSlaveDB() ) . ":{$key}";
571         }
572 }