33use InvalidArgumentException;
37use Wikimedia\Assert\Assert;
38use Wikimedia\AtEase\AtEase;
59 private $dbLoadBalancer;
64 private $extStoreAccess;
79 private $cacheExpiry = 604800;
84 private $compressBlobs =
false;
89 private $legacyEncoding =
false;
94 private $useExternalStore =
false;
113 $this->dbLoadBalancer = $dbLoadBalancer;
114 $this->extStoreAccess = $extStoreAccess;
115 $this->cache = $cache;
116 $this->dbDomain = $dbDomain;
123 return $this->cacheExpiry;
130 $this->cacheExpiry = $cacheExpiry;
137 return $this->compressBlobs;
144 $this->compressBlobs = $compressBlobs;
152 return $this->legacyEncoding;
164 $this->legacyEncoding = $legacyEncoding;
171 return $this->useExternalStore;
178 $this->useExternalStore = $useExternalStore;
184 private function getDBLoadBalancer() {
185 return $this->dbLoadBalancer;
193 private function getDBConnection( $index ) {
194 $lb = $this->getDBLoadBalancer();
195 return $lb->getConnectionRef( $index, [], $this->dbDomain );
212 # Write to external storage if required
213 if ( $this->useExternalStore ) {
215 $data = $this->extStoreAccess->insert( $data, [
'domain' => $this->dbDomain ] );
222 $flags .=
'external';
233 [
'old_text' => $data,
'old_flags' => $flags ],
237 $textId = $dbw->insertId();
257 public function getBlob( $blobAddress, $queryFlags = 0 ) {
258 Assert::parameterType(
'string', $blobAddress,
'$blobAddress' );
261 $blob = $this->cache->getWithSetCallback(
262 $this->getCacheKey( $blobAddress ),
263 $this->getCacheTTL(),
264 function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags, &$error ) {
266 [ $result, $errors ] = $this->fetchBlobs( [ $blobAddress ], $queryFlags );
268 $error = $errors[$blobAddress] ??
null;
270 $ttl = WANObjectCache::TTL_UNCACHEABLE;
272 return $result[$blobAddress];
274 $this->getCacheOptions()
278 if ( $error[0] ===
'badrevision' ) {
285 Assert::postcondition( is_string(
$blob ),
'Blob must not be null' );
305 [ $blobsByAddress, $errors ] = $this->fetchBlobs( $blobAddresses, $queryFlags );
307 $blobsByAddress = array_map(
static function (
$blob ) {
309 }, $blobsByAddress );
311 $result = StatusValue::newGood( $blobsByAddress );
312 foreach ( $errors as $error ) {
314 $result->warning( ...$error );
333 private function fetchBlobs( $blobAddresses, $queryFlags ) {
334 $textIdToBlobAddress = [];
337 foreach ( $blobAddresses as $blobAddress ) {
340 }
catch ( InvalidArgumentException $ex ) {
341 throw new BlobAccessException(
342 $ex->getMessage() .
'. Use findBadBlobs.php to remedy.',
349 if ( $schema ===
'bad' ) {
353 .
": loading known-bad content ($blobAddress), returning empty string"
355 $result[$blobAddress] =
'';
356 $errors[$blobAddress] = [
358 'The content of this revision is missing or corrupted (bad schema)'
360 } elseif ( $schema ===
'tt' ) {
361 $textId = intval( $id );
363 if ( $textId < 1 || $id !== (
string)$textId ) {
364 $errors[$blobAddress] = [
366 "Bad blob address: $blobAddress. Use findBadBlobs.php to remedy."
368 $result[$blobAddress] =
false;
371 $textIdToBlobAddress[$textId] = $blobAddress;
373 $errors[$blobAddress] = [
375 "Unknown blob address schema: $schema. Use findBadBlobs.php to remedy."
377 $result[$blobAddress] =
false;
381 $textIds = array_keys( $textIdToBlobAddress );
383 return [ $result, $errors ];
388 ? self::READ_LATEST_IMMUTABLE
390 [ $index, $options, $fallbackIndex, $fallbackOptions ] =
393 $dbConnection = $this->getDBConnection( $index );
394 $rows = $dbConnection->select(
396 [
'old_id',
'old_text',
'old_flags' ],
397 [
'old_id' => $textIds ],
402 if ( $rows instanceof IResultWrapper ) {
403 $numRows = $rows->numRows();
408 if ( $numRows !== count( $textIds ) && $fallbackIndex !==
null ) {
409 $fetchedTextIds = [];
410 foreach ( $rows as $row ) {
411 $fetchedTextIds[] = $row->old_id;
413 $missingTextIds = array_diff( $textIds, $fetchedTextIds );
414 $dbConnection = $this->getDBConnection( $fallbackIndex );
415 $rowsFromFallback = $dbConnection->select(
417 [
'old_id',
'old_text',
'old_flags' ],
418 [
'old_id' => $missingTextIds ],
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 ];
468 private function getCacheKey( $blobAddress ) {
469 return $this->cache->makeGlobalKey(
471 $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ),
481 private function getCacheOptions() {
484 'pcTTL' => WANObjectCache::TTL_PROC_LONG,
485 'segmentable' =>
true
509 public function expandBlob( $raw, $flags, $blobAddress =
null ) {
510 if ( is_string( $flags ) ) {
513 if ( in_array(
'error', $flags ) ) {
515 "The content of this revision is missing or corrupted (error flag)"
520 if ( in_array(
'external', $flags ) ) {
522 $parts = explode(
'://', $url, 2 );
523 if ( count( $parts ) == 1 || $parts[1] ==
'' ) {
527 if ( $blobAddress ) {
529 return $this->cache->getWithSetCallback(
530 $this->getCacheKey( $blobAddress ),
531 $this->getCacheTTL(),
532 function () use ( $url, $flags ) {
534 $blob = $this->extStoreAccess
535 ->fetchFromURL( $url, [
'domain' => $this->dbDomain ] );
539 $this->getCacheOptions()
542 $blob = $this->extStoreAccess->fetchFromURL( $url, [
'domain' => $this->dbDomain ] );
572 $blobFlags[] =
'utf-8';
574 if ( $this->compressBlobs ) {
575 if ( function_exists(
'gzdeflate' ) ) {
576 $deflated = gzdeflate(
$blob );
578 if ( $deflated ===
false ) {
582 $blobFlags[] =
'gzip';
585 wfDebug( __METHOD__ .
" -- no zlib support, not compressing" );
588 return implode(
',', $blobFlags );
607 if ( in_array(
'error', $blobFlags ) ) {
612 if ( in_array(
'gzip', $blobFlags ) ) {
613 # Deal with optional compression of archived pages.
614 # This can be done periodically via maintenance/compressOld.php, and
615 # as pages are saved if $wgCompressRevisions is set.
618 if (
$blob ===
false ) {
619 wfWarn( __METHOD__ .
': gzinflate() failed' );
624 if ( in_array(
'object', $blobFlags ) ) {
625 # Generic compressed storage
631 $blob = $obj->getText();
635 if (
$blob !==
false && $this->legacyEncoding
636 && !in_array(
'utf-8', $blobFlags ) && !in_array(
'utf8', $blobFlags )
638 # Old revisions kept around in a legacy encoding?
639 # Upconvert on demand.
640 # ("utf8" checked for compatibility with some broken
641 # conversion scripts 2008-12-30)
643 # *input* string. We just ignore those too.
646 AtEase::suppressWarnings();
647 $blob = iconv( $this->legacyEncoding,
'UTF-8//IGNORE',
$blob );
648 AtEase::restoreWarnings();
661 private function getCacheTTL() {
662 $cache = $this->cache;
664 if ( $cache->
getQoS( $cache::ATTR_DURABILITY ) >= $cache::QOS_DURABILITY_RDBMS ) {
666 $ttl = $cache::TTL_UNCACHEABLE;
668 $ttl = $this->cacheExpiry ?: $cache::TTL_UNCACHEABLE;
697 if ( $schema !==
'tt' ) {
701 $textId = intval( $id );
703 if ( !$textId || $id !== (
string)$textId ) {
704 throw new InvalidArgumentException(
"Malformed text_id: $id" );
734 return $flagsString ===
'' ? [] : explode(
',', $flagsString );
748 if ( !preg_match(
'/^([-+.\w]+):([^\s?]+)(\?([^\s]*))?$/', $address, $m ) ) {
749 throw new InvalidArgumentException(
"Bad blob address: $address" );
752 $schema = strtolower( $m[1] );
756 return [ $schema, $id, $parameters ];
760 if ( $this->useExternalStore && $this->extStoreAccess->isReadOnly() ) {
764 return ( $this->getDBLoadBalancer()->getReadOnlyReason() !==
false );
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfCgiToArray( $query)
This is the logical opposite of wfArrayToCgi(): it accepts a query string as its argument and returns...
Helper class for DAO classes.
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
static hasFlags( $bitfield, $flags)
This is the main interface for fetching or inserting objects with ExternalStore.
static unserialize(string $str, bool $allowDouble=false)
Unserialize a HistoryBlob.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Multi-datacenter aware caching interface.
Interface for database access objects.