Fix undefined $dirRoot
[lhc/web/wiklou.git] / includes / filerepo / backend / FSFileBackend.php
1 g<?php
2 /**
3 * @file
4 * @ingroup FileBackend
5 * @author Aaron Schulz
6 */
7
8 /**
9 * Class for a file system (FS) based file backend.
10 *
11 * All "containers" each map to a directory under the backend's base directory.
12 * For backwards-compatibility, some container paths can be set to custom paths.
13 * The wiki ID will not be used in any custom paths, so this should be avoided.
14 *
15 * Having directories with thousands of files will diminish performance.
16 * Sharding can be accomplished by using FileRepo-style hash paths.
17 *
18 * Status messages should avoid mentioning the internal FS paths.
19 * Likewise, error suppression should be used to avoid path disclosure.
20 *
21 * @ingroup FileBackend
22 * @since 1.19
23 */
24 class FSFileBackend extends FileBackend {
25 protected $basePath; // string; directory holding the container directories
26 /** @var Array Map of container names to root paths */
27 protected $containerPaths = array(); // for custom container paths
28 protected $fileMode; // integer; file permission mode
29
30 /**
31 * @see FileBackend::__construct()
32 * Additional $config params include:
33 * basePath : File system directory that holds containers.
34 * containerPaths : Map of container names to custom file system directories.
35 * This should only be used for backwards-compatibility.
36 * fileMode : Octal UNIX file permissions to use on files stored.
37 */
38 public function __construct( array $config ) {
39 parent::__construct( $config );
40 if ( isset( $config['basePath'] ) ) {
41 if ( substr( $this->basePath, -1 ) === '/' ) {
42 $this->basePath = substr( $this->basePath, 0, -1 ); // remove trailing slash
43 }
44 } else {
45 $this->basePath = null; // none; containers must have explicit paths
46 }
47 $this->containerPaths = (array)$config['containerPaths'];
48 foreach ( $this->containerPaths as &$path ) {
49 if ( substr( $path, -1 ) === '/' ) {
50 $path = substr( $path, 0, -1 ); // remove trailing slash
51 }
52 }
53 $this->fileMode = isset( $config['fileMode'] )
54 ? $config['fileMode']
55 : 0644;
56 }
57
58 /**
59 * @see FileBackend::resolveContainerPath()
60 */
61 protected function resolveContainerPath( $container, $relStoragePath ) {
62 if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
63 return $relStoragePath; // container has a root directory
64 }
65 return null;
66 }
67
68 /**
69 * Given the short (unresolved) and full (resolved) name of
70 * a container, return the file system path of the container.
71 *
72 * @param $shortCont string
73 * @param $fullCont string
74 * @return string|null
75 */
76 protected function containerFSRoot( $shortCont, $fullCont ) {
77 if ( isset( $this->containerPaths[$shortCont] ) ) {
78 return $this->containerPaths[$shortCont];
79 } elseif ( isset( $this->basePath ) ) {
80 return "{$this->basePath}/{$fullCont}";
81 }
82 return null; // no container base path defined
83 }
84
85 /**
86 * Get the absolute file system path for a storage path
87 *
88 * @param $storagePath string Storage path
89 * @return string|null
90 */
91 protected function resolveToFSPath( $storagePath ) {
92 list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
93 if ( $relPath === null ) {
94 return null; // invalid
95 }
96 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath );
97 $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
98 if ( $relPath != '' ) {
99 $fsPath .= "/{$relPath}";
100 }
101 return $fsPath;
102 }
103
104 /**
105 * @see FileBackend::isPathUsableInternal()
106 */
107 public function isPathUsableInternal( $storagePath ) {
108 $fsPath = $this->resolveToFSPath( $storagePath );
109 if ( $fsPath === null ) {
110 return false; // invalid
111 }
112 $parentDir = dirname( $fsPath );
113
114 wfSuppressWarnings();
115 if ( file_exists( $fsPath ) ) {
116 $ok = is_file( $fsPath ) && is_writable( $fsPath );
117 } else {
118 $ok = is_dir( $parentDir ) && is_writable( $parentDir );
119 }
120 wfRestoreWarnings();
121
122 return $ok;
123 }
124
125 /**
126 * @see FileBackend::doStoreInternal()
127 */
128 protected function doStoreInternal( array $params ) {
129 $status = Status::newGood();
130
131 $dest = $this->resolveToFSPath( $params['dst'] );
132 if ( $dest === null ) {
133 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
134 return $status;
135 }
136
137 if ( file_exists( $dest ) ) {
138 if ( !empty( $params['overwrite'] ) ) {
139 wfSuppressWarnings();
140 $ok = unlink( $dest );
141 wfRestoreWarnings();
142 if ( !$ok ) {
143 $status->fatal( 'backend-fail-delete', $params['dst'] );
144 return $status;
145 }
146 } else {
147 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
148 return $status;
149 }
150 }
151
152 wfSuppressWarnings();
153 $ok = copy( $params['src'], $dest );
154 wfRestoreWarnings();
155 if ( !$ok ) {
156 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
157 return $status;
158 }
159
160 $this->chmod( $dest );
161
162 return $status;
163 }
164
165 /**
166 * @see FileBackend::doCopyInternal()
167 */
168 protected function doCopyInternal( array $params ) {
169 $status = Status::newGood();
170
171 $source = $this->resolveToFSPath( $params['src'] );
172 if ( $source === null ) {
173 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
174 return $status;
175 }
176
177 $dest = $this->resolveToFSPath( $params['dst'] );
178 if ( $dest === null ) {
179 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
180 return $status;
181 }
182
183 if ( file_exists( $dest ) ) {
184 if ( !empty( $params['overwrite'] ) ) {
185 wfSuppressWarnings();
186 $ok = unlink( $dest );
187 wfRestoreWarnings();
188 if ( !$ok ) {
189 $status->fatal( 'backend-fail-delete', $params['dst'] );
190 return $status;
191 }
192 } else {
193 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
194 return $status;
195 }
196 }
197
198 wfSuppressWarnings();
199 $ok = copy( $source, $dest );
200 wfRestoreWarnings();
201 if ( !$ok ) {
202 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
203 return $status;
204 }
205
206 $this->chmod( $dest );
207
208 return $status;
209 }
210
211 /**
212 * @see FileBackend::doMoveInternal()
213 */
214 protected function doMoveInternal( array $params ) {
215 $status = Status::newGood();
216
217 $source = $this->resolveToFSPath( $params['src'] );
218 if ( $source === null ) {
219 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
220 return $status;
221 }
222
223 $dest = $this->resolveToFSPath( $params['dst'] );
224 if ( $dest === null ) {
225 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
226 return $status;
227 }
228
229 if ( file_exists( $dest ) ) {
230 if ( !empty( $params['overwrite'] ) ) {
231 // Windows does not support moving over existing files
232 if ( wfIsWindows() ) {
233 wfSuppressWarnings();
234 $ok = unlink( $dest );
235 wfRestoreWarnings();
236 if ( !$ok ) {
237 $status->fatal( 'backend-fail-delete', $params['dst'] );
238 return $status;
239 }
240 }
241 } else {
242 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
243 return $status;
244 }
245 }
246
247 wfSuppressWarnings();
248 $ok = rename( $source, $dest );
249 clearstatcache(); // file no longer at source
250 wfRestoreWarnings();
251 if ( !$ok ) {
252 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
253 return $status;
254 }
255
256 return $status;
257 }
258
259 /**
260 * @see FileBackend::doDeleteInternal()
261 */
262 protected function doDeleteInternal( array $params ) {
263 $status = Status::newGood();
264
265 $source = $this->resolveToFSPath( $params['src'] );
266 if ( $source === null ) {
267 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
268 return $status;
269 }
270
271 if ( !is_file( $source ) ) {
272 if ( empty( $params['ignoreMissingSource'] ) ) {
273 $status->fatal( 'backend-fail-delete', $params['src'] );
274 }
275 return $status; // do nothing; either OK or bad status
276 }
277
278 wfSuppressWarnings();
279 $ok = unlink( $source );
280 wfRestoreWarnings();
281 if ( !$ok ) {
282 $status->fatal( 'backend-fail-delete', $params['src'] );
283 return $status;
284 }
285
286 return $status;
287 }
288
289 /**
290 * @see FileBackend::doCreateInternal()
291 */
292 protected function doCreateInternal( array $params ) {
293 $status = Status::newGood();
294
295 $dest = $this->resolveToFSPath( $params['dst'] );
296 if ( $dest === null ) {
297 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
298 return $status;
299 }
300
301 if ( file_exists( $dest ) ) {
302 if ( !empty( $params['overwrite'] ) ) {
303 wfSuppressWarnings();
304 $ok = unlink( $dest );
305 wfRestoreWarnings();
306 if ( !$ok ) {
307 $status->fatal( 'backend-fail-delete', $params['dst'] );
308 return $status;
309 }
310 } else {
311 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
312 return $status;
313 }
314 }
315
316 wfSuppressWarnings();
317 $ok = file_put_contents( $dest, $params['content'] );
318 wfRestoreWarnings();
319 if ( !$ok ) {
320 $status->fatal( 'backend-fail-create', $params['dst'] );
321 return $status;
322 }
323
324 $this->chmod( $dest );
325
326 return $status;
327 }
328
329 /**
330 * @see FileBackend::doPrepareInternal()
331 */
332 protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
333 $status = Status::newGood();
334 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
335 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
336 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
337 if ( !wfMkdirParents( $dir ) ) { // make directory and its parents
338 $status->fatal( 'directorycreateerror', $params['dir'] );
339 } elseif ( !is_writable( $dir ) ) {
340 $status->fatal( 'directoryreadonlyerror', $params['dir'] );
341 } elseif ( !is_readable( $dir ) ) {
342 $status->fatal( 'directorynotreadableerror', $params['dir'] );
343 }
344 return $status;
345 }
346
347 /**
348 * @see FileBackend::doSecureInternal()
349 */
350 protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
351 $status = Status::newGood();
352 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
353 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
354 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
355 // Seed new directories with a blank index.html, to prevent crawling...
356 if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
357 wfSuppressWarnings();
358 $ok = file_put_contents( "{$dir}/index.html", '' );
359 wfRestoreWarnings();
360 if ( !$ok ) {
361 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
362 return $status;
363 }
364 }
365 // Add a .htaccess file to the root of the container...
366 if ( !empty( $params['noAccess'] ) ) {
367 if ( !file_exists( "{$contRoot}/.htaccess" ) ) {
368 wfSuppressWarnings();
369 $ok = file_put_contents( "{$contRoot}/.htaccess", "Deny from all\n" );
370 wfRestoreWarnings();
371 if ( !$ok ) {
372 $storeDir = "mwstore://{$this->name}/{$shortCont}";
373 $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
374 return $status;
375 }
376 }
377 }
378 return $status;
379 }
380
381 /**
382 * @see FileBackend::doCleanInternal()
383 */
384 protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
385 $status = Status::newGood();
386 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
387 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
388 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
389 wfSuppressWarnings();
390 if ( is_dir( $dir ) ) {
391 rmdir( $dir ); // remove directory if empty
392 }
393 wfRestoreWarnings();
394 return $status;
395 }
396
397 /**
398 * @see FileBackend::doFileExists()
399 */
400 protected function doGetFileStat( array $params ) {
401 $source = $this->resolveToFSPath( $params['src'] );
402 if ( $source === null ) {
403 return false; // invalid storage path
404 }
405
406 $this->trapWarnings();
407 $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
408 $hadError = $this->untrapWarnings();
409
410 if ( $stat ) {
411 return array(
412 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
413 'size' => $stat['size']
414 );
415 } elseif ( !$hadError ) {
416 return false; // file does not exist
417 } else {
418 return null; // failure
419 }
420 }
421
422 /**
423 * @see FileBackend::doClearCache()
424 */
425 protected function doClearCache( array $paths = null ) {
426 clearstatcache(); // clear the PHP file stat cache
427 }
428
429 /**
430 * @see FileBackend::getFileListInternal()
431 */
432 public function getFileListInternal( $fullCont, $dirRel, array $params ) {
433 list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
434 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
435 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
436 wfSuppressWarnings();
437 $exists = is_dir( $dir );
438 wfRestoreWarnings();
439 if ( !$exists ) {
440 return array(); // nothing under this dir
441 }
442 wfSuppressWarnings();
443 $readable = is_readable( $dir );
444 wfRestoreWarnings();
445 if ( !$readable ) {
446 return null; // bad permissions?
447 }
448 return new FSFileBackendFileList( $dir );
449 }
450
451 /**
452 * @see FileBackend::getLocalReference()
453 */
454 public function getLocalReference( array $params ) {
455 $source = $this->resolveToFSPath( $params['src'] );
456 if ( $source === null ) {
457 return null;
458 }
459 return new FSFile( $source );
460 }
461
462 /**
463 * @see FileBackend::getLocalCopy()
464 */
465 public function getLocalCopy( array $params ) {
466 $source = $this->resolveToFSPath( $params['src'] );
467 if ( $source === null ) {
468 return null;
469 }
470
471 // Create a new temporary file with the same extension...
472 $ext = FileBackend::extensionFromPath( $params['src'] );
473 $tmpFile = TempFSFile::factory( wfBaseName( $source ) . '_', $ext );
474 if ( !$tmpFile ) {
475 return null;
476 }
477 $tmpPath = $tmpFile->getPath();
478
479 // Copy the source file over the temp file
480 wfSuppressWarnings();
481 $ok = copy( $source, $tmpPath );
482 wfRestoreWarnings();
483 if ( !$ok ) {
484 return null;
485 }
486
487 $this->chmod( $tmpPath );
488
489 return $tmpFile;
490 }
491
492 /**
493 * Chmod a file, suppressing the warnings
494 *
495 * @param $path string Absolute file system path
496 * @return bool Success
497 */
498 protected function chmod( $path ) {
499 wfSuppressWarnings();
500 $ok = chmod( $path, $this->fileMode );
501 wfRestoreWarnings();
502
503 return $ok;
504 }
505
506 /**
507 * Suppress E_WARNING errors and track whether any happen
508 *
509 * @return void
510 */
511 protected function trapWarnings() {
512 $this->hadWarningErrors[] = false; // push to stack
513 set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
514 }
515
516 /**
517 * Unsuppress E_WARNING errors and return true if any happened
518 *
519 * @return bool
520 */
521 protected function untrapWarnings() {
522 restore_error_handler(); // restore previous handler
523 return array_pop( $this->hadWarningErrors ); // pop from stack
524 }
525
526 private function handleWarning() {
527 $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
528 return true; // suppress from PHP handler
529 }
530 }
531
532 /**
533 * Wrapper around RecursiveDirectoryIterator that catches
534 * exception or does any custom behavoir that we may want.
535 * Do not use this class from places outside FSFileBackend.
536 *
537 * @ingroup FileBackend
538 */
539 class FSFileBackendFileList implements Iterator {
540 /** @var RecursiveIteratorIterator */
541 protected $iter;
542 protected $suffixStart; // integer
543
544 /**
545 * @param $dir string file system directory
546 */
547 public function __construct( $dir ) {
548 $dir = realpath( $dir ); // normalize
549 $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
550 try {
551 $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS;
552 $this->iter = new RecursiveIteratorIterator(
553 new RecursiveDirectoryIterator( $dir, $flags ) );
554 } catch ( UnexpectedValueException $e ) {
555 $this->iter = null; // bad permissions? deleted?
556 }
557 }
558
559 public function current() {
560 // Return only the relative path and normalize slashes to FileBackend-style
561 // Make sure to use the realpath since the suffix is based upon that
562 return str_replace( '\\', '/',
563 substr( realpath( $this->iter->current() ), $this->suffixStart ) );
564 }
565
566 public function key() {
567 return $this->iter->key();
568 }
569
570 public function next() {
571 try {
572 $this->iter->next();
573 } catch ( UnexpectedValueException $e ) {
574 $this->iter = null;
575 }
576 }
577
578 public function rewind() {
579 try {
580 $this->iter->rewind();
581 } catch ( UnexpectedValueException $e ) {
582 $this->iter = null;
583 }
584 }
585
586 public function valid() {
587 return $this->iter && $this->iter->valid();
588 }
589 }