]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/FileStore.php
MediaWiki 1.11.0
[autoinstallsdev/mediawiki.git] / includes / FileStore.php
1 <?php
2
3 /**
4  * @todo document (needs one-sentence top-level class description).
5  */
6 class FileStore {
7         const DELETE_ORIGINAL = 1;
8         
9         /**
10          * Fetch the FileStore object for a given storage group
11          */
12         static function get( $group ) {
13                 global $wgFileStore;
14                 
15                 if( isset( $wgFileStore[$group] ) ) {
16                         $info = $wgFileStore[$group];
17                         return new FileStore( $group,
18                                 $info['directory'],
19                                 $info['url'],
20                                 intval( $info['hash'] ) );
21                 } else {
22                         return null;
23                 }
24         }
25         
26         private function __construct( $group, $directory, $path, $hash ) {
27                 $this->mGroup = $group;
28                 $this->mDirectory = $directory;
29                 $this->mPath = $path;
30                 $this->mHashLevel = $hash;
31         }
32         
33         /**
34          * Acquire a lock; use when performing write operations on a store.
35          * This is attached to your master database connection, so if you
36          * suffer an uncaught error the lock will be released when the
37          * connection is closed.
38          *
39          * @todo Probably only works on MySQL. Abstract to the Database class?
40          */
41         static function lock() {
42                 global $wgDBtype;
43                 if ($wgDBtype != 'mysql')
44                         return true;
45                 $dbw = wfGetDB( DB_MASTER );
46                 $lockname = $dbw->addQuotes( FileStore::lockName() );
47                 $result = $dbw->query( "SELECT GET_LOCK($lockname, 5) AS lockstatus", __METHOD__ );
48                 $row = $dbw->fetchObject( $result );
49                 $dbw->freeResult( $result );
50                 
51                 if( $row->lockstatus == 1 ) {
52                         return true;
53                 } else {
54                         wfDebug( __METHOD__." failed to acquire lock\n" );
55                         return false;
56                 }
57         }
58         
59         /**
60          * Release the global file store lock.
61          */
62         static function unlock() {
63                 global $wgDBtype;
64                 if ($wgDBtype != 'mysql')
65                         return true;
66                 $dbw = wfGetDB( DB_MASTER );
67                 $lockname = $dbw->addQuotes( FileStore::lockName() );
68                 $result = $dbw->query( "SELECT RELEASE_LOCK($lockname)", __METHOD__ );
69                 $dbw->fetchObject( $result );
70                 $dbw->freeResult( $result );
71         }
72         
73         private static function lockName() {
74                 return 'MediaWiki.' . wfWikiID() . '.FileStore';
75         }
76         
77         /**
78          * Copy a file into the file store from elsewhere in the filesystem.
79          * Should be protected by FileStore::lock() to avoid race conditions.
80          *
81          * @param $key storage key string
82          * @param $flags
83          *  DELETE_ORIGINAL - remove the source file on transaction commit.
84          *
85          * @throws FSException if copy can't be completed
86          * @return FSTransaction
87          */
88         function insert( $key, $sourcePath, $flags=0 ) {
89                 $destPath = $this->filePath( $key );
90                 return $this->copyFile( $sourcePath, $destPath, $flags );
91         }
92         
93         /**
94          * Copy a file from the file store to elsewhere in the filesystem.
95          * Should be protected by FileStore::lock() to avoid race conditions.
96          *
97          * @param $key storage key string
98          * @param $flags
99          *  DELETE_ORIGINAL - remove the source file on transaction commit.
100          *
101          * @throws FSException if copy can't be completed
102          * @return FSTransaction on success
103          */
104         function export( $key, $destPath, $flags=0 ) {
105                 $sourcePath = $this->filePath( $key );
106                 return $this->copyFile( $sourcePath, $destPath, $flags );
107         }
108         
109         private function copyFile( $sourcePath, $destPath, $flags=0 ) {
110                 if( !file_exists( $sourcePath ) ) {
111                         // Abort! Abort!
112                         throw new FSException( "missing source file '$sourcePath'" );
113                 }
114                 
115                 $transaction = new FSTransaction();
116                 
117                 if( $flags & self::DELETE_ORIGINAL ) {
118                         $transaction->addCommit( FSTransaction::DELETE_FILE, $sourcePath );
119                 }
120                 
121                 if( file_exists( $destPath ) ) {
122                         // An identical file is already present; no need to copy.
123                 } else {
124                         if( !file_exists( dirname( $destPath ) ) ) {
125                                 wfSuppressWarnings();
126                                 $ok = mkdir( dirname( $destPath ), 0777, true );
127                                 wfRestoreWarnings();
128                                 
129                                 if( !$ok ) {
130                                         throw new FSException(
131                                                 "failed to create directory for '$destPath'" );
132                                 }
133                         }
134                         
135                         wfSuppressWarnings();
136                         $ok = copy( $sourcePath, $destPath );
137                         wfRestoreWarnings();
138                         
139                         if( $ok ) {
140                                 wfDebug( __METHOD__." copied '$sourcePath' to '$destPath'\n" );
141                                 $transaction->addRollback( FSTransaction::DELETE_FILE, $destPath );
142                         } else {
143                                 throw new FSException(
144                                         __METHOD__." failed to copy '$sourcePath' to '$destPath'" );
145                         }
146                 }
147                 
148                 return $transaction;
149         }
150         
151         /**
152          * Delete a file from the file store.
153          * Caller's responsibility to make sure it's not being used by another row.
154          *
155          * File is not actually removed until transaction commit.
156          * Should be protected by FileStore::lock() to avoid race conditions.
157          *
158          * @param $key storage key string
159          * @throws FSException if file can't be deleted
160          * @return FSTransaction
161          */
162         function delete( $key ) {
163                 $destPath = $this->filePath( $key );
164                 if( false === $destPath ) {
165                         throw new FSExcepton( "file store does not contain file '$key'" );
166                 } else {
167                         return FileStore::deleteFile( $destPath );
168                 }
169         }
170         
171         /**
172          * Delete a non-managed file on a transactional basis.
173          *
174          * File is not actually removed until transaction commit.
175          * Should be protected by FileStore::lock() to avoid race conditions.
176          *
177          * @param $path file to remove
178          * @throws FSException if file can't be deleted
179          * @return FSTransaction
180          *
181          * @todo Might be worth preliminary permissions check
182          */
183         static function deleteFile( $path ) {
184                 if( file_exists( $path ) ) {
185                         $transaction = new FSTransaction();
186                         $transaction->addCommit( FSTransaction::DELETE_FILE, $path );
187                         return $transaction;
188                 } else {
189                         throw new FSException( "cannot delete missing file '$path'" );
190                 }
191         }
192         
193         /**
194          * Stream a contained file directly to HTTP output.
195          * Will throw a 404 if file is missing; 400 if invalid key.
196          * @return true on success, false on failure
197          */
198         function stream( $key ) {
199                 $path = $this->filePath( $key );
200                 if( $path === false ) {
201                         wfHttpError( 400, "Bad request", "Invalid or badly-formed filename." );
202                         return false;
203                 }
204                 
205                 if( file_exists( $path ) ) {
206                         // Set the filename for more convenient save behavior from browsers
207                         // FIXME: Is this safe?
208                         header( 'Content-Disposition: inline; filename="' . $key . '"' );
209                         
210                         require_once 'StreamFile.php';
211                         wfStreamFile( $path );
212                 } else {
213                         return wfHttpError( 404, "Not found",
214                                 "The requested resource does not exist." );
215                 }
216         }
217         
218         /**
219          * Confirm that the given file key is valid.
220          * Note that a valid key may refer to a file that does not exist.
221          *
222          * Key should consist of a 31-digit base-36 SHA-1 hash and
223          * an optional alphanumeric extension, all lowercase.
224          * The whole must not exceed 64 characters.
225          *
226          * @param $key
227          * @return boolean
228          */
229         static function validKey( $key ) {
230                 return preg_match( '/^[0-9a-z]{31,32}(\.[0-9a-z]{1,31})?$/', $key );
231         }
232         
233         
234         /**
235          * Calculate file storage key from a file on disk.
236          * You must pass an extension to it, as some files may be calculated
237          * out of a temporary file etc.
238          *
239          * @param $path to file
240          * @param $extension
241          * @return string or false if could not open file or bad extension
242          */
243         static function calculateKey( $path, $extension ) {
244                 wfSuppressWarnings();
245                 $hash = sha1_file( $path );
246                 wfRestoreWarnings();
247                 if( $hash === false ) {
248                         wfDebug( __METHOD__.": couldn't hash file '$path'\n" );
249                         return false;
250                 }
251                 
252                 $base36 = wfBaseConvert( $hash, 16, 36, 31 );
253                 if( $extension == '' ) {
254                         $key = $base36;
255                 } else {
256                         $key = $base36 . '.' . $extension;
257                 }
258                 
259                 // Sanity check
260                 if( self::validKey( $key ) ) {
261                         return $key;
262                 } else {
263                         wfDebug( __METHOD__.": generated bad key '$key'\n" );
264                         return false;
265                 }
266         }
267         
268         /**
269          * Return filesystem path to the given file.
270          * Note that the file may or may not exist.
271          * @return string or false if an invalid key
272          */
273         function filePath( $key ) {
274                 if( self::validKey( $key ) ) {
275                         return $this->mDirectory . DIRECTORY_SEPARATOR .
276                                 $this->hashPath( $key, DIRECTORY_SEPARATOR );
277                 } else {
278                         return false;
279                 }
280         }
281         
282         /**
283          * Return URL path to the given file, if the store is public.
284          * @return string or false if not public
285          */
286         function urlPath( $key ) {
287                 if( $this->mUrl && self::validKey( $key ) ) {
288                         return $this->mUrl . '/' . $this->hashPath( $key, '/' );
289                 } else {
290                         return false;
291                 }
292         }
293         
294         private function hashPath( $key, $separator ) {
295                 $parts = array();
296                 for( $i = 0; $i < $this->mHashLevel; $i++ ) {
297                         $parts[] = $key{$i};
298                 }
299                 $parts[] = $key;
300                 return implode( $separator, $parts );
301         }
302 }
303
304 /**
305  * Wrapper for file store transaction stuff.
306  *
307  * FileStore methods may return one of these for undoable operations;
308  * you can then call its rollback() or commit() methods to perform
309  * final cleanup if dependent database work fails or succeeds.
310  */
311 class FSTransaction {
312         const DELETE_FILE = 1;
313         
314         /**
315          * Combine more items into a fancier transaction
316          */
317         function add( FSTransaction $transaction ) {
318                 $this->mOnCommit = array_merge(
319                         $this->mOnCommit, $transaction->mOnCommit );
320                 $this->mOnRollback = array_merge(
321                         $this->mOnRollback, $transaction->mOnRollback );
322         }
323         
324         /**
325          * Perform final actions for success.
326          * @return true if actions applied ok, false if errors
327          */
328         function commit() {
329                 return $this->apply( $this->mOnCommit );
330         }
331         
332         /**
333          * Perform final actions for failure.
334          * @return true if actions applied ok, false if errors
335          */
336         function rollback() {
337                 return $this->apply( $this->mOnRollback );
338         }
339         
340         // --- Private and friend functions below...
341         
342         function __construct() {
343                 $this->mOnCommit = array();
344                 $this->mOnRollback = array();
345         }
346         
347         function addCommit( $action, $path ) {
348                 $this->mOnCommit[] = array( $action, $path );
349         }
350         
351         function addRollback( $action, $path ) {
352                 $this->mOnRollback[] = array( $action, $path );
353         }
354         
355         private function apply( $actions ) {
356                 $result = true;
357                 foreach( $actions as $item ) {
358                         list( $action, $path ) = $item;
359                         if( $action == self::DELETE_FILE ) {
360                                 wfSuppressWarnings();
361                                 $ok = unlink( $path );
362                                 wfRestoreWarnings();
363                                 if( $ok )
364                                         wfDebug( __METHOD__.": deleting file '$path'\n" );
365                                 else
366                                         wfDebug( __METHOD__.": failed to delete file '$path'\n" );
367                                 $result = $result && $ok;
368                         }
369                 }
370                 return $result;
371         }
372 }
373
374 /**
375  * @addtogroup Exception
376  */
377 class FSException extends MWException { }
378
379