Merged FileBackend branch. Manually avoiding merging the many prop-only changes SVN...
[lhc/web/wiklou.git] / includes / filerepo / backend / FSFileBackend.php
1 <?php
2 /**
3 * @file
4 * @ingroup FileBackend
5 * @author Aaron Schulz
6 */
7
8 /**
9 * Class for a file system based file backend.
10 * Status messages should avoid mentioning the internal FS paths.
11 * Likewise, error suppression should be used to avoid path disclosure.
12 *
13 * @ingroup FileBackend
14 */
15 class FSFileBackend extends FileBackend {
16 /** @var Array Map of container names to paths */
17 protected $containerPaths = array();
18 protected $fileMode; // file permission mode
19
20 /**
21 * @see FileBackend::__construct()
22 * Additional $config params include:
23 * containerPaths : Map of container names to absolute file system paths
24 * fileMode : Octal UNIX file permissions to use on files stored
25 */
26 function __construct( array $config ) {
27 parent::__construct( $config );
28 $this->containerPaths = (array)$config['containerPaths'];
29 foreach ( $this->containerPaths as $container => &$path ) {
30 if ( substr( $path, -1 ) === '/' ) {
31 $path = substr( $path, 0, -1 ); // remove trailing slash
32 }
33 }
34 $this->fileMode = isset( $config['fileMode'] )
35 ? $config['fileMode']
36 : 0644;
37 }
38
39 /**
40 * @see FileBackend::resolveContainerPath()
41 */
42 protected function resolveContainerPath( $container, $relStoragePath ) {
43 // Get absolute path given the container base dir
44 if ( isset( $this->containerPaths[$container] ) ) {
45 return $this->containerPaths[$container] . "/{$relStoragePath}";
46 }
47 return null;
48 }
49
50 /**
51 * @see FileBackend::doStore()
52 */
53 protected function doStore( array $params ) {
54 $status = Status::newGood();
55
56 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
57 if ( $dest === null ) {
58 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
59 return $status;
60 }
61 if ( file_exists( $dest ) ) {
62 if ( !empty( $params['overwriteDest'] ) ) {
63 wfSuppressWarnings();
64 $ok = unlink( $dest );
65 wfRestoreWarnings();
66 if ( !$ok ) {
67 $status->fatal( 'backend-fail-delete', $params['dst'] );
68 return $status;
69 }
70 } else {
71 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
72 return $status;
73 }
74 } else {
75 if ( !wfMkdirParents( dirname( $dest ) ) ) {
76 $status->fatal( 'directorycreateerror', $params['dst'] );
77 return $status;
78 }
79 }
80
81 wfSuppressWarnings();
82 $ok = copy( $params['src'], $dest );
83 wfRestoreWarnings();
84 if ( !$ok ) {
85 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
86 return $status;
87 }
88
89 $this->chmod( $dest );
90
91 return $status;
92 }
93
94 /**
95 * @see FileBackend::doCopy()
96 */
97 protected function doCopy( array $params ) {
98 $status = Status::newGood();
99
100 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
101 if ( $source === null ) {
102 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
103 return $status;
104 }
105
106 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
107 if ( $dest === null ) {
108 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
109 return $status;
110 }
111
112 if ( file_exists( $dest ) ) {
113 if ( !empty( $params['overwriteDest'] ) ) {
114 wfSuppressWarnings();
115 $ok = unlink( $dest );
116 wfRestoreWarnings();
117 if ( !$ok ) {
118 $status->fatal( 'backend-fail-delete', $params['dst'] );
119 return $status;
120 }
121 } else {
122 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
123 return $status;
124 }
125 } else {
126 if ( !wfMkdirParents( dirname( $dest ) ) ) {
127 $status->fatal( 'directorycreateerror', $params['dst'] );
128 return $status;
129 }
130 }
131
132 wfSuppressWarnings();
133 $ok = copy( $source, $dest );
134 wfRestoreWarnings();
135 if ( !$ok ) {
136 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
137 return $status;
138 }
139
140 $this->chmod( $dest );
141
142 return $status;
143 }
144
145 /**
146 * @see FileBackend::doMove()
147 */
148 protected function doMove( array $params ) {
149 $status = Status::newGood();
150
151 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
152 if ( $source === null ) {
153 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
154 return $status;
155 }
156 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
157 if ( $dest === null ) {
158 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
159 return $status;
160 }
161
162 if ( file_exists( $dest ) ) {
163 if ( !empty( $params['overwriteDest'] ) ) {
164 // Windows does not support moving over existing files
165 if ( wfIsWindows() ) {
166 wfSuppressWarnings();
167 $ok = unlink( $dest );
168 wfRestoreWarnings();
169 if ( !$ok ) {
170 $status->fatal( 'backend-fail-delete', $params['dst'] );
171 return $status;
172 }
173 }
174 } else {
175 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
176 return $status;
177 }
178 } else {
179 if ( !wfMkdirParents( dirname( $dest ) ) ) {
180 $status->fatal( 'directorycreateerror', $params['dst'] );
181 return $status;
182 }
183 }
184
185 wfSuppressWarnings();
186 $ok = rename( $source, $dest );
187 clearstatcache(); // file no longer at source
188 wfRestoreWarnings();
189 if ( !$ok ) {
190 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
191 return $status;
192 }
193
194 return $status;
195 }
196
197 /**
198 * @see FileBackend::doDelete()
199 */
200 protected function doDelete( array $params ) {
201 $status = Status::newGood();
202
203 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
204 if ( $source === null ) {
205 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
206 return $status;
207 }
208
209 if ( !is_file( $source ) ) {
210 if ( empty( $params['ignoreMissingSource'] ) ) {
211 $status->fatal( 'backend-fail-delete', $params['src'] );
212 }
213 return $status; // do nothing; either OK or bad status
214 }
215
216 wfSuppressWarnings();
217 $ok = unlink( $source );
218 wfRestoreWarnings();
219 if ( !$ok ) {
220 $status->fatal( 'backend-fail-delete', $params['src'] );
221 return $status;
222 }
223
224 return $status;
225 }
226
227 /**
228 * @see FileBackend::doConcatenate()
229 */
230 protected function doConcatenate( array $params ) {
231 $status = Status::newGood();
232
233 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
234 if ( $dest === null ) {
235 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
236 return $status;
237 }
238
239 // Check if the destination file exists and we can't handle that
240 $destExists = file_exists( $dest );
241 if ( $destExists && empty( $params['overwriteDest'] ) ) {
242 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
243 return $status;
244 }
245
246 // Create a new temporary file...
247 wfSuppressWarnings();
248 $tmpPath = tempnam( wfTempDir(), 'concatenate' );
249 wfRestoreWarnings();
250 if ( $tmpPath === false ) {
251 $status->fatal( 'backend-fail-createtemp' );
252 return $status;
253 }
254
255 // Build up that file using the source chunks (in order)...
256 wfSuppressWarnings();
257 $tmpHandle = fopen( $tmpPath, 'a' );
258 wfRestoreWarnings();
259 if ( $tmpHandle === false ) {
260 $status->fatal( 'backend-fail-opentemp', $tmpPath );
261 return $status;
262 }
263 foreach ( $params['srcs'] as $virtualSource ) {
264 list( $c, $source ) = $this->resolveStoragePath( $virtualSource );
265 if ( $source === null ) {
266 fclose( $tmpHandle );
267 $status->fatal( 'backend-fail-invalidpath', $virtualSource );
268 return $status;
269 }
270 // Load chunk into memory (it should be a small file)
271 $sourceHandle = fopen( $source, 'r' );
272 if ( $sourceHandle === false ) {
273 fclose( $tmpHandle );
274 $status->fatal( 'backend-fail-read', $virtualSource );
275 return $status;
276 }
277 // Append chunk to file (pass chunk size to avoid magic quotes)
278 if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
279 fclose( $sourceHandle );
280 fclose( $tmpHandle );
281 $status->fatal( 'backend-fail-writetemp', $tmpPath );
282 return $status;
283 }
284 fclose( $sourceHandle );
285 }
286 wfSuppressWarnings();
287 if ( !fclose( $tmpHandle ) ) {
288 $status->fatal( 'backend-fail-closetemp', $tmpPath );
289 return $status;
290 }
291 wfRestoreWarnings();
292
293 // Handle overwrite behavior of file destination if applicable.
294 // Note that we already checked if no overwrite params were set above.
295 if ( $destExists ) {
296 // Windows does not support moving over existing files
297 if ( wfIsWindows() ) {
298 wfSuppressWarnings();
299 $ok = unlink( $dest );
300 wfRestoreWarnings();
301 if ( !$ok ) {
302 $status->fatal( 'backend-fail-delete', $params['dst'] );
303 return $status;
304 }
305 }
306 } else {
307 // Make sure destination directory exists
308 if ( !wfMkdirParents( dirname( $dest ) ) ) {
309 $status->fatal( 'directorycreateerror', $params['dst'] );
310 return $status;
311 }
312 }
313
314 // Rename the temporary file to the destination path
315 wfSuppressWarnings();
316 $ok = rename( $tmpPath, $dest );
317 wfRestoreWarnings();
318 if ( !$ok ) {
319 $status->fatal( 'backend-fail-move', $tmpPath, $params['dst'] );
320 return $status;
321 }
322
323 $this->chmod( $dest );
324
325 return $status;
326 }
327
328 /**
329 * @see FileBackend::doCreate()
330 */
331 protected function doCreate( array $params ) {
332 $status = Status::newGood();
333
334 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
335 if ( $dest === null ) {
336 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
337 return $status;
338 }
339
340 if ( file_exists( $dest ) ) {
341 if ( !empty( $params['overwriteDest'] ) ) {
342 wfSuppressWarnings();
343 $ok = unlink( $dest );
344 wfRestoreWarnings();
345 if ( !$ok ) {
346 $status->fatal( 'backend-fail-delete', $params['dst'] );
347 return $status;
348 }
349 } else {
350 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
351 return $status;
352 }
353 } else {
354 if ( !wfMkdirParents( dirname( $dest ) ) ) {
355 $status->fatal( 'directorycreateerror', $params['dst'] );
356 return $status;
357 }
358 }
359
360 wfSuppressWarnings();
361 $ok = file_put_contents( $dest, $params['content'] );
362 wfRestoreWarnings();
363 if ( !$ok ) {
364 $status->fatal( 'backend-fail-create', $params['dst'] );
365 return $status;
366 }
367
368 $this->chmod( $dest );
369
370 return $status;
371 }
372
373 /**
374 * @see FileBackend::prepare()
375 */
376 function prepare( array $params ) {
377 $status = Status::newGood();
378 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
379 if ( $dir === null ) {
380 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
381 return $status; // invalid storage path
382 }
383 if ( !wfMkdirParents( $dir ) ) {
384 $status->fatal( 'directorycreateerror', $params['dir'] );
385 return $status;
386 } elseif ( !is_writable( $dir ) ) {
387 $status->fatal( 'directoryreadonlyerror', $params['dir'] );
388 return $status;
389 } elseif ( !is_readable( $dir ) ) {
390 $status->fatal( 'directorynotreadableerror', $params['dir'] );
391 return $status;
392 }
393 return $status;
394 }
395
396 /**
397 * @see FileBackend::secure()
398 */
399 function secure( array $params ) {
400 $status = Status::newGood();
401 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
402 if ( $dir === null ) {
403 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
404 return $status; // invalid storage path
405 }
406 if ( !wfMkdirParents( $dir ) ) {
407 $status->fatal( 'directorycreateerror', $params['dir'] );
408 return $status;
409 }
410 // Add a .htaccess file to the root of the deleted zone
411 if ( !empty( $params['noAccess'] ) && !file_exists( "{$dir}/.htaccess" ) ) {
412 wfSuppressWarnings();
413 $ok = file_put_contents( "{$dir}/.htaccess", "Deny from all\n" );
414 wfRestoreWarnings();
415 if ( !$ok ) {
416 $status->fatal( 'backend-fail-create', $params['dir'] . '/.htaccess' );
417 return $status;
418 }
419 }
420 // Seed new directories with a blank index.html, to prevent crawling
421 if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
422 wfSuppressWarnings();
423 $ok = file_put_contents( "{$dir}/index.html", '' );
424 wfRestoreWarnings();
425 if ( !$ok ) {
426 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
427 return $status;
428 }
429 }
430 return $status;
431 }
432
433 /**
434 * @see FileBackend::clean()
435 */
436 function clean( array $params ) {
437 $status = Status::newGood();
438 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
439 if ( $dir === null ) {
440 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
441 return $status; // invalid storage path
442 }
443 wfSuppressWarnings();
444 if ( is_dir( $dir ) ) {
445 rmdir( $dir ); // remove directory if empty
446 }
447 wfRestoreWarnings();
448 return $status;
449 }
450
451 /**
452 * @see FileBackend::fileExists()
453 */
454 function fileExists( array $params ) {
455 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
456 if ( $source === null ) {
457 return false; // invalid storage path
458 }
459 wfSuppressWarnings();
460 $exists = is_file( $source );
461 wfRestoreWarnings();
462 return $exists;
463 }
464
465 /**
466 * @see FileBackend::getFileTimestamp()
467 */
468 function getFileTimestamp( array $params ) {
469 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
470 if ( $source === null ) {
471 return false; // invalid storage path
472 }
473 $fsFile = new FSFile( $source );
474 return $fsFile->getTimestamp();
475 }
476
477 /**
478 * @see FileBackend::getFileList()
479 */
480 function getFileList( array $params ) {
481 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
482 if ( $dir === null ) { // invalid storage path
483 return null;
484 }
485 wfSuppressWarnings();
486 $exists = is_dir( $dir );
487 wfRestoreWarnings();
488 if ( !$exists ) {
489 return array(); // nothing under this dir
490 }
491 wfSuppressWarnings();
492 $readable = is_readable( $dir );
493 wfRestoreWarnings();
494 if ( !$readable ) {
495 return null; // bad permissions?
496 }
497 return new FSFileIterator( $dir );
498 }
499
500 /**
501 * @see FileBackend::getLocalReference()
502 */
503 function getLocalReference( array $params ) {
504 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
505 if ( $source === null ) {
506 return null;
507 }
508 return new FSFile( $source );
509 }
510
511 /**
512 * @see FileBackend::getLocalCopy()
513 */
514 function getLocalCopy( array $params ) {
515 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
516 if ( $source === null ) {
517 return null;
518 }
519
520 // Get source file extension
521 $i = strrpos( $source, '.' );
522 $ext = strtolower( $i ? substr( $source, $i + 1 ) : '' );
523 // Create a new temporary file...
524 $tmpFile = TempFSFile::factory( wfBaseName( $source ) . '_', $ext );
525 if ( !$tmpFile ) {
526 return null;
527 }
528 $tmpPath = $tmpFile->getPath();
529
530 // Copy the source file over the temp file
531 wfSuppressWarnings();
532 $ok = copy( $source, $tmpPath );
533 wfRestoreWarnings();
534 if ( !$ok ) {
535 return null;
536 }
537
538 $this->chmod( $tmpPath );
539
540 return $tmpFile;
541 }
542
543 /**
544 * Chmod a file, suppressing the warnings
545 *
546 * @param $path string Absolute file system path
547 * @return bool Success
548 */
549 protected function chmod( $path ) {
550 wfSuppressWarnings();
551 $ok = chmod( $path, $this->fileMode );
552 wfRestoreWarnings();
553
554 return $ok;
555 }
556 }
557
558 /**
559 * Wrapper around RecursiveDirectoryIterator that catches
560 * exception or does any custom behavoir that we may want.
561 *
562 * @ingroup FileBackend
563 */
564 class FSFileIterator implements Iterator {
565 /** @var RecursiveIteratorIterator */
566 protected $iter;
567
568 /**
569 * Get an FSFileIterator from a file system directory
570 *
571 * @param $dir string
572 */
573 public function __construct( $dir ) {
574 try {
575 $this->iter = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) );
576 } catch ( UnexpectedValueException $e ) {
577 $this->iter = null; // bad permissions? deleted?
578 }
579 }
580
581 public function current() {
582 return $this->iter->current();
583 }
584
585 public function key() {
586 return $this->iter->key();
587 }
588
589 public function next() {
590 try {
591 $this->iter->next();
592 } catch ( UnexpectedValueException $e ) {
593 $this->iter = null;
594 }
595 }
596
597 public function rewind() {
598 try {
599 $this->iter->rewind();
600 } catch ( UnexpectedValueException $e ) {
601 $this->iter = null;
602 }
603 }
604
605 public function valid() {
606 return $this->iter && $this->iter->valid();
607 }
608 }