MediaWiki REL1_31
FSFileBackend.php
Go to the documentation of this file.
1<?php
23use Wikimedia\Timestamp\ConvertibleTimestamp;
24
43 protected $basePath;
44
46 protected $containerPaths = [];
47
49 protected $fileMode;
51 protected $dirMode;
52
54 protected $fileOwner;
55
57 protected $isWindows;
59 protected $currentUser;
60
62 protected $hadWarningErrors = [];
63
74 public function __construct( array $config ) {
75 parent::__construct( $config );
76
77 $this->isWindows = ( strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN' );
78 // Remove any possible trailing slash from directories
79 if ( isset( $config['basePath'] ) ) {
80 $this->basePath = rtrim( $config['basePath'], '/' ); // remove trailing slash
81 } else {
82 $this->basePath = null; // none; containers must have explicit paths
83 }
84
85 if ( isset( $config['containerPaths'] ) ) {
86 $this->containerPaths = (array)$config['containerPaths'];
87 foreach ( $this->containerPaths as &$path ) {
88 $path = rtrim( $path, '/' ); // remove trailing slash
89 }
90 }
91
92 $this->fileMode = isset( $config['fileMode'] ) ? $config['fileMode'] : 0644;
93 $this->dirMode = isset( $config['directoryMode'] ) ? $config['directoryMode'] : 0777;
94 if ( isset( $config['fileOwner'] ) && function_exists( 'posix_getuid' ) ) {
95 $this->fileOwner = $config['fileOwner'];
96 // cache this, assuming it doesn't change
97 $this->currentUser = posix_getpwuid( posix_getuid() )['name'];
98 }
99 }
100
101 public function getFeatures() {
102 if ( $this->isWindows && version_compare( PHP_VERSION, '7.1', 'lt' ) ) {
103 // PHP before 7.1 used 8-bit code page for filesystem paths on Windows;
104 // See http://php.net/manual/en/migration71.windows-support.php
105 return 0;
106 } else {
108 }
109 }
110
111 protected function resolveContainerPath( $container, $relStoragePath ) {
112 // Check that container has a root directory
113 if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
114 // Check for sane relative paths (assume the base paths are OK)
115 if ( $this->isLegalRelPath( $relStoragePath ) ) {
116 return $relStoragePath;
117 }
118 }
119
120 return null;
121 }
122
129 protected function isLegalRelPath( $path ) {
130 // Check for file names longer than 255 chars
131 if ( preg_match( '![^/]{256}!', $path ) ) { // ext3/NTFS
132 return false;
133 }
134 if ( $this->isWindows ) { // NTFS
135 return !preg_match( '![:*?"<>|]!', $path );
136 } else {
137 return true;
138 }
139 }
140
149 protected function containerFSRoot( $shortCont, $fullCont ) {
150 if ( isset( $this->containerPaths[$shortCont] ) ) {
151 return $this->containerPaths[$shortCont];
152 } elseif ( isset( $this->basePath ) ) {
153 return "{$this->basePath}/{$fullCont}";
154 }
155
156 return null; // no container base path defined
157 }
158
165 protected function resolveToFSPath( $storagePath ) {
166 list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
167 if ( $relPath === null ) {
168 return null; // invalid
169 }
170 list( , $shortCont, ) = FileBackend::splitStoragePath( $storagePath );
171 $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
172 if ( $relPath != '' ) {
173 $fsPath .= "/{$relPath}";
174 }
175
176 return $fsPath;
177 }
178
179 public function isPathUsableInternal( $storagePath ) {
180 $fsPath = $this->resolveToFSPath( $storagePath );
181 if ( $fsPath === null ) {
182 return false; // invalid
183 }
184 $parentDir = dirname( $fsPath );
185
186 if ( file_exists( $fsPath ) ) {
187 $ok = is_file( $fsPath ) && is_writable( $fsPath );
188 } else {
189 $ok = is_dir( $parentDir ) && is_writable( $parentDir );
190 }
191
192 if ( $this->fileOwner !== null && $this->currentUser !== $this->fileOwner ) {
193 $ok = false;
194 trigger_error( __METHOD__ . ": PHP process owner is not '{$this->fileOwner}'." );
195 }
196
197 return $ok;
198 }
199
200 protected function doCreateInternal( array $params ) {
201 $status = $this->newStatus();
202
203 $dest = $this->resolveToFSPath( $params['dst'] );
204 if ( $dest === null ) {
205 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
206
207 return $status;
208 }
209
210 if ( !empty( $params['async'] ) ) { // deferred
211 $tempFile = TempFSFile::factory( 'create_', 'tmp', $this->tmpDirectory );
212 if ( !$tempFile ) {
213 $status->fatal( 'backend-fail-create', $params['dst'] );
214
215 return $status;
216 }
217 $this->trapWarnings();
218 $bytes = file_put_contents( $tempFile->getPath(), $params['content'] );
219 $this->untrapWarnings();
220 if ( $bytes === false ) {
221 $status->fatal( 'backend-fail-create', $params['dst'] );
222
223 return $status;
224 }
225 $cmd = implode( ' ', [
226 $this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
227 escapeshellarg( $this->cleanPathSlashes( $tempFile->getPath() ) ),
228 escapeshellarg( $this->cleanPathSlashes( $dest ) )
229 ] );
230 $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
231 if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
232 $status->fatal( 'backend-fail-create', $params['dst'] );
233 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
234 }
235 };
236 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
237 $tempFile->bind( $status->value );
238 } else { // immediate write
239 $this->trapWarnings();
240 $bytes = file_put_contents( $dest, $params['content'] );
241 $this->untrapWarnings();
242 if ( $bytes === false ) {
243 $status->fatal( 'backend-fail-create', $params['dst'] );
244
245 return $status;
246 }
247 $this->chmod( $dest );
248 }
249
250 return $status;
251 }
252
253 protected function doStoreInternal( array $params ) {
254 $status = $this->newStatus();
255
256 $dest = $this->resolveToFSPath( $params['dst'] );
257 if ( $dest === null ) {
258 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
259
260 return $status;
261 }
262
263 if ( !empty( $params['async'] ) ) { // deferred
264 $cmd = implode( ' ', [
265 $this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
266 escapeshellarg( $this->cleanPathSlashes( $params['src'] ) ),
267 escapeshellarg( $this->cleanPathSlashes( $dest ) )
268 ] );
269 $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
270 if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
271 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
272 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
273 }
274 };
275 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
276 } else { // immediate write
277 $this->trapWarnings();
278 $ok = copy( $params['src'], $dest );
279 $this->untrapWarnings();
280 // In some cases (at least over NFS), copy() returns true when it fails
281 if ( !$ok || ( filesize( $params['src'] ) !== filesize( $dest ) ) ) {
282 if ( $ok ) { // PHP bug
283 unlink( $dest ); // remove broken file
284 trigger_error( __METHOD__ . ": copy() failed but returned true." );
285 }
286 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
287
288 return $status;
289 }
290 $this->chmod( $dest );
291 }
292
293 return $status;
294 }
295
296 protected function doCopyInternal( array $params ) {
297 $status = $this->newStatus();
298
299 $source = $this->resolveToFSPath( $params['src'] );
300 if ( $source === null ) {
301 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
302
303 return $status;
304 }
305
306 $dest = $this->resolveToFSPath( $params['dst'] );
307 if ( $dest === null ) {
308 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
309
310 return $status;
311 }
312
313 if ( !is_file( $source ) ) {
314 if ( empty( $params['ignoreMissingSource'] ) ) {
315 $status->fatal( 'backend-fail-copy', $params['src'] );
316 }
317
318 return $status; // do nothing; either OK or bad status
319 }
320
321 if ( !empty( $params['async'] ) ) { // deferred
322 $cmd = implode( ' ', [
323 $this->isWindows ? 'COPY /B /Y' : 'cp', // (binary, overwrite)
324 escapeshellarg( $this->cleanPathSlashes( $source ) ),
325 escapeshellarg( $this->cleanPathSlashes( $dest ) )
326 ] );
327 $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
328 if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
329 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
330 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
331 }
332 };
333 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd, $dest );
334 } else { // immediate write
335 $this->trapWarnings();
336 $ok = ( $source === $dest ) ? true : copy( $source, $dest );
337 $this->untrapWarnings();
338 // In some cases (at least over NFS), copy() returns true when it fails
339 if ( !$ok || ( filesize( $source ) !== filesize( $dest ) ) ) {
340 if ( $ok ) { // PHP bug
341 $this->trapWarnings();
342 unlink( $dest ); // remove broken file
343 $this->untrapWarnings();
344 trigger_error( __METHOD__ . ": copy() failed but returned true." );
345 }
346 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
347
348 return $status;
349 }
350 $this->chmod( $dest );
351 }
352
353 return $status;
354 }
355
356 protected function doMoveInternal( array $params ) {
357 $status = $this->newStatus();
358
359 $source = $this->resolveToFSPath( $params['src'] );
360 if ( $source === null ) {
361 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
362
363 return $status;
364 }
365
366 $dest = $this->resolveToFSPath( $params['dst'] );
367 if ( $dest === null ) {
368 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
369
370 return $status;
371 }
372
373 if ( !is_file( $source ) ) {
374 if ( empty( $params['ignoreMissingSource'] ) ) {
375 $status->fatal( 'backend-fail-move', $params['src'] );
376 }
377
378 return $status; // do nothing; either OK or bad status
379 }
380
381 if ( !empty( $params['async'] ) ) { // deferred
382 $cmd = implode( ' ', [
383 $this->isWindows ? 'MOVE /Y' : 'mv', // (overwrite)
384 escapeshellarg( $this->cleanPathSlashes( $source ) ),
385 escapeshellarg( $this->cleanPathSlashes( $dest ) )
386 ] );
387 $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
388 if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
389 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
390 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
391 }
392 };
393 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
394 } else { // immediate write
395 $this->trapWarnings();
396 $ok = ( $source === $dest ) ? true : rename( $source, $dest );
397 $this->untrapWarnings();
398 clearstatcache(); // file no longer at source
399 if ( !$ok ) {
400 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
401
402 return $status;
403 }
404 }
405
406 return $status;
407 }
408
409 protected function doDeleteInternal( array $params ) {
410 $status = $this->newStatus();
411
412 $source = $this->resolveToFSPath( $params['src'] );
413 if ( $source === null ) {
414 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
415
416 return $status;
417 }
418
419 if ( !is_file( $source ) ) {
420 if ( empty( $params['ignoreMissingSource'] ) ) {
421 $status->fatal( 'backend-fail-delete', $params['src'] );
422 }
423
424 return $status; // do nothing; either OK or bad status
425 }
426
427 if ( !empty( $params['async'] ) ) { // deferred
428 $cmd = implode( ' ', [
429 $this->isWindows ? 'DEL' : 'unlink',
430 escapeshellarg( $this->cleanPathSlashes( $source ) )
431 ] );
432 $handler = function ( $errors, StatusValue $status, array $params, $cmd ) {
433 if ( $errors !== '' && !( $this->isWindows && $errors[0] === " " ) ) {
434 $status->fatal( 'backend-fail-delete', $params['src'] );
435 trigger_error( "$cmd\n$errors", E_USER_WARNING ); // command output
436 }
437 };
438 $status->value = new FSFileOpHandle( $this, $params, $handler, $cmd );
439 } else { // immediate write
440 $this->trapWarnings();
441 $ok = unlink( $source );
442 $this->untrapWarnings();
443 if ( !$ok ) {
444 $status->fatal( 'backend-fail-delete', $params['src'] );
445
446 return $status;
447 }
448 }
449
450 return $status;
451 }
452
459 protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
460 $status = $this->newStatus();
461 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
462 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
463 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
464 $existed = is_dir( $dir ); // already there?
465 // Create the directory and its parents as needed...
466 $this->trapWarnings();
467 if ( !$existed && !mkdir( $dir, $this->dirMode, true ) && !is_dir( $dir ) ) {
468 $this->logger->error( __METHOD__ . ": cannot create directory $dir" );
469 $status->fatal( 'directorycreateerror', $params['dir'] ); // fails on races
470 } elseif ( !is_writable( $dir ) ) {
471 $this->logger->error( __METHOD__ . ": directory $dir is read-only" );
472 $status->fatal( 'directoryreadonlyerror', $params['dir'] );
473 } elseif ( !is_readable( $dir ) ) {
474 $this->logger->error( __METHOD__ . ": directory $dir is not readable" );
475 $status->fatal( 'directorynotreadableerror', $params['dir'] );
476 }
477 $this->untrapWarnings();
478 // Respect any 'noAccess' or 'noListing' flags...
479 if ( is_dir( $dir ) && !$existed ) {
480 $status->merge( $this->doSecureInternal( $fullCont, $dirRel, $params ) );
481 }
482
483 return $status;
484 }
485
486 protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
487 $status = $this->newStatus();
488 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
489 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
490 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
491 // Seed new directories with a blank index.html, to prevent crawling...
492 if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
493 $this->trapWarnings();
494 $bytes = file_put_contents( "{$dir}/index.html", $this->indexHtmlPrivate() );
495 $this->untrapWarnings();
496 if ( $bytes === false ) {
497 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
498 }
499 }
500 // Add a .htaccess file to the root of the container...
501 if ( !empty( $params['noAccess'] ) && !file_exists( "{$contRoot}/.htaccess" ) ) {
502 $this->trapWarnings();
503 $bytes = file_put_contents( "{$contRoot}/.htaccess", $this->htaccessPrivate() );
504 $this->untrapWarnings();
505 if ( $bytes === false ) {
506 $storeDir = "mwstore://{$this->name}/{$shortCont}";
507 $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
508 }
509 }
510
511 return $status;
512 }
513
514 protected function doPublishInternal( $fullCont, $dirRel, array $params ) {
515 $status = $this->newStatus();
516 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
517 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
518 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
519 // Unseed new directories with a blank index.html, to allow crawling...
520 if ( !empty( $params['listing'] ) && is_file( "{$dir}/index.html" ) ) {
521 $exists = ( file_get_contents( "{$dir}/index.html" ) === $this->indexHtmlPrivate() );
522 $this->trapWarnings();
523 if ( $exists && !unlink( "{$dir}/index.html" ) ) { // reverse secure()
524 $status->fatal( 'backend-fail-delete', $params['dir'] . '/index.html' );
525 }
526 $this->untrapWarnings();
527 }
528 // Remove the .htaccess file from the root of the container...
529 if ( !empty( $params['access'] ) && is_file( "{$contRoot}/.htaccess" ) ) {
530 $exists = ( file_get_contents( "{$contRoot}/.htaccess" ) === $this->htaccessPrivate() );
531 $this->trapWarnings();
532 if ( $exists && !unlink( "{$contRoot}/.htaccess" ) ) { // reverse secure()
533 $storeDir = "mwstore://{$this->name}/{$shortCont}";
534 $status->fatal( 'backend-fail-delete', "{$storeDir}/.htaccess" );
535 }
536 $this->untrapWarnings();
537 }
538
539 return $status;
540 }
541
542 protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
543 $status = $this->newStatus();
544 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
545 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
546 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
547 $this->trapWarnings();
548 if ( is_dir( $dir ) ) {
549 rmdir( $dir ); // remove directory if empty
550 }
551 $this->untrapWarnings();
552
553 return $status;
554 }
555
556 protected function doGetFileStat( array $params ) {
557 $source = $this->resolveToFSPath( $params['src'] );
558 if ( $source === null ) {
559 return false; // invalid storage path
560 }
561
562 $this->trapWarnings(); // don't trust 'false' if there were errors
563 $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
564 $hadError = $this->untrapWarnings();
565
566 if ( $stat ) {
567 $ct = new ConvertibleTimestamp( $stat['mtime'] );
568
569 return [
570 'mtime' => $ct->getTimestamp( TS_MW ),
571 'size' => $stat['size']
572 ];
573 } elseif ( !$hadError ) {
574 return false; // file does not exist
575 } else {
576 return null; // failure
577 }
578 }
579
580 protected function doClearCache( array $paths = null ) {
581 clearstatcache(); // clear the PHP file stat cache
582 }
583
584 protected function doDirectoryExists( $fullCont, $dirRel, array $params ) {
585 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
586 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
587 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
588
589 $this->trapWarnings(); // don't trust 'false' if there were errors
590 $exists = is_dir( $dir );
591 $hadError = $this->untrapWarnings();
592
593 return $hadError ? null : $exists;
594 }
595
603 public function getDirectoryListInternal( $fullCont, $dirRel, array $params ) {
604 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
605 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
606 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
607 $exists = is_dir( $dir );
608 if ( !$exists ) {
609 $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
610
611 return []; // nothing under this dir
612 } elseif ( !is_readable( $dir ) ) {
613 $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
614
615 return null; // bad permissions?
616 }
617
618 return new FSFileBackendDirList( $dir, $params );
619 }
620
628 public function getFileListInternal( $fullCont, $dirRel, array $params ) {
629 list( , $shortCont, ) = FileBackend::splitStoragePath( $params['dir'] );
630 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
631 $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
632 $exists = is_dir( $dir );
633 if ( !$exists ) {
634 $this->logger->warning( __METHOD__ . "() given directory does not exist: '$dir'\n" );
635
636 return []; // nothing under this dir
637 } elseif ( !is_readable( $dir ) ) {
638 $this->logger->warning( __METHOD__ . "() given directory is unreadable: '$dir'\n" );
639
640 return null; // bad permissions?
641 }
642
643 return new FSFileBackendFileList( $dir, $params );
644 }
645
646 protected function doGetLocalReferenceMulti( array $params ) {
647 $fsFiles = []; // (path => FSFile)
648
649 foreach ( $params['srcs'] as $src ) {
650 $source = $this->resolveToFSPath( $src );
651 if ( $source === null || !is_file( $source ) ) {
652 $fsFiles[$src] = null; // invalid path or file does not exist
653 } else {
654 $fsFiles[$src] = new FSFile( $source );
655 }
656 }
657
658 return $fsFiles;
659 }
660
661 protected function doGetLocalCopyMulti( array $params ) {
662 $tmpFiles = []; // (path => TempFSFile)
663
664 foreach ( $params['srcs'] as $src ) {
665 $source = $this->resolveToFSPath( $src );
666 if ( $source === null ) {
667 $tmpFiles[$src] = null; // invalid path
668 } else {
669 // Create a new temporary file with the same extension...
671 $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
672 if ( !$tmpFile ) {
673 $tmpFiles[$src] = null;
674 } else {
675 $tmpPath = $tmpFile->getPath();
676 // Copy the source file over the temp file
677 $this->trapWarnings();
678 $ok = copy( $source, $tmpPath );
679 $this->untrapWarnings();
680 if ( !$ok ) {
681 $tmpFiles[$src] = null;
682 } else {
683 $this->chmod( $tmpPath );
684 $tmpFiles[$src] = $tmpFile;
685 }
686 }
687 }
688 }
689
690 return $tmpFiles;
691 }
692
693 protected function directoriesAreVirtual() {
694 return false;
695 }
696
702 protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
703 $statuses = [];
704
705 $pipes = [];
706 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
707 $pipes[$index] = popen( "{$fileOpHandle->cmd} 2>&1", 'r' );
708 }
709
710 $errs = [];
711 foreach ( $pipes as $index => $pipe ) {
712 // Result will be empty on success in *NIX. On Windows,
713 // it may be something like " 1 file(s) [copied|moved].".
714 $errs[$index] = stream_get_contents( $pipe );
715 fclose( $pipe );
716 }
717
718 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
719 $status = $this->newStatus();
720 $function = $fileOpHandle->call;
721 $function( $errs[$index], $status, $fileOpHandle->params, $fileOpHandle->cmd );
722 $statuses[$index] = $status;
723 if ( $status->isOK() && $fileOpHandle->chmodPath ) {
724 $this->chmod( $fileOpHandle->chmodPath );
725 }
726 }
727
728 clearstatcache(); // files changed
729 return $statuses;
730 }
731
738 protected function chmod( $path ) {
739 $this->trapWarnings();
740 $ok = chmod( $path, $this->fileMode );
741 $this->untrapWarnings();
742
743 return $ok;
744 }
745
751 protected function indexHtmlPrivate() {
752 return '';
753 }
754
760 protected function htaccessPrivate() {
761 return "Deny from all\n";
762 }
763
770 protected function cleanPathSlashes( $path ) {
771 return $this->isWindows ? strtr( $path, '/', '\\' ) : $path;
772 }
773
777 protected function trapWarnings() {
778 $this->hadWarningErrors[] = false; // push to stack
779 set_error_handler( [ $this, 'handleWarning' ], E_WARNING );
780 }
781
787 protected function untrapWarnings() {
788 restore_error_handler(); // restore previous handler
789 return array_pop( $this->hadWarningErrors ); // pop from stack
790 }
791
798 public function handleWarning( $errno, $errstr ) {
799 $this->logger->error( $errstr ); // more detailed error logging
800 $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
801
802 return true; // suppress from PHP handler
803 }
804}
805
810 public $cmd; // string; shell command
811 public $chmodPath; // string; file to chmod
812
820 public function __construct(
822 ) {
823 $this->backend = $backend;
824 $this->params = $params;
825 $this->call = $call;
826 $this->cmd = $cmd;
827 $this->chmodPath = $chmodPath;
828 }
829}
830
838abstract class FSFileBackendList implements Iterator {
840 protected $iter;
841
843 protected $suffixStart;
844
846 protected $pos = 0;
847
849 protected $params = [];
850
855 public function __construct( $dir, array $params ) {
856 $path = realpath( $dir ); // normalize
857 if ( $path === false ) {
858 $path = $dir;
859 }
860 $this->suffixStart = strlen( $path ) + 1; // size of "path/to/dir/"
861 $this->params = $params;
862
863 try {
864 $this->iter = $this->initIterator( $path );
865 } catch ( UnexpectedValueException $e ) {
866 $this->iter = null; // bad permissions? deleted?
867 }
868 }
869
876 protected function initIterator( $dir ) {
877 if ( !empty( $this->params['topOnly'] ) ) { // non-recursive
878 # Get an iterator that will get direct sub-nodes
879 return new DirectoryIterator( $dir );
880 } else { // recursive
881 # Get an iterator that will return leaf nodes (non-directories)
882 # RecursiveDirectoryIterator extends FilesystemIterator.
883 # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
884 $flags = FilesystemIterator::CURRENT_AS_SELF | FilesystemIterator::SKIP_DOTS;
885
886 return new RecursiveIteratorIterator(
887 new RecursiveDirectoryIterator( $dir, $flags ),
888 RecursiveIteratorIterator::CHILD_FIRST // include dirs
889 );
890 }
891 }
892
897 public function key() {
898 return $this->pos;
899 }
900
905 public function current() {
906 return $this->getRelPath( $this->iter->current()->getPathname() );
907 }
908
913 public function next() {
914 try {
915 $this->iter->next();
916 $this->filterViaNext();
917 } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
918 throw new FileBackendError( "File iterator gave UnexpectedValueException." );
919 }
920 ++$this->pos;
921 }
922
927 public function rewind() {
928 $this->pos = 0;
929 try {
930 $this->iter->rewind();
931 $this->filterViaNext();
932 } catch ( UnexpectedValueException $e ) { // bad permissions? deleted?
933 throw new FileBackendError( "File iterator gave UnexpectedValueException." );
934 }
935 }
936
941 public function valid() {
942 return $this->iter && $this->iter->valid();
943 }
944
948 protected function filterViaNext() {
949 }
950
958 protected function getRelPath( $dir ) {
959 $path = realpath( $dir );
960 if ( $path === false ) {
961 $path = $dir;
962 }
963
964 return strtr( substr( $path, $this->suffixStart ), '\\', '/' );
965 }
966}
967
969 protected function filterViaNext() {
970 while ( $this->iter->valid() ) {
971 if ( $this->iter->current()->isDot() || !$this->iter->current()->isDir() ) {
972 $this->iter->next(); // skip non-directories and dot files
973 } else {
974 break;
975 }
976 }
977 }
978}
979
981 protected function filterViaNext() {
982 while ( $this->iter->valid() ) {
983 if ( !$this->iter->current()->isFile() ) {
984 $this->iter->next(); // skip non-files and dot files
985 } else {
986 break;
987 }
988 }
989 }
990}
and give any other recipients of the Program a copy of this License along with the Program You may charge a fee for the physical act of transferring a copy
Definition COPYING.txt:87
filterViaNext()
Filter out items by advancing to the next ones.
filterViaNext()
Filter out items by advancing to the next ones.
Wrapper around RecursiveDirectoryIterator/DirectoryIterator that catches exception or does any custom...
getRelPath( $dir)
Return only the relative path and normalize slashes to FileBackend-style.
__construct( $dir, array $params)
filterViaNext()
Filter out items by advancing to the next ones.
initIterator( $dir)
Return an appropriate iterator object to wrap.
Class for a file system (FS) based file backend.
doCopyInternal(array $params)
string $basePath
Directory holding the container directories.
doDirectoryExists( $fullCont, $dirRel, array $params)
__construct(array $config)
getDirectoryListInternal( $fullCont, $dirRel, array $params)
doDeleteInternal(array $params)
indexHtmlPrivate()
Return the text of an index.html file to hide directory listings.
chmod( $path)
Chmod a file, suppressing the warnings.
doExecuteOpHandlesInternal(array $fileOpHandles)
resolveToFSPath( $storagePath)
Get the absolute file system path for a storage path.
doGetFileStat(array $params)
htaccessPrivate()
Return the text of a .htaccess file to make a directory private.
doGetLocalCopyMulti(array $params)
isLegalRelPath( $path)
Sanity check a relative file system path for validity.
untrapWarnings()
Stop listening for E_WARNING errors and return true if any happened.
string $fileOwner
Required OS username to own files.
cleanPathSlashes( $path)
Clean up directory separators for the given OS.
doPublishInternal( $fullCont, $dirRel, array $params)
resolveContainerPath( $container, $relStoragePath)
Resolve a relative storage path, checking if it's allowed by the backend.
array $containerPaths
Map of container names to root paths for custom container paths.
doCleanInternal( $fullCont, $dirRel, array $params)
directoriesAreVirtual()
Is this a key/value store where directories are just virtual? Virtual directories exists in so much a...
doSecureInternal( $fullCont, $dirRel, array $params)
doMoveInternal(array $params)
array $hadWarningErrors
doClearCache(array $paths=null)
Clears any additional stat caches for storage paths.
doStoreInternal(array $params)
doPrepareInternal( $fullCont, $dirRel, array $params)
getFeatures()
Get the a bitfield of extra features supported by the backend medium.
containerFSRoot( $shortCont, $fullCont)
Given the short (unresolved) and full (resolved) name of a container, return the file system path of ...
int $fileMode
File permission mode.
isPathUsableInternal( $storagePath)
Check if a file can be created or changed at a given storage path.
doCreateInternal(array $params)
trapWarnings()
Listen for E_WARNING errors and track whether any happen.
doGetLocalReferenceMulti(array $params)
string $currentUser
OS username running this script.
handleWarning( $errno, $errstr)
int $dirMode
File permission mode.
getFileListInternal( $fullCont, $dirRel, array $params)
__construct(FSFileBackend $backend, array $params, $call, $cmd, $chmodPath=null)
Class representing a non-directory file on the file system.
Definition FSFile.php:29
File backend exception for checked exceptions (e.g.
FileBackendStore helper class for performing asynchronous file operations.
Base class for all backends using particular storage medium.
resolveStoragePathReal( $storagePath)
Like resolveStoragePath() except null values are returned if the container is sharded and the shard c...
const ATTR_UNICODE_PATHS
static splitStoragePath( $storagePath)
Split a storage path into a backend name, a container name, and a relative file path.
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
newStatus()
Yields the result of the status wrapper callback on either:
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static factory( $prefix, $extension='', $tmpDirectory=null)
Make a new temporary file on the file system.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
the array() calling protocol came about after MediaWiki 1.4rc1.
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1051
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition hooks.txt:903
returning false will NOT prevent logging $e
Definition hooks.txt:2176
$source
if(!is_readable( $file)) $ext
Definition router.php:55
$params