MediaWiki REL1_30
SwiftFileBackend.php
Go to the documentation of this file.
1<?php
36 protected $http;
38 protected $authTTL;
40 protected $swiftAuthUrl;
44 protected $swiftUser;
46 protected $swiftKey;
50 protected $rgwS3AccessKey;
52 protected $rgwS3SecretKey;
53
55 protected $srvCache;
56
59
61 protected $authCreds;
63 protected $authSessionTimestamp = 0;
65 protected $authErrorTimestamp = null;
66
68 protected $isRGW = false;
69
100 public function __construct( array $config ) {
101 parent::__construct( $config );
102 // Required settings
103 $this->swiftAuthUrl = $config['swiftAuthUrl'];
104 $this->swiftUser = $config['swiftUser'];
105 $this->swiftKey = $config['swiftKey'];
106 // Optional settings
107 $this->authTTL = isset( $config['swiftAuthTTL'] )
108 ? $config['swiftAuthTTL']
109 : 15 * 60; // some sane number
110 $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
111 ? $config['swiftTempUrlKey']
112 : '';
113 $this->swiftStorageUrl = isset( $config['swiftStorageUrl'] )
114 ? $config['swiftStorageUrl']
115 : null;
116 $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
117 ? $config['shardViaHashLevels']
118 : '';
119 $this->rgwS3AccessKey = isset( $config['rgwS3AccessKey'] )
120 ? $config['rgwS3AccessKey']
121 : '';
122 $this->rgwS3SecretKey = isset( $config['rgwS3SecretKey'] )
123 ? $config['rgwS3SecretKey']
124 : '';
125 // HTTP helper client
126 $this->http = new MultiHttpClient( [] );
127 // Cache container information to mask latency
128 if ( isset( $config['wanCache'] ) && $config['wanCache'] instanceof WANObjectCache ) {
129 $this->memCache = $config['wanCache'];
130 }
131 // Process cache for container info
132 $this->containerStatCache = new ProcessCacheLRU( 300 );
133 // Cache auth token information to avoid RTTs
134 if ( !empty( $config['cacheAuthInfo'] ) && isset( $config['srvCache'] ) ) {
135 $this->srvCache = $config['srvCache'];
136 } else {
137 $this->srvCache = new EmptyBagOStuff();
138 }
139 }
140
145
146 protected function resolveContainerPath( $container, $relStoragePath ) {
147 if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) {
148 return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
149 } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
150 return null; // too long for Swift
151 }
152
153 return $relStoragePath;
154 }
155
156 public function isPathUsableInternal( $storagePath ) {
157 list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
158 if ( $rel === null ) {
159 return false; // invalid
160 }
161
162 return is_array( $this->getContainerStat( $container ) );
163 }
164
172 protected function sanitizeHdrs( array $params ) {
173 return isset( $params['headers'] )
174 ? $this->getCustomHeaders( $params['headers'] )
175 : [];
176 }
177
182 protected function getCustomHeaders( array $rawHeaders ) {
183 $headers = [];
184
185 // Normalize casing, and strip out illegal headers
186 foreach ( $rawHeaders as $name => $value ) {
187 $name = strtolower( $name );
188 if ( preg_match( '/^content-(type|length)$/', $name ) ) {
189 continue; // blacklisted
190 } elseif ( preg_match( '/^(x-)?content-/', $name ) ) {
191 $headers[$name] = $value; // allowed
192 } elseif ( preg_match( '/^content-(disposition)/', $name ) ) {
193 $headers[$name] = $value; // allowed
194 }
195 }
196 // By default, Swift has annoyingly low maximum header value limits
197 if ( isset( $headers['content-disposition'] ) ) {
198 $disposition = '';
199 // @note: assume FileBackend::makeContentDisposition() already used
200 foreach ( explode( ';', $headers['content-disposition'] ) as $part ) {
201 $part = trim( $part );
202 $new = ( $disposition === '' ) ? $part : "{$disposition};{$part}";
203 if ( strlen( $new ) <= 255 ) {
204 $disposition = $new;
205 } else {
206 break; // too long; sigh
207 }
208 }
209 $headers['content-disposition'] = $disposition;
210 }
211
212 return $headers;
213 }
214
219 protected function getMetadataHeaders( array $rawHeaders ) {
220 $headers = [];
221 foreach ( $rawHeaders as $name => $value ) {
222 $name = strtolower( $name );
223 if ( strpos( $name, 'x-object-meta-' ) === 0 ) {
224 $headers[$name] = $value;
225 }
226 }
227
228 return $headers;
229 }
230
235 protected function getMetadata( array $rawHeaders ) {
236 $metadata = [];
237 foreach ( $this->getMetadataHeaders( $rawHeaders ) as $name => $value ) {
238 $metadata[substr( $name, strlen( 'x-object-meta-' ) )] = $value;
239 }
240
241 return $metadata;
242 }
243
244 protected function doCreateInternal( array $params ) {
245 $status = $this->newStatus();
246
247 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
248 if ( $dstRel === null ) {
249 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
250
251 return $status;
252 }
253
254 $sha1Hash = Wikimedia\base_convert( sha1( $params['content'] ), 16, 36, 31 );
255 $contentType = isset( $params['headers']['content-type'] )
256 ? $params['headers']['content-type']
257 : $this->getContentType( $params['dst'], $params['content'], null );
258
259 $reqs = [ [
260 'method' => 'PUT',
261 'url' => [ $dstCont, $dstRel ],
262 'headers' => [
263 'content-length' => strlen( $params['content'] ),
264 'etag' => md5( $params['content'] ),
265 'content-type' => $contentType,
266 'x-object-meta-sha1base36' => $sha1Hash
267 ] + $this->sanitizeHdrs( $params ),
268 'body' => $params['content']
269 ] ];
270
271 $method = __METHOD__;
272 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
273 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
274 if ( $rcode === 201 ) {
275 // good
276 } elseif ( $rcode === 412 ) {
277 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
278 } else {
279 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
280 }
281 };
282
283 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
284 if ( !empty( $params['async'] ) ) { // deferred
285 $status->value = $opHandle;
286 } else { // actually write the object in Swift
287 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
288 }
289
290 return $status;
291 }
292
293 protected function doStoreInternal( array $params ) {
294 $status = $this->newStatus();
295
296 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
297 if ( $dstRel === null ) {
298 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
299
300 return $status;
301 }
302
303 MediaWiki\suppressWarnings();
304 $sha1Hash = sha1_file( $params['src'] );
305 MediaWiki\restoreWarnings();
306 if ( $sha1Hash === false ) { // source doesn't exist?
307 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
308
309 return $status;
310 }
311 $sha1Hash = Wikimedia\base_convert( $sha1Hash, 16, 36, 31 );
312 $contentType = isset( $params['headers']['content-type'] )
313 ? $params['headers']['content-type']
314 : $this->getContentType( $params['dst'], null, $params['src'] );
315
316 $handle = fopen( $params['src'], 'rb' );
317 if ( $handle === false ) { // source doesn't exist?
318 $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
319
320 return $status;
321 }
322
323 $reqs = [ [
324 'method' => 'PUT',
325 'url' => [ $dstCont, $dstRel ],
326 'headers' => [
327 'content-length' => filesize( $params['src'] ),
328 'etag' => md5_file( $params['src'] ),
329 'content-type' => $contentType,
330 'x-object-meta-sha1base36' => $sha1Hash
331 ] + $this->sanitizeHdrs( $params ),
332 'body' => $handle // resource
333 ] ];
334
335 $method = __METHOD__;
336 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
337 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
338 if ( $rcode === 201 ) {
339 // good
340 } elseif ( $rcode === 412 ) {
341 $status->fatal( 'backend-fail-contenttype', $params['dst'] );
342 } else {
343 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
344 }
345 };
346
347 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
348 $opHandle->resourcesToClose[] = $handle;
349
350 if ( !empty( $params['async'] ) ) { // deferred
351 $status->value = $opHandle;
352 } else { // actually write the object in Swift
353 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
354 }
355
356 return $status;
357 }
358
359 protected function doCopyInternal( array $params ) {
360 $status = $this->newStatus();
361
362 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
363 if ( $srcRel === null ) {
364 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
365
366 return $status;
367 }
368
369 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
370 if ( $dstRel === null ) {
371 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
372
373 return $status;
374 }
375
376 $reqs = [ [
377 'method' => 'PUT',
378 'url' => [ $dstCont, $dstRel ],
379 'headers' => [
380 'x-copy-from' => '/' . rawurlencode( $srcCont ) .
381 '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
382 ] + $this->sanitizeHdrs( $params ), // extra headers merged into object
383 ] ];
384
385 $method = __METHOD__;
386 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
387 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
388 if ( $rcode === 201 ) {
389 // good
390 } elseif ( $rcode === 404 ) {
391 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
392 } else {
393 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
394 }
395 };
396
397 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
398 if ( !empty( $params['async'] ) ) { // deferred
399 $status->value = $opHandle;
400 } else { // actually write the object in Swift
401 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
402 }
403
404 return $status;
405 }
406
407 protected function doMoveInternal( array $params ) {
408 $status = $this->newStatus();
409
410 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
411 if ( $srcRel === null ) {
412 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
413
414 return $status;
415 }
416
417 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
418 if ( $dstRel === null ) {
419 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
420
421 return $status;
422 }
423
424 $reqs = [
425 [
426 'method' => 'PUT',
427 'url' => [ $dstCont, $dstRel ],
428 'headers' => [
429 'x-copy-from' => '/' . rawurlencode( $srcCont ) .
430 '/' . str_replace( "%2F", "/", rawurlencode( $srcRel ) )
431 ] + $this->sanitizeHdrs( $params ) // extra headers merged into object
432 ]
433 ];
434 if ( "{$srcCont}/{$srcRel}" !== "{$dstCont}/{$dstRel}" ) {
435 $reqs[] = [
436 'method' => 'DELETE',
437 'url' => [ $srcCont, $srcRel ],
438 'headers' => []
439 ];
440 }
441
442 $method = __METHOD__;
443 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
444 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
445 if ( $request['method'] === 'PUT' && $rcode === 201 ) {
446 // good
447 } elseif ( $request['method'] === 'DELETE' && $rcode === 204 ) {
448 // good
449 } elseif ( $rcode === 404 ) {
450 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
451 } else {
452 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
453 }
454 };
455
456 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
457 if ( !empty( $params['async'] ) ) { // deferred
458 $status->value = $opHandle;
459 } else { // actually move the object in Swift
460 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
461 }
462
463 return $status;
464 }
465
466 protected function doDeleteInternal( array $params ) {
467 $status = $this->newStatus();
468
469 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
470 if ( $srcRel === null ) {
471 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
472
473 return $status;
474 }
475
476 $reqs = [ [
477 'method' => 'DELETE',
478 'url' => [ $srcCont, $srcRel ],
479 'headers' => []
480 ] ];
481
482 $method = __METHOD__;
483 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
484 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
485 if ( $rcode === 204 ) {
486 // good
487 } elseif ( $rcode === 404 ) {
488 if ( empty( $params['ignoreMissingSource'] ) ) {
489 $status->fatal( 'backend-fail-delete', $params['src'] );
490 }
491 } else {
492 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
493 }
494 };
495
496 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
497 if ( !empty( $params['async'] ) ) { // deferred
498 $status->value = $opHandle;
499 } else { // actually delete the object in Swift
500 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
501 }
502
503 return $status;
504 }
505
506 protected function doDescribeInternal( array $params ) {
507 $status = $this->newStatus();
508
509 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
510 if ( $srcRel === null ) {
511 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
512
513 return $status;
514 }
515
516 // Fetch the old object headers/metadata...this should be in stat cache by now
517 $stat = $this->getFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
518 if ( $stat && !isset( $stat['xattr'] ) ) { // older cache entry
519 $stat = $this->doGetFileStat( [ 'src' => $params['src'], 'latest' => 1 ] );
520 }
521 if ( !$stat ) {
522 $status->fatal( 'backend-fail-describe', $params['src'] );
523
524 return $status;
525 }
526
527 // POST clears prior headers, so we need to merge the changes in to the old ones
528 $metaHdrs = [];
529 foreach ( $stat['xattr']['metadata'] as $name => $value ) {
530 $metaHdrs["x-object-meta-$name"] = $value;
531 }
532 $customHdrs = $this->sanitizeHdrs( $params ) + $stat['xattr']['headers'];
533
534 $reqs = [ [
535 'method' => 'POST',
536 'url' => [ $srcCont, $srcRel ],
537 'headers' => $metaHdrs + $customHdrs
538 ] ];
539
540 $method = __METHOD__;
541 $handler = function ( array $request, StatusValue $status ) use ( $method, $params ) {
542 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $request['response'];
543 if ( $rcode === 202 ) {
544 // good
545 } elseif ( $rcode === 404 ) {
546 $status->fatal( 'backend-fail-describe', $params['src'] );
547 } else {
548 $this->onError( $status, $method, $params, $rerr, $rcode, $rdesc );
549 }
550 };
551
552 $opHandle = new SwiftFileOpHandle( $this, $handler, $reqs );
553 if ( !empty( $params['async'] ) ) { // deferred
554 $status->value = $opHandle;
555 } else { // actually change the object in Swift
556 $status->merge( current( $this->executeOpHandlesInternal( [ $opHandle ] ) ) );
557 }
558
559 return $status;
560 }
561
562 protected function doPrepareInternal( $fullCont, $dir, array $params ) {
563 $status = $this->newStatus();
564
565 // (a) Check if container already exists
566 $stat = $this->getContainerStat( $fullCont );
567 if ( is_array( $stat ) ) {
568 return $status; // already there
569 } elseif ( $stat === null ) {
570 $status->fatal( 'backend-fail-internal', $this->name );
571 $this->logger->error( __METHOD__ . ': cannot get container stat' );
572
573 return $status;
574 }
575
576 // (b) Create container as needed with proper ACLs
577 if ( $stat === false ) {
578 $params['op'] = 'prepare';
579 $status->merge( $this->createContainer( $fullCont, $params ) );
580 }
581
582 return $status;
583 }
584
585 protected function doSecureInternal( $fullCont, $dir, array $params ) {
586 $status = $this->newStatus();
587 if ( empty( $params['noAccess'] ) ) {
588 return $status; // nothing to do
589 }
590
591 $stat = $this->getContainerStat( $fullCont );
592 if ( is_array( $stat ) ) {
593 // Make container private to end-users...
594 $status->merge( $this->setContainerAccess(
595 $fullCont,
596 [ $this->swiftUser ], // read
597 [ $this->swiftUser ] // write
598 ) );
599 } elseif ( $stat === false ) {
600 $status->fatal( 'backend-fail-usable', $params['dir'] );
601 } else {
602 $status->fatal( 'backend-fail-internal', $this->name );
603 $this->logger->error( __METHOD__ . ': cannot get container stat' );
604 }
605
606 return $status;
607 }
608
609 protected function doPublishInternal( $fullCont, $dir, array $params ) {
610 $status = $this->newStatus();
611
612 $stat = $this->getContainerStat( $fullCont );
613 if ( is_array( $stat ) ) {
614 // Make container public to end-users...
615 $status->merge( $this->setContainerAccess(
616 $fullCont,
617 [ $this->swiftUser, '.r:*' ], // read
618 [ $this->swiftUser ] // write
619 ) );
620 } elseif ( $stat === false ) {
621 $status->fatal( 'backend-fail-usable', $params['dir'] );
622 } else {
623 $status->fatal( 'backend-fail-internal', $this->name );
624 $this->logger->error( __METHOD__ . ': cannot get container stat' );
625 }
626
627 return $status;
628 }
629
630 protected function doCleanInternal( $fullCont, $dir, array $params ) {
631 $status = $this->newStatus();
632
633 // Only containers themselves can be removed, all else is virtual
634 if ( $dir != '' ) {
635 return $status; // nothing to do
636 }
637
638 // (a) Check the container
639 $stat = $this->getContainerStat( $fullCont, true );
640 if ( $stat === false ) {
641 return $status; // ok, nothing to do
642 } elseif ( !is_array( $stat ) ) {
643 $status->fatal( 'backend-fail-internal', $this->name );
644 $this->logger->error( __METHOD__ . ': cannot get container stat' );
645
646 return $status;
647 }
648
649 // (b) Delete the container if empty
650 if ( $stat['count'] == 0 ) {
651 $params['op'] = 'clean';
652 $status->merge( $this->deleteContainer( $fullCont, $params ) );
653 }
654
655 return $status;
656 }
657
658 protected function doGetFileStat( array $params ) {
659 $params = [ 'srcs' => [ $params['src'] ], 'concurrency' => 1 ] + $params;
660 unset( $params['src'] );
661 $stats = $this->doGetFileStatMulti( $params );
662
663 return reset( $stats );
664 }
665
676 protected function convertSwiftDate( $ts, $format = TS_MW ) {
677 try {
678 $timestamp = new MWTimestamp( $ts );
679
680 return $timestamp->getTimestamp( $format );
681 } catch ( Exception $e ) {
682 throw new FileBackendError( $e->getMessage() );
683 }
684 }
685
693 protected function addMissingMetadata( array $objHdrs, $path ) {
694 if ( isset( $objHdrs['x-object-meta-sha1base36'] ) ) {
695 return $objHdrs; // nothing to do
696 }
697
699 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
700 $this->logger->error( __METHOD__ . ": $path was not stored with SHA-1 metadata." );
701
702 $objHdrs['x-object-meta-sha1base36'] = false;
703
704 $auth = $this->getAuthentication();
705 if ( !$auth ) {
706 return $objHdrs; // failed
707 }
708
709 // Find prior custom HTTP headers
710 $postHeaders = $this->getCustomHeaders( $objHdrs );
711 // Find prior metadata headers
712 $postHeaders += $this->getMetadataHeaders( $objHdrs );
713
714 $status = $this->newStatus();
716 $scopeLockS = $this->getScopedFileLocks( [ $path ], LockManager::LOCK_UW, $status );
717 if ( $status->isOK() ) {
718 $tmpFile = $this->getLocalCopy( [ 'src' => $path, 'latest' => 1 ] );
719 if ( $tmpFile ) {
720 $hash = $tmpFile->getSha1Base36();
721 if ( $hash !== false ) {
722 $objHdrs['x-object-meta-sha1base36'] = $hash;
723 // Merge new SHA1 header into the old ones
724 $postHeaders['x-object-meta-sha1base36'] = $hash;
725 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
726 list( $rcode ) = $this->http->run( [
727 'method' => 'POST',
728 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
729 'headers' => $this->authTokenHeaders( $auth ) + $postHeaders
730 ] );
731 if ( $rcode >= 200 && $rcode <= 299 ) {
732 $this->deleteFileCache( $path );
733
734 return $objHdrs; // success
735 }
736 }
737 }
738 }
739
740 $this->logger->error( __METHOD__ . ": unable to set SHA-1 metadata for $path" );
741
742 return $objHdrs; // failed
743 }
744
745 protected function doGetFileContentsMulti( array $params ) {
746 $contents = [];
747
748 $auth = $this->getAuthentication();
749
750 $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
751 // Blindly create tmp files and stream to them, catching any exception if the file does
752 // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
753 $reqs = []; // (path => op)
754
755 foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
756 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
757 if ( $srcRel === null || !$auth ) {
758 $contents[$path] = false;
759 continue;
760 }
761 // Create a new temporary memory file...
762 $handle = fopen( 'php://temp', 'wb' );
763 if ( $handle ) {
764 $reqs[$path] = [
765 'method' => 'GET',
766 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
767 'headers' => $this->authTokenHeaders( $auth )
768 + $this->headersFromParams( $params ),
769 'stream' => $handle,
770 ];
771 }
772 $contents[$path] = false;
773 }
774
775 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
776 $reqs = $this->http->runMulti( $reqs, $opts );
777 foreach ( $reqs as $path => $op ) {
778 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
779 if ( $rcode >= 200 && $rcode <= 299 ) {
780 rewind( $op['stream'] ); // start from the beginning
781 $contents[$path] = stream_get_contents( $op['stream'] );
782 } elseif ( $rcode === 404 ) {
783 $contents[$path] = false;
784 } else {
785 $this->onError( null, __METHOD__,
786 [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
787 }
788 fclose( $op['stream'] ); // close open handle
789 }
790
791 return $contents;
792 }
793
794 protected function doDirectoryExists( $fullCont, $dir, array $params ) {
795 $prefix = ( $dir == '' ) ? null : "{$dir}/";
796 $status = $this->objectListing( $fullCont, 'names', 1, null, $prefix );
797 if ( $status->isOK() ) {
798 return ( count( $status->value ) ) > 0;
799 }
800
801 return null; // error
802 }
803
811 public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
812 return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
813 }
814
822 public function getFileListInternal( $fullCont, $dir, array $params ) {
823 return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
824 }
825
837 public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
838 $dirs = [];
839 if ( $after === INF ) {
840 return $dirs; // nothing more
841 }
842
843 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
844
845 $prefix = ( $dir == '' ) ? null : "{$dir}/";
846 // Non-recursive: only list dirs right under $dir
847 if ( !empty( $params['topOnly'] ) ) {
848 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
849 if ( !$status->isOK() ) {
850 throw new FileBackendError( "Iterator page I/O error." );
851 }
852 $objects = $status->value;
853 foreach ( $objects as $object ) { // files and directories
854 if ( substr( $object, -1 ) === '/' ) {
855 $dirs[] = $object; // directories end in '/'
856 }
857 }
858 } else {
859 // Recursive: list all dirs under $dir and its subdirs
860 $getParentDir = function ( $path ) {
861 return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
862 };
863
864 // Get directory from last item of prior page
865 $lastDir = $getParentDir( $after ); // must be first page
866 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
867
868 if ( !$status->isOK() ) {
869 throw new FileBackendError( "Iterator page I/O error." );
870 }
871
872 $objects = $status->value;
873
874 foreach ( $objects as $object ) { // files
875 $objectDir = $getParentDir( $object ); // directory of object
876
877 if ( $objectDir !== false && $objectDir !== $dir ) {
878 // Swift stores paths in UTF-8, using binary sorting.
879 // See function "create_container_table" in common/db.py.
880 // If a directory is not "greater" than the last one,
881 // then it was already listed by the calling iterator.
882 if ( strcmp( $objectDir, $lastDir ) > 0 ) {
883 $pDir = $objectDir;
884 do { // add dir and all its parent dirs
885 $dirs[] = "{$pDir}/";
886 $pDir = $getParentDir( $pDir );
887 } while ( $pDir !== false // sanity
888 && strcmp( $pDir, $lastDir ) > 0 // not done already
889 && strlen( $pDir ) > strlen( $dir ) // within $dir
890 );
891 }
892 $lastDir = $objectDir;
893 }
894 }
895 }
896 // Page on the unfiltered directory listing (what is returned may be filtered)
897 if ( count( $objects ) < $limit ) {
898 $after = INF; // avoid a second RTT
899 } else {
900 $after = end( $objects ); // update last item
901 }
902
903 return $dirs;
904 }
905
917 public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
918 $files = []; // list of (path, stat array or null) entries
919 if ( $after === INF ) {
920 return $files; // nothing more
921 }
922
923 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
924
925 $prefix = ( $dir == '' ) ? null : "{$dir}/";
926 // $objects will contain a list of unfiltered names or CF_Object items
927 // Non-recursive: only list files right under $dir
928 if ( !empty( $params['topOnly'] ) ) {
929 if ( !empty( $params['adviseStat'] ) ) {
930 $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix, '/' );
931 } else {
932 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix, '/' );
933 }
934 } else {
935 // Recursive: list all files under $dir and its subdirs
936 if ( !empty( $params['adviseStat'] ) ) {
937 $status = $this->objectListing( $fullCont, 'info', $limit, $after, $prefix );
938 } else {
939 $status = $this->objectListing( $fullCont, 'names', $limit, $after, $prefix );
940 }
941 }
942
943 // Reformat this list into a list of (name, stat array or null) entries
944 if ( !$status->isOK() ) {
945 throw new FileBackendError( "Iterator page I/O error." );
946 }
947
948 $objects = $status->value;
949 $files = $this->buildFileObjectListing( $params, $dir, $objects );
950
951 // Page on the unfiltered object listing (what is returned may be filtered)
952 if ( count( $objects ) < $limit ) {
953 $after = INF; // avoid a second RTT
954 } else {
955 $after = end( $objects ); // update last item
956 $after = is_object( $after ) ? $after->name : $after;
957 }
958
959 return $files;
960 }
961
971 private function buildFileObjectListing( array $params, $dir, array $objects ) {
972 $names = [];
973 foreach ( $objects as $object ) {
974 if ( is_object( $object ) ) {
975 if ( isset( $object->subdir ) || !isset( $object->name ) ) {
976 continue; // virtual directory entry; ignore
977 }
978 $stat = [
979 // Convert various random Swift dates to TS_MW
980 'mtime' => $this->convertSwiftDate( $object->last_modified, TS_MW ),
981 'size' => (int)$object->bytes,
982 'sha1' => null,
983 // Note: manifiest ETags are not an MD5 of the file
984 'md5' => ctype_xdigit( $object->hash ) ? $object->hash : null,
985 'latest' => false // eventually consistent
986 ];
987 $names[] = [ $object->name, $stat ];
988 } elseif ( substr( $object, -1 ) !== '/' ) {
989 // Omit directories, which end in '/' in listings
990 $names[] = [ $object, null ];
991 }
992 }
993
994 return $names;
995 }
996
1003 public function loadListingStatInternal( $path, array $val ) {
1004 $this->cheapCache->set( $path, 'stat', $val );
1005 }
1006
1007 protected function doGetFileXAttributes( array $params ) {
1008 $stat = $this->getFileStat( $params );
1009 if ( $stat ) {
1010 if ( !isset( $stat['xattr'] ) ) {
1011 // Stat entries filled by file listings don't include metadata/headers
1012 $this->clearCache( [ $params['src'] ] );
1013 $stat = $this->getFileStat( $params );
1014 }
1015
1016 return $stat['xattr'];
1017 } else {
1018 return false;
1019 }
1020 }
1021
1022 protected function doGetFileSha1base36( array $params ) {
1023 $stat = $this->getFileStat( $params );
1024 if ( $stat ) {
1025 if ( !isset( $stat['sha1'] ) ) {
1026 // Stat entries filled by file listings don't include SHA1
1027 $this->clearCache( [ $params['src'] ] );
1028 $stat = $this->getFileStat( $params );
1029 }
1030
1031 return $stat['sha1'];
1032 } else {
1033 return false;
1034 }
1035 }
1036
1037 protected function doStreamFile( array $params ) {
1038 $status = $this->newStatus();
1039
1040 $flags = !empty( $params['headless'] ) ? StreamFile::STREAM_HEADLESS : 0;
1041
1042 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1043 if ( $srcRel === null ) {
1045 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
1046
1047 return $status;
1048 }
1049
1050 $auth = $this->getAuthentication();
1051 if ( !$auth || !is_array( $this->getContainerStat( $srcCont ) ) ) {
1053 $status->fatal( 'backend-fail-stream', $params['src'] );
1054
1055 return $status;
1056 }
1057
1058 // If "headers" is set, we only want to send them if the file is there.
1059 // Do not bother checking if the file exists if headers are not set though.
1060 if ( $params['headers'] && !$this->fileExists( $params ) ) {
1062 $status->fatal( 'backend-fail-stream', $params['src'] );
1063
1064 return $status;
1065 }
1066
1067 // Send the requested additional headers
1068 foreach ( $params['headers'] as $header ) {
1069 header( $header ); // aways send
1070 }
1071
1072 if ( empty( $params['allowOB'] ) ) {
1073 // Cancel output buffering and gzipping if set
1074 call_user_func( $this->obResetFunc );
1075 }
1076
1077 $handle = fopen( 'php://output', 'wb' );
1078 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1079 'method' => 'GET',
1080 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1081 'headers' => $this->authTokenHeaders( $auth )
1082 + $this->headersFromParams( $params ) + $params['options'],
1083 'stream' => $handle,
1084 'flags' => [ 'relayResponseHeaders' => empty( $params['headless'] ) ]
1085 ] );
1086
1087 if ( $rcode >= 200 && $rcode <= 299 ) {
1088 // good
1089 } elseif ( $rcode === 404 ) {
1090 $status->fatal( 'backend-fail-stream', $params['src'] );
1091 // Per T43113, nasty things can happen if bad cache entries get
1092 // stuck in cache. It's also possible that this error can come up
1093 // with simple race conditions. Clear out the stat cache to be safe.
1094 $this->clearCache( [ $params['src'] ] );
1095 $this->deleteFileCache( $params['src'] );
1096 } else {
1097 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1098 }
1099
1100 return $status;
1101 }
1102
1103 protected function doGetLocalCopyMulti( array $params ) {
1105 $tmpFiles = [];
1106
1107 $auth = $this->getAuthentication();
1108
1109 $ep = array_diff_key( $params, [ 'srcs' => 1 ] ); // for error logging
1110 // Blindly create tmp files and stream to them, catching any exception if the file does
1111 // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
1112 $reqs = []; // (path => op)
1113
1114 foreach ( $params['srcs'] as $path ) { // each path in this concurrent batch
1115 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1116 if ( $srcRel === null || !$auth ) {
1117 $tmpFiles[$path] = null;
1118 continue;
1119 }
1120 // Get source file extension
1122 // Create a new temporary file...
1123 $tmpFile = TempFSFile::factory( 'localcopy_', $ext, $this->tmpDirectory );
1124 if ( $tmpFile ) {
1125 $handle = fopen( $tmpFile->getPath(), 'wb' );
1126 if ( $handle ) {
1127 $reqs[$path] = [
1128 'method' => 'GET',
1129 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1130 'headers' => $this->authTokenHeaders( $auth )
1131 + $this->headersFromParams( $params ),
1132 'stream' => $handle,
1133 ];
1134 } else {
1135 $tmpFile = null;
1136 }
1137 }
1138 $tmpFiles[$path] = $tmpFile;
1139 }
1140
1141 $isLatest = ( $this->isRGW || !empty( $params['latest'] ) );
1142 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1143 $reqs = $this->http->runMulti( $reqs, $opts );
1144 foreach ( $reqs as $path => $op ) {
1145 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $op['response'];
1146 fclose( $op['stream'] ); // close open handle
1147 if ( $rcode >= 200 && $rcode <= 299 ) {
1148 $size = $tmpFiles[$path] ? $tmpFiles[$path]->getSize() : 0;
1149 // Double check that the disk is not full/broken
1150 if ( $size != $rhdrs['content-length'] ) {
1151 $tmpFiles[$path] = null;
1152 $rerr = "Got {$size}/{$rhdrs['content-length']} bytes";
1153 $this->onError( null, __METHOD__,
1154 [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1155 }
1156 // Set the file stat process cache in passing
1157 $stat = $this->getStatFromHeaders( $rhdrs );
1158 $stat['latest'] = $isLatest;
1159 $this->cheapCache->set( $path, 'stat', $stat );
1160 } elseif ( $rcode === 404 ) {
1161 $tmpFiles[$path] = false;
1162 } else {
1163 $tmpFiles[$path] = null;
1164 $this->onError( null, __METHOD__,
1165 [ 'src' => $path ] + $ep, $rerr, $rcode, $rdesc );
1166 }
1167 }
1168
1169 return $tmpFiles;
1170 }
1171
1172 public function getFileHttpUrl( array $params ) {
1173 if ( $this->swiftTempUrlKey != '' ||
1174 ( $this->rgwS3AccessKey != '' && $this->rgwS3SecretKey != '' )
1175 ) {
1176 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
1177 if ( $srcRel === null ) {
1178 return null; // invalid path
1179 }
1180
1181 $auth = $this->getAuthentication();
1182 if ( !$auth ) {
1183 return null;
1184 }
1185
1186 $ttl = isset( $params['ttl'] ) ? $params['ttl'] : 86400;
1187 $expires = time() + $ttl;
1188
1189 if ( $this->swiftTempUrlKey != '' ) {
1190 $url = $this->storageUrl( $auth, $srcCont, $srcRel );
1191 // Swift wants the signature based on the unencoded object name
1192 $contPath = parse_url( $this->storageUrl( $auth, $srcCont ), PHP_URL_PATH );
1193 $signature = hash_hmac( 'sha1',
1194 "GET\n{$expires}\n{$contPath}/{$srcRel}",
1195 $this->swiftTempUrlKey
1196 );
1197
1198 return "{$url}?temp_url_sig={$signature}&temp_url_expires={$expires}";
1199 } else { // give S3 API URL for rgw
1200 // Path for signature starts with the bucket
1201 $spath = '/' . rawurlencode( $srcCont ) . '/' .
1202 str_replace( '%2F', '/', rawurlencode( $srcRel ) );
1203 // Calculate the hash
1204 $signature = base64_encode( hash_hmac(
1205 'sha1',
1206 "GET\n\n\n{$expires}\n{$spath}",
1207 $this->rgwS3SecretKey,
1208 true // raw
1209 ) );
1210 // See https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
1211 // Note: adding a newline for empty CanonicalizedAmzHeaders does not work.
1212 // Note: S3 API is the rgw default; remove the /swift/ URL bit.
1213 return str_replace( '/swift/v1', '', $this->storageUrl( $auth ) . $spath ) .
1214 '?' .
1215 http_build_query( [
1216 'Signature' => $signature,
1217 'Expires' => $expires,
1218 'AWSAccessKeyId' => $this->rgwS3AccessKey
1219 ] );
1220 }
1221 }
1222
1223 return null;
1224 }
1225
1226 protected function directoriesAreVirtual() {
1227 return true;
1228 }
1229
1238 protected function headersFromParams( array $params ) {
1239 $hdrs = [];
1240 if ( !empty( $params['latest'] ) ) {
1241 $hdrs['x-newest'] = 'true';
1242 }
1243
1244 return $hdrs;
1245 }
1246
1252 protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
1254 $statuses = [];
1255
1256 $auth = $this->getAuthentication();
1257 if ( !$auth ) {
1258 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1259 $statuses[$index] = $this->newStatus( 'backend-fail-connect', $this->name );
1260 }
1261
1262 return $statuses;
1263 }
1264
1265 // Split the HTTP requests into stages that can be done concurrently
1266 $httpReqsByStage = []; // map of (stage => index => HTTP request)
1267 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
1269 $reqs = $fileOpHandle->httpOp;
1270 // Convert the 'url' parameter to an actual URL using $auth
1271 foreach ( $reqs as $stage => &$req ) {
1272 list( $container, $relPath ) = $req['url'];
1273 $req['url'] = $this->storageUrl( $auth, $container, $relPath );
1274 $req['headers'] = isset( $req['headers'] ) ? $req['headers'] : [];
1275 $req['headers'] = $this->authTokenHeaders( $auth ) + $req['headers'];
1276 $httpReqsByStage[$stage][$index] = $req;
1277 }
1278 $statuses[$index] = $this->newStatus();
1279 }
1280
1281 // Run all requests for the first stage, then the next, and so on
1282 $reqCount = count( $httpReqsByStage );
1283 for ( $stage = 0; $stage < $reqCount; ++$stage ) {
1284 $httpReqs = $this->http->runMulti( $httpReqsByStage[$stage] );
1285 foreach ( $httpReqs as $index => $httpReq ) {
1286 // Run the callback for each request of this operation
1287 $callback = $fileOpHandles[$index]->callback;
1288 call_user_func_array( $callback, [ $httpReq, $statuses[$index] ] );
1289 // On failure, abort all remaining requests for this operation
1290 // (e.g. abort the DELETE request if the COPY request fails for a move)
1291 if ( !$statuses[$index]->isOK() ) {
1292 $stages = count( $fileOpHandles[$index]->httpOp );
1293 for ( $s = ( $stage + 1 ); $s < $stages; ++$s ) {
1294 unset( $httpReqsByStage[$s][$index] );
1295 }
1296 }
1297 }
1298 }
1299
1300 return $statuses;
1301 }
1302
1325 protected function setContainerAccess( $container, array $readGrps, array $writeGrps ) {
1326 $status = $this->newStatus();
1327 $auth = $this->getAuthentication();
1328
1329 if ( !$auth ) {
1330 $status->fatal( 'backend-fail-connect', $this->name );
1331
1332 return $status;
1333 }
1334
1335 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1336 'method' => 'POST',
1337 'url' => $this->storageUrl( $auth, $container ),
1338 'headers' => $this->authTokenHeaders( $auth ) + [
1339 'x-container-read' => implode( ',', $readGrps ),
1340 'x-container-write' => implode( ',', $writeGrps )
1341 ]
1342 ] );
1343
1344 if ( $rcode != 204 && $rcode !== 202 ) {
1345 $status->fatal( 'backend-fail-internal', $this->name );
1346 $this->logger->error( __METHOD__ . ': unexpected rcode value (' . $rcode . ')' );
1347 }
1348
1349 return $status;
1350 }
1351
1360 protected function getContainerStat( $container, $bypassCache = false ) {
1361 $ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
1362
1363 if ( $bypassCache ) { // purge cache
1364 $this->containerStatCache->clear( $container );
1365 } elseif ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1366 $this->primeContainerCache( [ $container ] ); // check persistent cache
1367 }
1368 if ( !$this->containerStatCache->has( $container, 'stat' ) ) {
1369 $auth = $this->getAuthentication();
1370 if ( !$auth ) {
1371 return null;
1372 }
1373
1374 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1375 'method' => 'HEAD',
1376 'url' => $this->storageUrl( $auth, $container ),
1377 'headers' => $this->authTokenHeaders( $auth )
1378 ] );
1379
1380 if ( $rcode === 204 ) {
1381 $stat = [
1382 'count' => $rhdrs['x-container-object-count'],
1383 'bytes' => $rhdrs['x-container-bytes-used']
1384 ];
1385 if ( $bypassCache ) {
1386 return $stat;
1387 } else {
1388 $this->containerStatCache->set( $container, 'stat', $stat ); // cache it
1389 $this->setContainerCache( $container, $stat ); // update persistent cache
1390 }
1391 } elseif ( $rcode === 404 ) {
1392 return false;
1393 } else {
1394 $this->onError( null, __METHOD__,
1395 [ 'cont' => $container ], $rerr, $rcode, $rdesc );
1396
1397 return null;
1398 }
1399 }
1400
1401 return $this->containerStatCache->get( $container, 'stat' );
1402 }
1403
1411 protected function createContainer( $container, array $params ) {
1412 $status = $this->newStatus();
1413
1414 $auth = $this->getAuthentication();
1415 if ( !$auth ) {
1416 $status->fatal( 'backend-fail-connect', $this->name );
1417
1418 return $status;
1419 }
1420
1421 // @see SwiftFileBackend::setContainerAccess()
1422 if ( empty( $params['noAccess'] ) ) {
1423 $readGrps = [ '.r:*', $this->swiftUser ]; // public
1424 } else {
1425 $readGrps = [ $this->swiftUser ]; // private
1426 }
1427 $writeGrps = [ $this->swiftUser ]; // sanity
1428
1429 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1430 'method' => 'PUT',
1431 'url' => $this->storageUrl( $auth, $container ),
1432 'headers' => $this->authTokenHeaders( $auth ) + [
1433 'x-container-read' => implode( ',', $readGrps ),
1434 'x-container-write' => implode( ',', $writeGrps )
1435 ]
1436 ] );
1437
1438 if ( $rcode === 201 ) { // new
1439 // good
1440 } elseif ( $rcode === 202 ) { // already there
1441 // this shouldn't really happen, but is OK
1442 } else {
1443 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1444 }
1445
1446 return $status;
1447 }
1448
1456 protected function deleteContainer( $container, array $params ) {
1457 $status = $this->newStatus();
1458
1459 $auth = $this->getAuthentication();
1460 if ( !$auth ) {
1461 $status->fatal( 'backend-fail-connect', $this->name );
1462
1463 return $status;
1464 }
1465
1466 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1467 'method' => 'DELETE',
1468 'url' => $this->storageUrl( $auth, $container ),
1469 'headers' => $this->authTokenHeaders( $auth )
1470 ] );
1471
1472 if ( $rcode >= 200 && $rcode <= 299 ) { // deleted
1473 $this->containerStatCache->clear( $container ); // purge
1474 } elseif ( $rcode === 404 ) { // not there
1475 // this shouldn't really happen, but is OK
1476 } elseif ( $rcode === 409 ) { // not empty
1477 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc ); // race?
1478 } else {
1479 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1480 }
1481
1482 return $status;
1483 }
1484
1497 private function objectListing(
1498 $fullCont, $type, $limit, $after = null, $prefix = null, $delim = null
1499 ) {
1500 $status = $this->newStatus();
1501
1502 $auth = $this->getAuthentication();
1503 if ( !$auth ) {
1504 $status->fatal( 'backend-fail-connect', $this->name );
1505
1506 return $status;
1507 }
1508
1509 $query = [ 'limit' => $limit ];
1510 if ( $type === 'info' ) {
1511 $query['format'] = 'json';
1512 }
1513 if ( $after !== null ) {
1514 $query['marker'] = $after;
1515 }
1516 if ( $prefix !== null ) {
1517 $query['prefix'] = $prefix;
1518 }
1519 if ( $delim !== null ) {
1520 $query['delimiter'] = $delim;
1521 }
1522
1523 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1524 'method' => 'GET',
1525 'url' => $this->storageUrl( $auth, $fullCont ),
1526 'query' => $query,
1527 'headers' => $this->authTokenHeaders( $auth )
1528 ] );
1529
1530 $params = [ 'cont' => $fullCont, 'prefix' => $prefix, 'delim' => $delim ];
1531 if ( $rcode === 200 ) { // good
1532 if ( $type === 'info' ) {
1533 $status->value = FormatJson::decode( trim( $rbody ) );
1534 } else {
1535 $status->value = explode( "\n", trim( $rbody ) );
1536 }
1537 } elseif ( $rcode === 204 ) {
1538 $status->value = []; // empty container
1539 } elseif ( $rcode === 404 ) {
1540 $status->value = []; // no container
1541 } else {
1542 $this->onError( $status, __METHOD__, $params, $rerr, $rcode, $rdesc );
1543 }
1544
1545 return $status;
1546 }
1547
1548 protected function doPrimeContainerCache( array $containerInfo ) {
1549 foreach ( $containerInfo as $container => $info ) {
1550 $this->containerStatCache->set( $container, 'stat', $info );
1551 }
1552 }
1553
1554 protected function doGetFileStatMulti( array $params ) {
1555 $stats = [];
1556
1557 $auth = $this->getAuthentication();
1558
1559 $reqs = [];
1560 foreach ( $params['srcs'] as $path ) {
1561 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
1562 if ( $srcRel === null ) {
1563 $stats[$path] = false;
1564 continue; // invalid storage path
1565 } elseif ( !$auth ) {
1566 $stats[$path] = null;
1567 continue;
1568 }
1569
1570 // (a) Check the container
1571 $cstat = $this->getContainerStat( $srcCont );
1572 if ( $cstat === false ) {
1573 $stats[$path] = false;
1574 continue; // ok, nothing to do
1575 } elseif ( !is_array( $cstat ) ) {
1576 $stats[$path] = null;
1577 continue;
1578 }
1579
1580 $reqs[$path] = [
1581 'method' => 'HEAD',
1582 'url' => $this->storageUrl( $auth, $srcCont, $srcRel ),
1583 'headers' => $this->authTokenHeaders( $auth ) + $this->headersFromParams( $params )
1584 ];
1585 }
1586
1587 $opts = [ 'maxConnsPerHost' => $params['concurrency'] ];
1588 $reqs = $this->http->runMulti( $reqs, $opts );
1589
1590 foreach ( $params['srcs'] as $path ) {
1591 if ( array_key_exists( $path, $stats ) ) {
1592 continue; // some sort of failure above
1593 }
1594 // (b) Check the file
1595 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $reqs[$path]['response'];
1596 if ( $rcode === 200 || $rcode === 204 ) {
1597 // Update the object if it is missing some headers
1598 $rhdrs = $this->addMissingMetadata( $rhdrs, $path );
1599 // Load the stat array from the headers
1600 $stat = $this->getStatFromHeaders( $rhdrs );
1601 if ( $this->isRGW ) {
1602 $stat['latest'] = true; // strong consistency
1603 }
1604 } elseif ( $rcode === 404 ) {
1605 $stat = false;
1606 } else {
1607 $stat = null;
1608 $this->onError( null, __METHOD__, $params, $rerr, $rcode, $rdesc );
1609 }
1610 $stats[$path] = $stat;
1611 }
1612
1613 return $stats;
1614 }
1615
1620 protected function getStatFromHeaders( array $rhdrs ) {
1621 // Fetch all of the custom metadata headers
1622 $metadata = $this->getMetadata( $rhdrs );
1623 // Fetch all of the custom raw HTTP headers
1624 $headers = $this->sanitizeHdrs( [ 'headers' => $rhdrs ] );
1625
1626 return [
1627 // Convert various random Swift dates to TS_MW
1628 'mtime' => $this->convertSwiftDate( $rhdrs['last-modified'], TS_MW ),
1629 // Empty objects actually return no content-length header in Ceph
1630 'size' => isset( $rhdrs['content-length'] ) ? (int)$rhdrs['content-length'] : 0,
1631 'sha1' => isset( $metadata['sha1base36'] ) ? $metadata['sha1base36'] : null,
1632 // Note: manifiest ETags are not an MD5 of the file
1633 'md5' => ctype_xdigit( $rhdrs['etag'] ) ? $rhdrs['etag'] : null,
1634 'xattr' => [ 'metadata' => $metadata, 'headers' => $headers ]
1635 ];
1636 }
1637
1641 protected function getAuthentication() {
1642 if ( $this->authErrorTimestamp !== null ) {
1643 if ( ( time() - $this->authErrorTimestamp ) < 60 ) {
1644 return null; // failed last attempt; don't bother
1645 } else { // actually retry this time
1646 $this->authErrorTimestamp = null;
1647 }
1648 }
1649 // Session keys expire after a while, so we renew them periodically
1650 $reAuth = ( ( time() - $this->authSessionTimestamp ) > $this->authTTL );
1651 // Authenticate with proxy and get a session key...
1652 if ( !$this->authCreds || $reAuth ) {
1653 $this->authSessionTimestamp = 0;
1654 $cacheKey = $this->getCredsCacheKey( $this->swiftUser );
1655 $creds = $this->srvCache->get( $cacheKey ); // credentials
1656 // Try to use the credential cache
1657 if ( isset( $creds['auth_token'] ) && isset( $creds['storage_url'] ) ) {
1658 $this->authCreds = $creds;
1659 // Skew the timestamp for worst case to avoid using stale credentials
1660 $this->authSessionTimestamp = time() - ceil( $this->authTTL / 2 );
1661 } else { // cache miss
1662 list( $rcode, $rdesc, $rhdrs, $rbody, $rerr ) = $this->http->run( [
1663 'method' => 'GET',
1664 'url' => "{$this->swiftAuthUrl}/v1.0",
1665 'headers' => [
1666 'x-auth-user' => $this->swiftUser,
1667 'x-auth-key' => $this->swiftKey
1668 ]
1669 ] );
1670
1671 if ( $rcode >= 200 && $rcode <= 299 ) { // OK
1672 $this->authCreds = [
1673 'auth_token' => $rhdrs['x-auth-token'],
1674 'storage_url' => ( $this->swiftStorageUrl !== null )
1675 ? $this->swiftStorageUrl
1676 : $rhdrs['x-storage-url']
1677 ];
1678
1679 $this->srvCache->set( $cacheKey, $this->authCreds, ceil( $this->authTTL / 2 ) );
1680 $this->authSessionTimestamp = time();
1681 } elseif ( $rcode === 401 ) {
1682 $this->onError( null, __METHOD__, [], "Authentication failed.", $rcode );
1683 $this->authErrorTimestamp = time();
1684
1685 return null;
1686 } else {
1687 $this->onError( null, __METHOD__, [], "HTTP return code: $rcode", $rcode );
1688 $this->authErrorTimestamp = time();
1689
1690 return null;
1691 }
1692 }
1693 // Ceph RGW does not use <account> in URLs (OpenStack Swift uses "/v1/<account>")
1694 if ( substr( $this->authCreds['storage_url'], -3 ) === '/v1' ) {
1695 $this->isRGW = true; // take advantage of strong consistency in Ceph
1696 }
1697 }
1698
1699 return $this->authCreds;
1700 }
1701
1708 protected function storageUrl( array $creds, $container = null, $object = null ) {
1709 $parts = [ $creds['storage_url'] ];
1710 if ( strlen( $container ) ) {
1711 $parts[] = rawurlencode( $container );
1712 }
1713 if ( strlen( $object ) ) {
1714 $parts[] = str_replace( "%2F", "/", rawurlencode( $object ) );
1715 }
1716
1717 return implode( '/', $parts );
1718 }
1719
1724 protected function authTokenHeaders( array $creds ) {
1725 return [ 'x-auth-token' => $creds['auth_token'] ];
1726 }
1727
1734 private function getCredsCacheKey( $username ) {
1735 return 'swiftcredentials:' . md5( $username . ':' . $this->swiftAuthUrl );
1736 }
1737
1749 public function onError( $status, $func, array $params, $err = '', $code = 0, $desc = '' ) {
1750 if ( $status instanceof StatusValue ) {
1751 $status->fatal( 'backend-fail-internal', $this->name );
1752 }
1753 if ( $code == 401 ) { // possibly a stale token
1754 $this->srvCache->delete( $this->getCredsCacheKey( $this->swiftUser ) );
1755 }
1756 $this->logger->error(
1757 "HTTP $code ($desc) in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
1758 ( $err ? ": $err" : "" )
1759 );
1760 }
1761}
1762
1768 public $httpOp;
1771
1777 public function __construct( SwiftFileBackend $backend, Closure $callback, array $httpOp ) {
1778 $this->backend = $backend;
1779 $this->callback = $callback;
1780 $this->httpOp = $httpOp;
1781 }
1782}
1783
1791abstract class SwiftFileBackendList implements Iterator {
1793 protected $bufferIter = [];
1794
1796 protected $bufferAfter = null;
1797
1799 protected $pos = 0;
1800
1802 protected $params = [];
1803
1805 protected $backend;
1806
1808 protected $container;
1809
1811 protected $dir;
1812
1814 protected $suffixStart;
1815
1816 const PAGE_SIZE = 9000; // file listing buffer size
1817
1824 public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
1825 $this->backend = $backend;
1826 $this->container = $fullCont;
1827 $this->dir = $dir;
1828 if ( substr( $this->dir, -1 ) === '/' ) {
1829 $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
1830 }
1831 if ( $this->dir == '' ) { // whole container
1832 $this->suffixStart = 0;
1833 } else { // dir within container
1834 $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
1835 }
1836 $this->params = $params;
1837 }
1838
1843 public function key() {
1844 return $this->pos;
1845 }
1846
1850 public function next() {
1851 // Advance to the next file in the page
1852 next( $this->bufferIter );
1853 ++$this->pos;
1854 // Check if there are no files left in this page and
1855 // advance to the next page if this page was not empty.
1856 if ( !$this->valid() && count( $this->bufferIter ) ) {
1857 $this->bufferIter = $this->pageFromList(
1858 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
1859 ); // updates $this->bufferAfter
1860 }
1861 }
1862
1866 public function rewind() {
1867 $this->pos = 0;
1868 $this->bufferAfter = null;
1869 $this->bufferIter = $this->pageFromList(
1870 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
1871 ); // updates $this->bufferAfter
1872 }
1873
1878 public function valid() {
1879 if ( $this->bufferIter === null ) {
1880 return false; // some failure?
1881 } else {
1882 return ( current( $this->bufferIter ) !== false ); // no paths can have this value
1883 }
1884 }
1885
1896 abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
1897}
1898
1907 public function current() {
1908 return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
1909 }
1910
1911 protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
1912 return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
1913 }
1914}
1915
1924 public function current() {
1925 list( $path, $stat ) = current( $this->bufferIter );
1926 $relPath = substr( $path, $this->suffixStart );
1927 if ( is_array( $stat ) ) {
1928 $storageDir = rtrim( $this->params['dir'], '/' );
1929 $this->backend->loadListingStatInternal( "$storageDir/$relPath", $stat );
1930 }
1931
1932 return $relPath;
1933 }
1934
1935 protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
1936 return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
1937 }
1938}
Apache License January http
$dir
Definition Autoload.php:8
interface is intended to be more or less compatible with the PHP memcached client.
Definition BagOStuff.php:47
A BagOStuff object with no objects in it.
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.
setContainerCache( $container, array $val)
Set the cached info for a container.
executeOpHandlesInternal(array $fileOpHandles)
Execute a list of FileBackendStoreOpHandle handles in parallel.
getFileStat(array $params)
Get quick information about a file at a storage path in the backend.
resolveStoragePathReal( $storagePath)
Like resolveStoragePath() except null values are returned if the container is sharded and the shard c...
clearCache(array $paths=null)
Invalidate any in-process file stat and property cache.
primeContainerCache(array $items)
Do a batch lookup from cache for container stats for all containers used in a list of container names...
deleteFileCache( $path)
Delete the cached stat info for a file path.
getContentType( $storagePath, $content, $fsPath)
Get the content type to use in HEAD/GET requests for a file.
fileExists(array $params)
Check if a file exists at a storage path in the backend.
getLocalCopy(array $params)
Get a local copy on disk of the file at a storage path in the backend.
string $name
Unique backend name.
const ATTR_UNICODE_PATHS
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
getScopedFileLocks(array $paths, $type, StatusValue $status, $timeout=0)
Lock the files at the given storage paths in the backend.
newStatus()
Yields the result of the status wrapper callback on either:
scopedProfileSection( $section)
const ATTR_METADATA
const ATTR_HEADERS
Bitfield flags for supported features.
Library for creating and parsing MW-style timestamps.
Class to handle concurrent HTTP requests.
Handles per process caching of items.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
const STREAM_HEADLESS
static send404Message( $fname, $flags=0)
Send out a standard 404 message for a file.
Iterator for listing directories.
pageFromList( $container, $dir, &$after, $limit, array $params)
Get the given list portion (page)
Iterator for listing regular files.
pageFromList( $container, $dir, &$after, $limit, array $params)
Get the given list portion (page)
SwiftFileBackend helper class to page through listings.
string $dir
Storage directory.
string $container
Container name.
__construct(SwiftFileBackend $backend, $fullCont, $dir, array $params)
string $bufferAfter
List items after this path.
pageFromList( $container, $dir, &$after, $limit, array $params)
Get the given list portion (page)
array $bufferIter
List of path or (path,stat array) entries.
SwiftFileBackend $backend
Class for an OpenStack Swift (or Ceph RGW) based file backend.
string $swiftUser
Swift user (account:user) to authenticate as.
sanitizeHdrs(array $params)
Sanitize and filter the custom headers from a $params array.
string $swiftAuthUrl
Authentication base URL (without version)
string $swiftTempUrlKey
Shared secret value for making temp URLs.
isPathUsableInternal( $storagePath)
Check if a file can be created or changed at a given storage path.
getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params)
Do not call this function outside of SwiftFileBackendFileList.
doPublishInternal( $fullCont, $dir, array $params)
doCreateInternal(array $params)
doGetFileStatMulti(array $params)
Get file stat information (concurrently if possible) for several files.
doGetFileSha1base36(array $params)
__construct(array $config)
MultiHttpClient $http
doGetFileXAttributes(array $params)
onError( $status, $func, array $params, $err='', $code=0, $desc='')
Log an unexpected exception for this backend.
buildFileObjectListing(array $params, $dir, array $objects)
Build a list of file objects, filtering out any directories and extracting any stat info if provided ...
authTokenHeaders(array $creds)
getStatFromHeaders(array $rhdrs)
string $swiftStorageUrl
Override of storage base URL.
createContainer( $container, array $params)
Create a Swift container.
doCopyInternal(array $params)
getCredsCacheKey( $username)
Get the cache key for a container.
getDirectoryListInternal( $fullCont, $dir, array $params)
string $rgwS3AccessKey
S3 access key (RADOS Gateway)
getFileHttpUrl(array $params)
getCustomHeaders(array $rawHeaders)
int $authTTL
TTL in seconds.
headersFromParams(array $params)
Get headers to send to Swift when reading a file based on a FileBackend params array,...
getMetadata(array $rawHeaders)
bool $isRGW
Whether the server is an Ceph RGW.
addMissingMetadata(array $objHdrs, $path)
Fill in any missing object metadata and save it to Swift.
doStoreInternal(array $params)
int $authErrorTimestamp
UNIX timestamp.
getMetadataHeaders(array $rawHeaders)
int $authSessionTimestamp
UNIX timestamp.
loadListingStatInternal( $path, array $val)
Do not call this function outside of SwiftFileBackendFileList.
doPrepareInternal( $fullCont, $dir, array $params)
doSecureInternal( $fullCont, $dir, array $params)
getFileListInternal( $fullCont, $dir, array $params)
doMoveInternal(array $params)
getFeatures()
Get the a bitfield of extra features supported by the backend medium.
deleteContainer( $container, array $params)
Delete a Swift container.
setContainerAccess( $container, array $readGrps, array $writeGrps)
Set read/write permissions for a Swift container.
doGetFileStat(array $params)
doGetLocalCopyMulti(array $params)
string $rgwS3SecretKey
S3 authentication key (RADOS Gateway)
doGetFileContentsMulti(array $params)
storageUrl(array $creds, $container=null, $object=null)
convertSwiftDate( $ts, $format=TS_MW)
Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT"/"2013-05-11T07:37:27.678360Z".
doStreamFile(array $params)
doPrimeContainerCache(array $containerInfo)
Fill the backend-specific process cache given an array of resolved container names and their correspo...
resolveContainerPath( $container, $relStoragePath)
Resolve a relative storage path, checking if it's allowed by the backend.
doCleanInternal( $fullCont, $dir, array $params)
ProcessCacheLRU $containerStatCache
Container stat cache.
getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params)
Do not call this function outside of SwiftFileBackendFileList.
string $swiftKey
Secret key for user.
doDirectoryExists( $fullCont, $dir, array $params)
objectListing( $fullCont, $type, $limit, $after=null, $prefix=null, $delim=null)
Get a list of objects under a container.
directoriesAreVirtual()
Is this a key/value store where directories are just virtual? Virtual directories exists in so much a...
doExecuteOpHandlesInternal(array $fileOpHandles)
doDeleteInternal(array $params)
doDescribeInternal(array $params)
getContainerStat( $container, $bypassCache=false)
Get a Swift container stat array, possibly from process cache.
array $httpOp
List of Requests for MultiHttpClient.
__construct(SwiftFileBackend $backend, Closure $callback, array $httpOp)
static factory( $prefix, $extension='', $tmpDirectory=null)
Make a new temporary file on the file system.
Multi-datacenter aware caching interface.
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
this hook is for auditing only $req
Definition hooks.txt:988
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition hooks.txt:2775
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 & $code
Definition hooks.txt:863
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2805
this hook is for auditing only or null if authentication failed before getting that far $username
Definition hooks.txt:783
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). '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:1049
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:901
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition hooks.txt:1610
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
returning false will NOT prevent logging $e
Definition hooks.txt:2146
$params
$header