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