Merge "Adding output parameter to PageHistoryBeforeList hook"
[lhc/web/wiklou.git] / includes / filebackend / FileOp.php
1 <?php
2 /**
3 * Helper class for representing operations with transaction support.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup FileBackend
22 * @author Aaron Schulz
23 */
24
25 /**
26 * FileBackend helper class for representing operations.
27 * Do not use this class from places outside FileBackend.
28 *
29 * Methods called from FileOpBatch::attempt() should avoid throwing
30 * exceptions at all costs. FileOp objects should be lightweight in order
31 * to support large arrays in memory and serialization.
32 *
33 * @ingroup FileBackend
34 * @since 1.19
35 */
36 abstract class FileOp {
37 /** @var Array */
38 protected $params = array();
39 /** @var FileBackendStore */
40 protected $backend;
41
42 protected $state = self::STATE_NEW; // integer
43 protected $failed = false; // boolean
44 protected $async = false; // boolean
45 protected $batchId; // string
46
47 protected $doOperation = true; // boolean; operation is not a no-op
48 protected $sourceSha1; // string
49 protected $destSameAsSource; // boolean
50 protected $destExists; // boolean
51
52 /* Object life-cycle */
53 const STATE_NEW = 1;
54 const STATE_CHECKED = 2;
55 const STATE_ATTEMPTED = 3;
56
57 /**
58 * Build a new file operation transaction
59 *
60 * @param $backend FileBackendStore
61 * @param $params Array
62 * @throws MWException
63 */
64 final public function __construct( FileBackendStore $backend, array $params ) {
65 $this->backend = $backend;
66 list( $required, $optional ) = $this->allowedParams();
67 foreach ( $required as $name ) {
68 if ( isset( $params[$name] ) ) {
69 $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] );
70 } else {
71 throw new MWException( "File operation missing parameter '$name'." );
72 }
73 }
74 foreach ( $optional as $name ) {
75 if ( isset( $params[$name] ) ) {
76 $this->params[$name] = self::normalizeAnyStoragePaths( $params[$name] );
77 }
78 }
79 $this->params = $params;
80 }
81
82 /**
83 * Normalize $item or anything in $item that is a valid storage path
84 *
85 * @param $item string|array
86 * @return string|Array
87 */
88 protected function normalizeAnyStoragePaths( $item ) {
89 if ( is_array( $item ) ) {
90 $res = array();
91 foreach ( $item as $k => $v ) {
92 $k = self::normalizeIfValidStoragePath( $k );
93 $v = self::normalizeIfValidStoragePath( $v );
94 $res[$k] = $v;
95 }
96 return $res;
97 } else {
98 return self::normalizeIfValidStoragePath( $item );
99 }
100 }
101
102 /**
103 * Normalize a string if it is a valid storage path
104 *
105 * @param $path string
106 * @return string
107 */
108 protected static function normalizeIfValidStoragePath( $path ) {
109 if ( FileBackend::isStoragePath( $path ) ) {
110 $res = FileBackend::normalizeStoragePath( $path );
111 return ( $res !== null ) ? $res : $path;
112 }
113 return $path;
114 }
115
116 /**
117 * Set the batch UUID this operation belongs to
118 *
119 * @param $batchId string
120 * @return void
121 */
122 final public function setBatchId( $batchId ) {
123 $this->batchId = $batchId;
124 }
125
126 /**
127 * Get the value of the parameter with the given name
128 *
129 * @param $name string
130 * @return mixed Returns null if the parameter is not set
131 */
132 final public function getParam( $name ) {
133 return isset( $this->params[$name] ) ? $this->params[$name] : null;
134 }
135
136 /**
137 * Check if this operation failed precheck() or attempt()
138 *
139 * @return bool
140 */
141 final public function failed() {
142 return $this->failed;
143 }
144
145 /**
146 * Get a new empty predicates array for precheck()
147 *
148 * @return Array
149 */
150 final public static function newPredicates() {
151 return array( 'exists' => array(), 'sha1' => array() );
152 }
153
154 /**
155 * Get a new empty dependency tracking array for paths read/written to
156 *
157 * @return Array
158 */
159 final public static function newDependencies() {
160 return array( 'read' => array(), 'write' => array() );
161 }
162
163 /**
164 * Update a dependency tracking array to account for this operation
165 *
166 * @param array $deps Prior path reads/writes; format of FileOp::newPredicates()
167 * @return Array
168 */
169 final public function applyDependencies( array $deps ) {
170 $deps['read'] += array_fill_keys( $this->storagePathsRead(), 1 );
171 $deps['write'] += array_fill_keys( $this->storagePathsChanged(), 1 );
172 return $deps;
173 }
174
175 /**
176 * Check if this operation changes files listed in $paths
177 *
178 * @param array $paths Prior path reads/writes; format of FileOp::newPredicates()
179 * @return boolean
180 */
181 final public function dependsOn( array $deps ) {
182 foreach ( $this->storagePathsChanged() as $path ) {
183 if ( isset( $deps['read'][$path] ) || isset( $deps['write'][$path] ) ) {
184 return true; // "output" or "anti" dependency
185 }
186 }
187 foreach ( $this->storagePathsRead() as $path ) {
188 if ( isset( $deps['write'][$path] ) ) {
189 return true; // "flow" dependency
190 }
191 }
192 return false;
193 }
194
195 /**
196 * Get the file journal entries for this file operation
197 *
198 * @param array $oPredicates Pre-op info about files (format of FileOp::newPredicates)
199 * @param array $nPredicates Post-op info about files (format of FileOp::newPredicates)
200 * @return Array
201 */
202 final public function getJournalEntries( array $oPredicates, array $nPredicates ) {
203 if ( !$this->doOperation ) {
204 return array(); // this is a no-op
205 }
206 $nullEntries = array();
207 $updateEntries = array();
208 $deleteEntries = array();
209 $pathsUsed = array_merge( $this->storagePathsRead(), $this->storagePathsChanged() );
210 foreach ( array_unique( $pathsUsed ) as $path ) {
211 $nullEntries[] = array( // assertion for recovery
212 'op' => 'null',
213 'path' => $path,
214 'newSha1' => $this->fileSha1( $path, $oPredicates )
215 );
216 }
217 foreach ( $this->storagePathsChanged() as $path ) {
218 if ( $nPredicates['sha1'][$path] === false ) { // deleted
219 $deleteEntries[] = array(
220 'op' => 'delete',
221 'path' => $path,
222 'newSha1' => ''
223 );
224 } else { // created/updated
225 $updateEntries[] = array(
226 'op' => $this->fileExists( $path, $oPredicates ) ? 'update' : 'create',
227 'path' => $path,
228 'newSha1' => $nPredicates['sha1'][$path]
229 );
230 }
231 }
232 return array_merge( $nullEntries, $updateEntries, $deleteEntries );
233 }
234
235 /**
236 * Check preconditions of the operation without writing anything.
237 * This must update $predicates for each path that the op can change
238 * except when a failing status object is returned.
239 *
240 * @param $predicates Array
241 * @return Status
242 */
243 final public function precheck( array &$predicates ) {
244 if ( $this->state !== self::STATE_NEW ) {
245 return Status::newFatal( 'fileop-fail-state', self::STATE_NEW, $this->state );
246 }
247 $this->state = self::STATE_CHECKED;
248 $status = $this->doPrecheck( $predicates );
249 if ( !$status->isOK() ) {
250 $this->failed = true;
251 }
252 return $status;
253 }
254
255 /**
256 * @return Status
257 */
258 protected function doPrecheck( array &$predicates ) {
259 return Status::newGood();
260 }
261
262 /**
263 * Attempt the operation
264 *
265 * @return Status
266 */
267 final public function attempt() {
268 if ( $this->state !== self::STATE_CHECKED ) {
269 return Status::newFatal( 'fileop-fail-state', self::STATE_CHECKED, $this->state );
270 } elseif ( $this->failed ) { // failed precheck
271 return Status::newFatal( 'fileop-fail-attempt-precheck' );
272 }
273 $this->state = self::STATE_ATTEMPTED;
274 if ( $this->doOperation ) {
275 $status = $this->doAttempt();
276 if ( !$status->isOK() ) {
277 $this->failed = true;
278 $this->logFailure( 'attempt' );
279 }
280 } else { // no-op
281 $status = Status::newGood();
282 }
283 return $status;
284 }
285
286 /**
287 * @return Status
288 */
289 protected function doAttempt() {
290 return Status::newGood();
291 }
292
293 /**
294 * Attempt the operation in the background
295 *
296 * @return Status
297 */
298 final public function attemptAsync() {
299 $this->async = true;
300 $result = $this->attempt();
301 $this->async = false;
302 return $result;
303 }
304
305 /**
306 * Get the file operation parameters
307 *
308 * @return Array (required params list, optional params list)
309 */
310 protected function allowedParams() {
311 return array( array(), array() );
312 }
313
314 /**
315 * Adjust params to FileBackendStore internal file calls
316 *
317 * @param $params Array
318 * @return Array (required params list, optional params list)
319 */
320 protected function setFlags( array $params ) {
321 return array( 'async' => $this->async ) + $params;
322 }
323
324 /**
325 * Get a list of storage paths read from for this operation
326 *
327 * @return Array
328 */
329 public function storagePathsRead() {
330 return array();
331 }
332
333 /**
334 * Get a list of storage paths written to for this operation
335 *
336 * @return Array
337 */
338 public function storagePathsChanged() {
339 return array();
340 }
341
342 /**
343 * Check for errors with regards to the destination file already existing.
344 * Also set the destExists, destSameAsSource and sourceSha1 member variables.
345 * A bad status will be returned if there is no chance it can be overwritten.
346 *
347 * @param $predicates Array
348 * @return Status
349 */
350 protected function precheckDestExistence( array $predicates ) {
351 $status = Status::newGood();
352 // Get hash of source file/string and the destination file
353 $this->sourceSha1 = $this->getSourceSha1Base36(); // FS file or data string
354 if ( $this->sourceSha1 === null ) { // file in storage?
355 $this->sourceSha1 = $this->fileSha1( $this->params['src'], $predicates );
356 }
357 $this->destSameAsSource = false;
358 $this->destExists = $this->fileExists( $this->params['dst'], $predicates );
359 if ( $this->destExists ) {
360 if ( $this->getParam( 'overwrite' ) ) {
361 return $status; // OK
362 } elseif ( $this->getParam( 'overwriteSame' ) ) {
363 $dhash = $this->fileSha1( $this->params['dst'], $predicates );
364 // Check if hashes are valid and match each other...
365 if ( !strlen( $this->sourceSha1 ) || !strlen( $dhash ) ) {
366 $status->fatal( 'backend-fail-hashes' );
367 } elseif ( $this->sourceSha1 !== $dhash ) {
368 // Give an error if the files are not identical
369 $status->fatal( 'backend-fail-notsame', $this->params['dst'] );
370 } else {
371 $this->destSameAsSource = true; // OK
372 }
373 return $status; // do nothing; either OK or bad status
374 } else {
375 $status->fatal( 'backend-fail-alreadyexists', $this->params['dst'] );
376 return $status;
377 }
378 }
379 return $status;
380 }
381
382 /**
383 * precheckDestExistence() helper function to get the source file SHA-1.
384 * Subclasses should overwride this iff the source is not in storage.
385 *
386 * @return string|bool Returns false on failure
387 */
388 protected function getSourceSha1Base36() {
389 return null; // N/A
390 }
391
392 /**
393 * Check if a file will exist in storage when this operation is attempted
394 *
395 * @param string $source Storage path
396 * @param $predicates Array
397 * @return bool
398 */
399 final protected function fileExists( $source, array $predicates ) {
400 if ( isset( $predicates['exists'][$source] ) ) {
401 return $predicates['exists'][$source]; // previous op assures this
402 } else {
403 $params = array( 'src' => $source, 'latest' => true );
404 return $this->backend->fileExists( $params );
405 }
406 }
407
408 /**
409 * Get the SHA-1 of a file in storage when this operation is attempted
410 *
411 * @param string $source Storage path
412 * @param $predicates Array
413 * @return string|bool False on failure
414 */
415 final protected function fileSha1( $source, array $predicates ) {
416 if ( isset( $predicates['sha1'][$source] ) ) {
417 return $predicates['sha1'][$source]; // previous op assures this
418 } elseif ( isset( $predicates['exists'][$source] ) && !$predicates['exists'][$source] ) {
419 return false; // previous op assures this
420 } else {
421 $params = array( 'src' => $source, 'latest' => true );
422 return $this->backend->getFileSha1Base36( $params );
423 }
424 }
425
426 /**
427 * Get the backend this operation is for
428 *
429 * @return FileBackendStore
430 */
431 public function getBackend() {
432 return $this->backend;
433 }
434
435 /**
436 * Log a file operation failure and preserve any temp files
437 *
438 * @param $action string
439 * @return void
440 */
441 final public function logFailure( $action ) {
442 $params = $this->params;
443 $params['failedAction'] = $action;
444 try {
445 wfDebugLog( 'FileOperation', get_class( $this ) .
446 " failed (batch #{$this->batchId}): " . FormatJson::encode( $params ) );
447 } catch ( Exception $e ) {
448 // bad config? debug log error?
449 }
450 }
451 }
452
453 /**
454 * Create a file in the backend with the given content.
455 * Parameters for this operation are outlined in FileBackend::doOperations().
456 */
457 class CreateFileOp extends FileOp {
458 protected function allowedParams() {
459 return array( array( 'content', 'dst' ),
460 array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) );
461 }
462
463 protected function doPrecheck( array &$predicates ) {
464 $status = Status::newGood();
465 // Check if the source data is too big
466 if ( strlen( $this->getParam( 'content' ) ) > $this->backend->maxFileSizeInternal() ) {
467 $status->fatal( 'backend-fail-maxsize',
468 $this->params['dst'], $this->backend->maxFileSizeInternal() );
469 $status->fatal( 'backend-fail-create', $this->params['dst'] );
470 return $status;
471 // Check if a file can be placed/changed at the destination
472 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
473 $status->fatal( 'backend-fail-usable', $this->params['dst'] );
474 $status->fatal( 'backend-fail-create', $this->params['dst'] );
475 return $status;
476 }
477 // Check if destination file exists
478 $status->merge( $this->precheckDestExistence( $predicates ) );
479 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
480 if ( $status->isOK() ) {
481 // Update file existence predicates
482 $predicates['exists'][$this->params['dst']] = true;
483 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
484 }
485 return $status; // safe to call attempt()
486 }
487
488 /**
489 * @return Status
490 */
491 protected function doAttempt() {
492 if ( !$this->destSameAsSource ) {
493 // Create the file at the destination
494 return $this->backend->createInternal( $this->setFlags( $this->params ) );
495 }
496 return Status::newGood();
497 }
498
499 /**
500 * @return bool|String
501 */
502 protected function getSourceSha1Base36() {
503 return wfBaseConvert( sha1( $this->params['content'] ), 16, 36, 31 );
504 }
505
506 /**
507 * @return array
508 */
509 public function storagePathsChanged() {
510 return array( $this->params['dst'] );
511 }
512 }
513
514 /**
515 * Store a file into the backend from a file on the file system.
516 * Parameters for this operation are outlined in FileBackend::doOperations().
517 */
518 class StoreFileOp extends FileOp {
519 /**
520 * @return array
521 */
522 protected function allowedParams() {
523 return array( array( 'src', 'dst' ),
524 array( 'overwrite', 'overwriteSame', 'disposition', 'headers' ) );
525 }
526
527 /**
528 * @param $predicates array
529 * @return Status
530 */
531 protected function doPrecheck( array &$predicates ) {
532 $status = Status::newGood();
533 // Check if the source file exists on the file system
534 if ( !is_file( $this->params['src'] ) ) {
535 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
536 return $status;
537 // Check if the source file is too big
538 } elseif ( filesize( $this->params['src'] ) > $this->backend->maxFileSizeInternal() ) {
539 $status->fatal( 'backend-fail-maxsize',
540 $this->params['dst'], $this->backend->maxFileSizeInternal() );
541 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
542 return $status;
543 // Check if a file can be placed/changed at the destination
544 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
545 $status->fatal( 'backend-fail-usable', $this->params['dst'] );
546 $status->fatal( 'backend-fail-store', $this->params['src'], $this->params['dst'] );
547 return $status;
548 }
549 // Check if destination file exists
550 $status->merge( $this->precheckDestExistence( $predicates ) );
551 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
552 if ( $status->isOK() ) {
553 // Update file existence predicates
554 $predicates['exists'][$this->params['dst']] = true;
555 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
556 }
557 return $status; // safe to call attempt()
558 }
559
560 /**
561 * @return Status
562 */
563 protected function doAttempt() {
564 // Store the file at the destination
565 if ( !$this->destSameAsSource ) {
566 return $this->backend->storeInternal( $this->setFlags( $this->params ) );
567 }
568 return Status::newGood();
569 }
570
571 /**
572 * @return bool|string
573 */
574 protected function getSourceSha1Base36() {
575 wfSuppressWarnings();
576 $hash = sha1_file( $this->params['src'] );
577 wfRestoreWarnings();
578 if ( $hash !== false ) {
579 $hash = wfBaseConvert( $hash, 16, 36, 31 );
580 }
581 return $hash;
582 }
583
584 public function storagePathsChanged() {
585 return array( $this->params['dst'] );
586 }
587 }
588
589 /**
590 * Copy a file from one storage path to another in the backend.
591 * Parameters for this operation are outlined in FileBackend::doOperations().
592 */
593 class CopyFileOp extends FileOp {
594 /**
595 * @return array
596 */
597 protected function allowedParams() {
598 return array( array( 'src', 'dst' ),
599 array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) );
600 }
601
602 /**
603 * @param $predicates array
604 * @return Status
605 */
606 protected function doPrecheck( array &$predicates ) {
607 $status = Status::newGood();
608 // Check if the source file exists
609 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
610 if ( $this->getParam( 'ignoreMissingSource' ) ) {
611 $this->doOperation = false; // no-op
612 // Update file existence predicates (cache 404s)
613 $predicates['exists'][$this->params['src']] = false;
614 $predicates['sha1'][$this->params['src']] = false;
615 return $status; // nothing to do
616 } else {
617 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
618 return $status;
619 }
620 // Check if a file can be placed/changed at the destination
621 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
622 $status->fatal( 'backend-fail-usable', $this->params['dst'] );
623 $status->fatal( 'backend-fail-copy', $this->params['src'], $this->params['dst'] );
624 return $status;
625 }
626 // Check if destination file exists
627 $status->merge( $this->precheckDestExistence( $predicates ) );
628 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
629 if ( $status->isOK() ) {
630 // Update file existence predicates
631 $predicates['exists'][$this->params['dst']] = true;
632 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
633 }
634 return $status; // safe to call attempt()
635 }
636
637 /**
638 * @return Status
639 */
640 protected function doAttempt() {
641 // Do nothing if the src/dst paths are the same
642 if ( $this->params['src'] !== $this->params['dst'] ) {
643 // Copy the file into the destination
644 if ( !$this->destSameAsSource ) {
645 return $this->backend->copyInternal( $this->setFlags( $this->params ) );
646 }
647 }
648 return Status::newGood();
649 }
650
651 /**
652 * @return array
653 */
654 public function storagePathsRead() {
655 return array( $this->params['src'] );
656 }
657
658 /**
659 * @return array
660 */
661 public function storagePathsChanged() {
662 return array( $this->params['dst'] );
663 }
664 }
665
666 /**
667 * Move a file from one storage path to another in the backend.
668 * Parameters for this operation are outlined in FileBackend::doOperations().
669 */
670 class MoveFileOp extends FileOp {
671 /**
672 * @return array
673 */
674 protected function allowedParams() {
675 return array( array( 'src', 'dst' ),
676 array( 'overwrite', 'overwriteSame', 'ignoreMissingSource', 'disposition' ) );
677 }
678
679 /**
680 * @param $predicates array
681 * @return Status
682 */
683 protected function doPrecheck( array &$predicates ) {
684 $status = Status::newGood();
685 // Check if the source file exists
686 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
687 if ( $this->getParam( 'ignoreMissingSource' ) ) {
688 $this->doOperation = false; // no-op
689 // Update file existence predicates (cache 404s)
690 $predicates['exists'][$this->params['src']] = false;
691 $predicates['sha1'][$this->params['src']] = false;
692 return $status; // nothing to do
693 } else {
694 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
695 return $status;
696 }
697 // Check if a file can be placed/changed at the destination
698 } elseif ( !$this->backend->isPathUsableInternal( $this->params['dst'] ) ) {
699 $status->fatal( 'backend-fail-usable', $this->params['dst'] );
700 $status->fatal( 'backend-fail-move', $this->params['src'], $this->params['dst'] );
701 return $status;
702 }
703 // Check if destination file exists
704 $status->merge( $this->precheckDestExistence( $predicates ) );
705 $this->params['dstExists'] = $this->destExists; // see FileBackendStore::setFileCache()
706 if ( $status->isOK() ) {
707 // Update file existence predicates
708 $predicates['exists'][$this->params['src']] = false;
709 $predicates['sha1'][$this->params['src']] = false;
710 $predicates['exists'][$this->params['dst']] = true;
711 $predicates['sha1'][$this->params['dst']] = $this->sourceSha1;
712 }
713 return $status; // safe to call attempt()
714 }
715
716 /**
717 * @return Status
718 */
719 protected function doAttempt() {
720 // Do nothing if the src/dst paths are the same
721 if ( $this->params['src'] !== $this->params['dst'] ) {
722 if ( !$this->destSameAsSource ) {
723 // Move the file into the destination
724 return $this->backend->moveInternal( $this->setFlags( $this->params ) );
725 } else {
726 // Just delete source as the destination needs no changes
727 $params = array( 'src' => $this->params['src'] );
728 return $this->backend->deleteInternal( $this->setFlags( $params ) );
729 }
730 }
731 return Status::newGood();
732 }
733
734 /**
735 * @return array
736 */
737 public function storagePathsRead() {
738 return array( $this->params['src'] );
739 }
740
741 /**
742 * @return array
743 */
744 public function storagePathsChanged() {
745 return array( $this->params['src'], $this->params['dst'] );
746 }
747 }
748
749 /**
750 * Delete a file at the given storage path from the backend.
751 * Parameters for this operation are outlined in FileBackend::doOperations().
752 */
753 class DeleteFileOp extends FileOp {
754 /**
755 * @return array
756 */
757 protected function allowedParams() {
758 return array( array( 'src' ), array( 'ignoreMissingSource' ) );
759 }
760
761 /**
762 * @param $predicates array
763 * @return Status
764 */
765 protected function doPrecheck( array &$predicates ) {
766 $status = Status::newGood();
767 // Check if the source file exists
768 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
769 if ( $this->getParam( 'ignoreMissingSource' ) ) {
770 $this->doOperation = false; // no-op
771 // Update file existence predicates (cache 404s)
772 $predicates['exists'][$this->params['src']] = false;
773 $predicates['sha1'][$this->params['src']] = false;
774 return $status; // nothing to do
775 } else {
776 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
777 return $status;
778 }
779 // Check if a file can be placed/changed at the source
780 } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
781 $status->fatal( 'backend-fail-usable', $this->params['src'] );
782 $status->fatal( 'backend-fail-delete', $this->params['src'] );
783 return $status;
784 }
785 // Update file existence predicates
786 $predicates['exists'][$this->params['src']] = false;
787 $predicates['sha1'][$this->params['src']] = false;
788 return $status; // safe to call attempt()
789 }
790
791 /**
792 * @return Status
793 */
794 protected function doAttempt() {
795 // Delete the source file
796 return $this->backend->deleteInternal( $this->setFlags( $this->params ) );
797 }
798
799 /**
800 * @return array
801 */
802 public function storagePathsChanged() {
803 return array( $this->params['src'] );
804 }
805 }
806
807 /**
808 * Change metadata for a file at the given storage path in the backend.
809 * Parameters for this operation are outlined in FileBackend::doOperations().
810 */
811 class DescribeFileOp extends FileOp {
812 /**
813 * @return array
814 */
815 protected function allowedParams() {
816 return array( array( 'src' ), array( 'disposition', 'headers' ) );
817 }
818
819 /**
820 * @param $predicates array
821 * @return Status
822 */
823 protected function doPrecheck( array &$predicates ) {
824 $status = Status::newGood();
825 // Check if the source file exists
826 if ( !$this->fileExists( $this->params['src'], $predicates ) ) {
827 $status->fatal( 'backend-fail-notexists', $this->params['src'] );
828 return $status;
829 // Check if a file can be placed/changed at the source
830 } elseif ( !$this->backend->isPathUsableInternal( $this->params['src'] ) ) {
831 $status->fatal( 'backend-fail-usable', $this->params['src'] );
832 $status->fatal( 'backend-fail-describe', $this->params['src'] );
833 return $status;
834 }
835 // Update file existence predicates
836 $predicates['exists'][$this->params['src']] =
837 $this->fileExists( $this->params['src'], $predicates );
838 $predicates['sha1'][$this->params['src']] =
839 $this->fileSha1( $this->params['src'], $predicates );
840 return $status; // safe to call attempt()
841 }
842
843 /**
844 * @return Status
845 */
846 protected function doAttempt() {
847 // Update the source file's metadata
848 return $this->backend->describeInternal( $this->setFlags( $this->params ) );
849 }
850
851 /**
852 * @return array
853 */
854 public function storagePathsChanged() {
855 return array( $this->params['src'] );
856 }
857 }
858
859 /**
860 * Placeholder operation that has no params and does nothing
861 */
862 class NullFileOp extends FileOp {}