62 private $dbLoadBalancer;
67 private $extStoreAccess;
87 private $compressBlobs =
false;
92 private $legacyEncoding =
false;
97 private $useExternalStore =
false;
116 $this->dbLoadBalancer = $dbLoadBalancer;
117 $this->extStoreAccess = $extStoreAccess;
118 $this->cache = $cache;
119 $this->dbDomain = $dbDomain;
126 return $this->cacheExpiry;
133 $this->cacheExpiry = $cacheExpiry;
140 return $this->compressBlobs;
147 $this->compressBlobs = $compressBlobs;
155 return $this->legacyEncoding;
167 $this->legacyEncoding = $legacyEncoding;
174 return $this->useExternalStore;
181 $this->useExternalStore = $useExternalStore;
187 private function getDBLoadBalancer() {
188 return $this->dbLoadBalancer;
196 private function getDBConnection( $index ) {
197 $lb = $this->getDBLoadBalancer();
198 return $lb->getConnectionRef( $index, [], $this->dbDomain );
214 # Write to external storage if required
215 if ( $this->useExternalStore ) {
218 $data = $this->extStoreAccess->insert( $data, [
'domain' => $this->dbDomain ] );
228 $flags .=
'external';
237 $dbw->newInsertQueryBuilder()
238 ->insertInto(
'text' )
239 ->row( [
'old_text' => $data,
'old_flags' => $flags ] )
240 ->caller( __METHOD__ )->execute();
242 $textId = $dbw->insertId();
259 public function getBlob( $blobAddress, $queryFlags = 0 ) {
260 Assert::parameterType(
'string', $blobAddress,
'$blobAddress' );
263 $blob = $this->cache->getWithSetCallback(
265 $this->getCacheTTL(),
266 function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags, &$error ) {
268 [ $result, $errors ] = $this->fetchBlobs( [ $blobAddress ], $queryFlags );
270 $error = $errors[$blobAddress] ??
null;
272 $ttl = WANObjectCache::TTL_UNCACHEABLE;
274 return $result[$blobAddress];
276 $this->getCacheOptions()
280 if ( $error[0] ===
'badrevision' ) {
287 Assert::postcondition( is_string( $blob ),
'Blob must not be null' );
307 [ $blobsByAddress, $errors ] = $this->fetchBlobs( $blobAddresses, $queryFlags );
309 $blobsByAddress = array_map(
static function ( $blob ) {
310 return $blob ===
false ? null : $blob;
311 }, $blobsByAddress );
313 $result = StatusValue::newGood( $blobsByAddress );
314 foreach ( $errors as $error ) {
316 $result->warning( ...$error );
335 private function fetchBlobs( $blobAddresses, $queryFlags ) {
336 $textIdToBlobAddress = [];
339 foreach ( $blobAddresses as $blobAddress ) {
342 }
catch ( InvalidArgumentException $ex ) {
343 throw new BlobAccessException(
344 $ex->getMessage() .
'. Use findBadBlobs.php to remedy.',
351 if ( $schema ===
'bad' ) {
355 .
": loading known-bad content ($blobAddress), returning empty string"
357 $result[$blobAddress] =
'';
358 $errors[$blobAddress] = [
360 'The content of this revision is missing or corrupted (bad schema)'
362 } elseif ( $schema ===
'tt' ) {
363 $textId = intval( $id );
365 if ( $textId < 1 || $id !== (
string)$textId ) {
366 $errors[$blobAddress] = [
368 "Bad blob address: $blobAddress. Use findBadBlobs.php to remedy."
370 $result[$blobAddress] =
false;
373 $textIdToBlobAddress[$textId] = $blobAddress;
375 $errors[$blobAddress] = [
377 "Unknown blob address schema: $schema. Use findBadBlobs.php to remedy."
379 $result[$blobAddress] =
false;
383 $textIds = array_keys( $textIdToBlobAddress );
385 return [ $result, $errors ];
390 ? IDBAccessObject::READ_LATEST_IMMUTABLE
392 [ $index, $options, $fallbackIndex, $fallbackOptions ] =
393 self::getDBOptions( $queryFlags );
395 $dbConnection = $this->getDBConnection( $index );
396 $rows = $dbConnection->newSelectQueryBuilder()
397 ->select( [
'old_id',
'old_text',
'old_flags' ] )
399 ->where( [
'old_id' => $textIds ] )
400 ->options( $options )
401 ->caller( __METHOD__ )->fetchResultSet();
403 if ( $rows instanceof IResultWrapper ) {
404 $numRows = $rows->numRows();
409 if ( $numRows !== count( $textIds ) && $fallbackIndex !==
null ) {
410 $fetchedTextIds = [];
411 foreach ( $rows as $row ) {
412 $fetchedTextIds[] = $row->old_id;
414 $missingTextIds = array_diff( $textIds, $fetchedTextIds );
415 $dbConnection = $this->getDBConnection( $fallbackIndex );
416 $rowsFromFallback = $dbConnection->newSelectQueryBuilder()
417 ->select( [
'old_id',
'old_text',
'old_flags' ] )
419 ->where( [
'old_id' => $missingTextIds ] )
420 ->options( $fallbackOptions )
421 ->caller( __METHOD__ )->fetchResultSet();
422 $appendIterator =
new AppendIterator();
423 $appendIterator->append( $rows );
424 $appendIterator->append( $rowsFromFallback );
425 $rows = $appendIterator;
428 foreach ( $rows as $row ) {
429 $blobAddress = $textIdToBlobAddress[$row->old_id];
431 if ( $row->old_text !==
null ) {
432 $blob = $this->
expandBlob( $row->old_text, $row->old_flags, $blobAddress );
434 if ( $blob ===
false ) {
435 $errors[$blobAddress] = [
437 "Bad data in text row {$row->old_id}. Use findBadBlobs.php to remedy."
440 $result[$blobAddress] = $blob;
444 if ( count( $result ) !== count( $blobAddresses ) ) {
445 foreach ( $blobAddresses as $blobAddress ) {
446 if ( !isset( $result[$blobAddress ] ) ) {
447 $errors[$blobAddress] = [
449 "Unable to fetch blob at $blobAddress. Use findBadBlobs.php to remedy."
451 $result[$blobAddress] =
false;
455 return [ $result, $errors ];
458 private static function getDBOptions( $bitfield ) {
459 if ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LATEST_IMMUTABLE ) ) {
462 } elseif ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LATEST ) ) {
464 $fallbackIndex =
null;
467 $fallbackIndex =
null;
470 $lockingOptions = [];
471 if ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_EXCLUSIVE ) ) {
472 $lockingOptions[] =
'FOR UPDATE';
473 } elseif ( DBAccessObjectUtils::hasFlags( $bitfield, IDBAccessObject::READ_LOCKING ) ) {
474 $lockingOptions[] =
'LOCK IN SHARE MODE';
477 if ( $fallbackIndex !==
null ) {
479 $fallbackOptions = $lockingOptions;
481 $options = $lockingOptions;
482 $fallbackOptions = [];
485 return [ $index, $options, $fallbackIndex, $fallbackOptions ];
499 return $this->cache->makeGlobalKey(
501 $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ),
511 private function getCacheOptions() {
514 'pcTTL' => WANObjectCache::TTL_PROC_LONG,
515 'segmentable' => true
539 public function expandBlob( $raw, $flags, $blobAddress =
null ) {
540 if ( is_string( $flags ) ) {
543 if ( in_array(
'error', $flags ) ) {
545 "The content of this revision is missing or corrupted (error flag)"
550 if ( in_array(
'external', $flags ) ) {
552 $parts = explode(
'://', $url, 2 );
553 if ( count( $parts ) == 1 || $parts[1] ==
'' ) {
557 if ( $blobAddress ) {
559 return $this->cache->getWithSetCallback(
561 $this->getCacheTTL(),
562 function () use ( $url, $flags ) {
564 $blob = $this->extStoreAccess
565 ->fetchFromURL( $url, [
'domain' => $this->dbDomain ] );
567 return $blob ===
false ? false : $this->
decompressData( $blob, $flags );
569 $this->getCacheOptions()
572 $blob = $this->extStoreAccess->fetchFromURL( $url, [
'domain' => $this->dbDomain ] );
573 return $blob ===
false ? false : $this->
decompressData( $blob, $flags );
602 $blobFlags[] =
'utf-8';
604 if ( $this->compressBlobs ) {
605 if ( function_exists(
'gzdeflate' ) ) {
606 $deflated = gzdeflate( $blob );
608 if ( $deflated ===
false ) {
612 $blobFlags[] =
'gzip';
615 wfDebug( __METHOD__ .
" -- no zlib support, not compressing" );
618 return implode(
',', $blobFlags );
637 if ( in_array(
'error', $blobFlags ) ) {
642 if ( in_array(
'gzip', $blobFlags ) ) {
643 # Deal with optional compression of archived pages.
644 # This can be done periodically via maintenance/compressOld.php, and
645 # as pages are saved if $wgCompressRevisions is set.
646 $blob = gzinflate( $blob );
648 if ( $blob ===
false ) {
649 wfWarn( __METHOD__ .
': gzinflate() failed' );
654 if ( in_array(
'object', $blobFlags ) ) {
655 # Generic compressed storage
661 $blob = $obj->getText();
665 if ( $blob !==
false && $this->legacyEncoding
666 && !in_array(
'utf-8', $blobFlags ) && !in_array(
'utf8', $blobFlags )
668 # Old revisions kept around in a legacy encoding?
669 # Upconvert on demand.
670 # ("utf8" checked for compatibility with some broken
671 # conversion scripts 2008-12-30)
673 # *input* string. We just ignore those too.
676 AtEase::suppressWarnings();
677 $blob = iconv( $this->legacyEncoding,
'UTF-8//IGNORE', $blob );
678 AtEase::restoreWarnings();
691 private function getCacheTTL() {
692 $cache = $this->cache;
694 if ( $cache->
getQoS( $cache::ATTR_DURABILITY ) >= $cache::QOS_DURABILITY_RDBMS ) {
696 $ttl = $cache::TTL_UNCACHEABLE;
698 $ttl = $this->cacheExpiry ?: $cache::TTL_UNCACHEABLE;
727 if ( $schema !==
'tt' ) {
731 $textId = intval( $id );
733 if ( !$textId || $id !== (
string)$textId ) {
734 throw new InvalidArgumentException(
"Malformed text_id: $id" );
764 return $flagsString ===
'' ? [] : explode(
',', $flagsString );
778 if ( !preg_match(
'/^([-+.\w]+):([^\s?]+)(\?([^\s]*))?$/', $address, $m ) ) {
779 throw new InvalidArgumentException(
"Bad blob address: $address" );
782 $schema = strtolower( $m[1] );
786 return [ $schema, $id, $parameters ];
790 if ( $this->useExternalStore && $this->extStoreAccess->isReadOnly() ) {
794 return ( $this->getDBLoadBalancer()->getReadOnlyReason() !==
false );
static unserialize(string $str, bool $allowDouble=false)
Unserialize a HistoryBlob.