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