60 private $dbLoadBalancer;
65 private $extStoreAccess;
85 private $compressBlobs =
false;
90 private $legacyEncoding =
false;
95 private $useExternalStore =
false;
114 $this->dbLoadBalancer = $dbLoadBalancer;
115 $this->extStoreAccess = $extStoreAccess;
116 $this->cache = $cache;
117 $this->dbDomain = $dbDomain;
124 return $this->cacheExpiry;
131 $this->cacheExpiry = $cacheExpiry;
138 return $this->compressBlobs;
145 $this->compressBlobs = $compressBlobs;
153 return $this->legacyEncoding;
165 $this->legacyEncoding = $legacyEncoding;
172 return $this->useExternalStore;
179 $this->useExternalStore = $useExternalStore;
185 private function getDBLoadBalancer() {
186 return $this->dbLoadBalancer;
194 private function getDBConnection( $index ) {
195 $lb = $this->getDBLoadBalancer();
196 return $lb->getConnection( $index, [], $this->dbDomain );
212 # Write to external storage if required
213 if ( $this->useExternalStore ) {
216 $data = $this->extStoreAccess->insert( $data, [
'domain' => $this->dbDomain ] );
224 return 'es:' . $data .
'?flags=' . $flags;
226 return 'es:' . $data;
231 $dbw->newInsertQueryBuilder()
232 ->insertInto(
'text' )
233 ->row( [
'old_text' => $data,
'old_flags' => $flags ] )
234 ->caller( __METHOD__ )->execute();
236 $textId = $dbw->insertId();
254 public function getBlob( $blobAddress, $queryFlags = 0 ) {
255 Assert::parameterType(
'string', $blobAddress,
'$blobAddress' );
258 $blob = $this->cache->getWithSetCallback(
260 $this->getCacheTTL(),
261 function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags, &$error ) {
263 [ $result, $errors ] = $this->fetchBlobs( [ $blobAddress ], $queryFlags );
265 $error = $errors[$blobAddress] ??
null;
267 $ttl = WANObjectCache::TTL_UNCACHEABLE;
269 return $result[$blobAddress];
271 $this->getCacheOptions()
275 if ( $error[0] ===
'badrevision' ) {
282 Assert::postcondition( is_string( $blob ),
'Blob must not be null' );
302 [ $blobsByAddress, $errors ] = $this->fetchBlobs( $blobAddresses, $queryFlags );
304 $blobsByAddress = array_map(
static function ( $blob ) {
305 return $blob ===
false ? null : $blob;
306 }, $blobsByAddress );
308 $result = StatusValue::newGood( $blobsByAddress );
309 foreach ( $errors as $error ) {
311 $result->warning( ...$error );
330 private function fetchBlobs( $blobAddresses, $queryFlags ) {
331 $textIdToBlobAddress = [];
334 foreach ( $blobAddresses as $blobAddress ) {
337 }
catch ( InvalidArgumentException $ex ) {
338 throw new BlobAccessException(
339 $ex->getMessage() .
'. Use findBadBlobs.php to remedy.',
345 if ( $schema ===
'es' ) {
349 $blob = $this->
expandBlob( $id,
'external', $blobAddress );
352 if ( $blob ===
false ) {
353 $errors[$blobAddress] = [
355 "Bad data in external store address $id. Use findBadBlobs.php to remedy."
358 $result[$blobAddress] = $blob;
359 } elseif ( $schema ===
'bad' ) {
363 .
": loading known-bad content ($blobAddress), returning empty string"
365 $result[$blobAddress] =
'';
366 $errors[$blobAddress] = [
368 'The content of this revision is missing or corrupted (bad schema)'
370 } elseif ( $schema ===
'tt' ) {
371 $textId = intval( $id );
373 if ( $textId < 1 || $id !== (
string)$textId ) {
374 $errors[$blobAddress] = [
376 "Bad blob address: $blobAddress. Use findBadBlobs.php to remedy."
378 $result[$blobAddress] =
false;
381 $textIdToBlobAddress[$textId] = $blobAddress;
383 $errors[$blobAddress] = [
385 "Unknown blob address schema: $schema. Use findBadBlobs.php to remedy."
387 $result[$blobAddress] =
false;
391 $textIds = array_keys( $textIdToBlobAddress );
393 return [ $result, $errors ];
397 $queryFlags |= DBAccessObjectUtils::hasFlags( $queryFlags, IDBAccessObject::READ_LATEST )
398 ? IDBAccessObject::READ_LATEST_IMMUTABLE
400 [ $index, $options, $fallbackIndex, $fallbackOptions ] =
401 self::getDBOptions( $queryFlags );
403 $dbConnection = $this->getDBConnection( $index );
404 $rows = $dbConnection->newSelectQueryBuilder()
405 ->select( [
'old_id',
'old_text',
'old_flags' ] )
407 ->where( [
'old_id' => $textIds ] )
408 ->options( $options )
409 ->caller( __METHOD__ )->fetchResultSet();
410 $numRows = $rows->numRows();
414 if ( $numRows !== count( $textIds ) && $fallbackIndex !==
null ) {
415 $fetchedTextIds = [];
416 foreach ( $rows as $row ) {
417 $fetchedTextIds[] = $row->old_id;
419 $missingTextIds = array_diff( $textIds, $fetchedTextIds );
420 $dbConnection = $this->getDBConnection( $fallbackIndex );
421 $rowsFromFallback = $dbConnection->newSelectQueryBuilder()
422 ->select( [
'old_id',
'old_text',
'old_flags' ] )
424 ->where( [
'old_id' => $missingTextIds ] )
425 ->options( $fallbackOptions )
426 ->caller( __METHOD__ )->fetchResultSet();
427 $appendIterator =
new AppendIterator();
428 $appendIterator->append( $rows );
429 $appendIterator->append( $rowsFromFallback );
430 $rows = $appendIterator;
433 foreach ( $rows as $row ) {
434 $blobAddress = $textIdToBlobAddress[$row->old_id];
436 if ( $row->old_text !==
null ) {
437 $blob = $this->
expandBlob( $row->old_text, $row->old_flags, $blobAddress );
439 if ( $blob ===
false ) {
440 $errors[$blobAddress] = [
442 "Bad data in text row {$row->old_id}. Use findBadBlobs.php to remedy."
445 $result[$blobAddress] = $blob;
449 if ( count( $result ) !== count( $blobAddresses ) ) {
450 foreach ( $blobAddresses as $blobAddress ) {
451 if ( !isset( $result[$blobAddress ] ) ) {
452 $errors[$blobAddress] = [
454 "Unable to fetch blob at $blobAddress. Use findBadBlobs.php to remedy."
456 $result[$blobAddress] =
false;
460 return [ $result, $errors ];
463 private static function getDBOptions( $bitfield ) {
464 if ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LATEST_IMMUTABLE ) ) {
467 } elseif ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LATEST ) ) {
469 $fallbackIndex =
null;
472 $fallbackIndex =
null;
475 $lockingOptions = [];
476 if ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_EXCLUSIVE ) ) {
477 $lockingOptions[] =
'FOR UPDATE';
478 } elseif ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LOCKING ) ) {
479 $lockingOptions[] =
'LOCK IN SHARE MODE';
482 if ( $fallbackIndex !==
null ) {
484 $fallbackOptions = $lockingOptions;
486 $options = $lockingOptions;
487 $fallbackOptions = [];
490 return [ $index, $options, $fallbackIndex, $fallbackOptions ];
504 return $this->cache->makeGlobalKey(
506 $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ),
516 private function getCacheOptions() {
519 'pcTTL' => WANObjectCache::TTL_PROC_LONG,
520 'segmentable' => true
544 public function expandBlob( $raw, $flags, $blobAddress =
null ) {
545 if ( is_string( $flags ) ) {
548 if ( in_array(
'error', $flags ) ) {
550 "The content of this revision is missing or corrupted (error flag)"
555 if ( in_array(
'external', $flags ) ) {
557 $parts = explode(
'://',
$url, 2 );
558 if ( count( $parts ) == 1 || $parts[1] ==
'' ) {
562 if ( $blobAddress ) {
564 return $this->cache->getWithSetCallback(
566 $this->getCacheTTL(),
567 function () use (
$url, $flags, $blobAddress ) {
569 $blob = $this->extStoreAccess
570 ->fetchFromURL(
$url, [
'domain' => $this->dbDomain ] );
572 return $blob ===
false ? false : $this->
decompressData( $blob, $flags, $blobAddress );
574 $this->getCacheOptions()
577 $blob = $this->extStoreAccess->fetchFromURL(
$url, [
'domain' => $this->dbDomain ] );
578 return $blob ===
false ? false : $this->
decompressData( $blob, $flags, $blobAddress );
607 $blobFlags[] =
'utf-8';
609 if ( $this->compressBlobs ) {
610 if ( function_exists(
'gzdeflate' ) ) {
611 $deflated = gzdeflate( $blob );
613 if ( $deflated ===
false ) {
617 $blobFlags[] =
'gzip';
620 wfDebug( __METHOD__ .
" -- no zlib support, not compressing" );
623 return implode(
',', $blobFlags );
642 public function decompressData(
string $blob, array $blobFlags, ?
string $blobAddress =
null ) {
643 if ( in_array(
'error', $blobFlags ) ) {
651 if ( in_array(
'gzip', $blobFlags ) ) {
654 $blob = @gzinflate( $blob );
655 if ( $blob ===
false ) {
656 wfWarn( __METHOD__ .
': gzinflate() failed' .
657 ( $blobAddress ?
' (at blob address ' . $blobAddress .
')' :
'' ) );
662 if ( in_array(
'object', $blobFlags ) ) {
663 # Generic compressed storage
669 $blob = $obj->getText();
673 if ( $blob !==
false && $this->legacyEncoding
674 && !in_array(
'utf-8', $blobFlags ) && !in_array(
'utf8', $blobFlags )
686 $blob = @iconv( $this->legacyEncoding,
'UTF-8//IGNORE', $blob );
699 private function getCacheTTL() {
700 $cache = $this->cache;
702 if ( $cache->
getQoS( $cache::ATTR_DURABILITY ) >= $cache::QOS_DURABILITY_RDBMS ) {
704 $ttl = $cache::TTL_UNCACHEABLE;
706 $ttl = $this->cacheExpiry ?: $cache::TTL_UNCACHEABLE;
735 if ( $schema !==
'tt' ) {
739 $textId = intval( $id );
741 if ( !$textId || $id !== (
string)$textId ) {
742 throw new InvalidArgumentException(
"Malformed text_id: $id" );
772 return $flagsString ===
'' ? [] : explode(
',', $flagsString );
785 if ( !preg_match(
'/^([-+.\w]+):([^\s?]+)(\?([^\s]*))?$/', $address, $m ) ) {
786 throw new InvalidArgumentException(
"Bad blob address: $address" );
789 $schema = strtolower( $m[1] );
793 return [ $schema, $id, $parameters ];
797 if ( $this->useExternalStore && $this->extStoreAccess->isReadOnly() ) {
801 return ( $this->getDBLoadBalancer()->getReadOnlyReason() !==
false );
static unserialize(string $str, bool $allowDouble=false)
Unserialize a HistoryBlob.