SqlBlobStore.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. <?php
  2. /**
  3. * Service for storing and loading data blobs representing revision content.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * Attribution notice: when this file was created, much of its content was taken
  21. * from the Revision.php file as present in release 1.30. Refer to the history
  22. * of that file for original authorship.
  23. *
  24. * @file
  25. */
  26. namespace MediaWiki\Storage;
  27. use AppendIterator;
  28. use DBAccessObjectUtils;
  29. use IDBAccessObject;
  30. use IExpiringStore;
  31. use InvalidArgumentException;
  32. use MWException;
  33. use StatusValue;
  34. use WANObjectCache;
  35. use ExternalStoreAccess;
  36. use Wikimedia\Assert\Assert;
  37. use Wikimedia\AtEase\AtEase;
  38. use Wikimedia\Rdbms\IDatabase;
  39. use Wikimedia\Rdbms\ILoadBalancer;
  40. /**
  41. * Service for storing and loading Content objects.
  42. *
  43. * @since 1.31
  44. *
  45. * @note This was written to act as a drop-in replacement for the corresponding
  46. * static methods in Revision.
  47. */
  48. class SqlBlobStore implements IDBAccessObject, BlobStore {
  49. // Note: the name has been taken unchanged from the Revision class.
  50. const TEXT_CACHE_GROUP = 'revisiontext:10';
  51. /**
  52. * @var ILoadBalancer
  53. */
  54. private $dbLoadBalancer;
  55. /**
  56. * @var ExternalStoreAccess
  57. */
  58. private $extStoreAccess;
  59. /**
  60. * @var WANObjectCache
  61. */
  62. private $cache;
  63. /**
  64. * @var string|bool DB domain ID of a wiki or false for the local one
  65. */
  66. private $dbDomain;
  67. /**
  68. * @var int
  69. */
  70. private $cacheExpiry = 604800; // 7 days
  71. /**
  72. * @var bool
  73. */
  74. private $compressBlobs = false;
  75. /**
  76. * @var bool|string
  77. */
  78. private $legacyEncoding = false;
  79. /**
  80. * @var boolean
  81. */
  82. private $useExternalStore = false;
  83. /**
  84. * @param ILoadBalancer $dbLoadBalancer A load balancer for acquiring database connections
  85. * @param ExternalStoreAccess $extStoreAccess Access layer for external storage
  86. * @param WANObjectCache $cache A cache manager for caching blobs. This can be the local
  87. * wiki's default instance even if $dbDomain refers to a different wiki, since
  88. * makeGlobalKey() is used to construct a key that allows cached blobs from the
  89. * same database to be re-used between wikis. For example, wiki A and wiki B will
  90. * use the same cache keys for blobs fetched from wiki C, regardless of the
  91. * wiki-specific default key space.
  92. * @param bool|string $dbDomain The ID of the target wiki database. Use false for the local wiki.
  93. */
  94. public function __construct(
  95. ILoadBalancer $dbLoadBalancer,
  96. ExternalStoreAccess $extStoreAccess,
  97. WANObjectCache $cache,
  98. $dbDomain = false
  99. ) {
  100. $this->dbLoadBalancer = $dbLoadBalancer;
  101. $this->extStoreAccess = $extStoreAccess;
  102. $this->cache = $cache;
  103. $this->dbDomain = $dbDomain;
  104. }
  105. /**
  106. * @return int time for which blobs can be cached, in seconds
  107. */
  108. public function getCacheExpiry() {
  109. return $this->cacheExpiry;
  110. }
  111. /**
  112. * @param int $cacheExpiry time for which blobs can be cached, in seconds
  113. */
  114. public function setCacheExpiry( $cacheExpiry ) {
  115. Assert::parameterType( 'integer', $cacheExpiry, '$cacheExpiry' );
  116. $this->cacheExpiry = $cacheExpiry;
  117. }
  118. /**
  119. * @return bool whether blobs should be compressed for storage
  120. */
  121. public function getCompressBlobs() {
  122. return $this->compressBlobs;
  123. }
  124. /**
  125. * @param bool $compressBlobs whether blobs should be compressed for storage
  126. */
  127. public function setCompressBlobs( $compressBlobs ) {
  128. $this->compressBlobs = $compressBlobs;
  129. }
  130. /**
  131. * @return false|string The legacy encoding to assume for blobs that are not marked as utf8.
  132. * False means handling of legacy encoding is disabled, and utf8 assumed.
  133. */
  134. public function getLegacyEncoding() {
  135. return $this->legacyEncoding;
  136. }
  137. /**
  138. * @deprecated since 1.34 No longer needed
  139. * @return null
  140. */
  141. public function getLegacyEncodingConversionLang() {
  142. wfDeprecated( __METHOD__ );
  143. return null;
  144. }
  145. /**
  146. * Set the legacy encoding to assume for blobs that do not have the utf-8 flag set.
  147. *
  148. * @note The second parameter, Language $language, was removed in 1.34.
  149. *
  150. * @param string $legacyEncoding The legacy encoding to assume for blobs that are
  151. * not marked as utf8.
  152. */
  153. public function setLegacyEncoding( $legacyEncoding ) {
  154. Assert::parameterType( 'string', $legacyEncoding, '$legacyEncoding' );
  155. $this->legacyEncoding = $legacyEncoding;
  156. }
  157. /**
  158. * @return bool Whether to use the ExternalStore mechanism for storing blobs.
  159. */
  160. public function getUseExternalStore() {
  161. return $this->useExternalStore;
  162. }
  163. /**
  164. * @param bool $useExternalStore Whether to use the ExternalStore mechanism for storing blobs.
  165. */
  166. public function setUseExternalStore( $useExternalStore ) {
  167. Assert::parameterType( 'boolean', $useExternalStore, '$useExternalStore' );
  168. $this->useExternalStore = $useExternalStore;
  169. }
  170. /**
  171. * @return ILoadBalancer
  172. */
  173. private function getDBLoadBalancer() {
  174. return $this->dbLoadBalancer;
  175. }
  176. /**
  177. * @param int $index A database index, like DB_MASTER or DB_REPLICA
  178. *
  179. * @return IDatabase
  180. */
  181. private function getDBConnection( $index ) {
  182. $lb = $this->getDBLoadBalancer();
  183. return $lb->getConnectionRef( $index, [], $this->dbDomain );
  184. }
  185. /**
  186. * Stores an arbitrary blob of data and returns an address that can be used with
  187. * getBlob() to retrieve the same blob of data,
  188. *
  189. * @param string $data
  190. * @param array $hints An array of hints.
  191. *
  192. * @throws BlobAccessException
  193. * @return string an address that can be used with getBlob() to retrieve the data.
  194. */
  195. public function storeBlob( $data, $hints = [] ) {
  196. try {
  197. $flags = $this->compressData( $data );
  198. # Write to external storage if required
  199. if ( $this->useExternalStore ) {
  200. // Store and get the URL
  201. $data = $this->extStoreAccess->insert( $data, [ 'domain' => $this->dbDomain ] );
  202. if ( !$data ) {
  203. throw new BlobAccessException( "Failed to store text to external storage" );
  204. }
  205. if ( $flags ) {
  206. $flags .= ',';
  207. }
  208. $flags .= 'external';
  209. // TODO: we could also return an address for the external store directly here.
  210. // That would mean bypassing the text table entirely when the external store is
  211. // used. We'll need to assess expected fallout before doing that.
  212. }
  213. $dbw = $this->getDBConnection( DB_MASTER );
  214. $old_id = $dbw->nextSequenceValue( 'text_old_id_seq' );
  215. $dbw->insert(
  216. 'text',
  217. [
  218. 'old_id' => $old_id,
  219. 'old_text' => $data,
  220. 'old_flags' => $flags,
  221. ],
  222. __METHOD__
  223. );
  224. $textId = $dbw->insertId();
  225. return self::makeAddressFromTextId( $textId );
  226. } catch ( MWException $e ) {
  227. throw new BlobAccessException( $e->getMessage(), 0, $e );
  228. }
  229. }
  230. /**
  231. * Retrieve a blob, given an address.
  232. * Currently hardcoded to the 'text' table storage engine.
  233. *
  234. * MCR migration note: this replaces Revision::loadText
  235. *
  236. * @param string $blobAddress
  237. * @param int $queryFlags
  238. *
  239. * @throws BlobAccessException
  240. * @return string
  241. */
  242. public function getBlob( $blobAddress, $queryFlags = 0 ) {
  243. Assert::parameterType( 'string', $blobAddress, '$blobAddress' );
  244. $error = null;
  245. $blob = $this->cache->getWithSetCallback(
  246. $this->getCacheKey( $blobAddress ),
  247. $this->getCacheTTL(),
  248. function ( $unused, &$ttl, &$setOpts ) use ( $blobAddress, $queryFlags, &$error ) {
  249. // Ignore $setOpts; blobs are immutable and negatives are not cached
  250. list( $result, $errors ) = $this->fetchBlobs( [ $blobAddress ], $queryFlags );
  251. // No negative caching; negative hits on text rows may be due to corrupted replica DBs
  252. $error = $errors[$blobAddress] ?? null;
  253. return $result[$blobAddress];
  254. },
  255. [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => IExpiringStore::TTL_PROC_LONG ]
  256. );
  257. if ( $error ) {
  258. throw new BlobAccessException( $error );
  259. }
  260. Assert::postcondition( is_string( $blob ), 'Blob must not be null' );
  261. return $blob;
  262. }
  263. /**
  264. * A batched version of BlobStore::getBlob.
  265. *
  266. * @param string[] $blobAddresses An array of blob addresses.
  267. * @param int $queryFlags See IDBAccessObject.
  268. * @throws BlobAccessException
  269. * @return StatusValue A status with a map of blobAddress => binary blob data or null
  270. * if fetching the blob has failed. Fetch failures errors are the
  271. * warnings in the status object.
  272. * @since 1.34
  273. */
  274. public function getBlobBatch( $blobAddresses, $queryFlags = 0 ) {
  275. $errors = null;
  276. $addressByCacheKey = $this->cache->makeMultiKeys(
  277. $blobAddresses,
  278. function ( $blobAddress ) {
  279. return $this->getCacheKey( $blobAddress );
  280. }
  281. );
  282. $blobsByCacheKey = $this->cache->getMultiWithUnionSetCallback(
  283. $addressByCacheKey,
  284. $this->getCacheTTL(),
  285. function ( array $blobAddresses, array &$ttls, array &$setOpts ) use ( $queryFlags, &$errors ) {
  286. // Ignore $setOpts; blobs are immutable and negatives are not cached
  287. list( $result, $errors ) = $this->fetchBlobs( $blobAddresses, $queryFlags );
  288. return $result;
  289. },
  290. [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => IExpiringStore::TTL_PROC_LONG ]
  291. );
  292. // Remap back to incoming blob addresses. The return value of the
  293. // WANObjectCache::getMultiWithUnionSetCallback is keyed on the internal
  294. // keys from WANObjectCache::makeMultiKeys, so we need to remap them
  295. // before returning to the client.
  296. $blobsByAddress = [];
  297. foreach ( $blobsByCacheKey as $cacheKey => $blob ) {
  298. $blobsByAddress[ $addressByCacheKey[ $cacheKey ] ] = $blob !== false ? $blob : null;
  299. }
  300. $result = StatusValue::newGood( $blobsByAddress );
  301. if ( $errors ) {
  302. foreach ( $errors as $error ) {
  303. $result->warning( 'internalerror', $error );
  304. }
  305. }
  306. return $result;
  307. }
  308. /**
  309. * MCR migration note: this corresponds to Revision::fetchText
  310. *
  311. * @param string[] $blobAddresses
  312. * @param int $queryFlags
  313. *
  314. * @throws BlobAccessException
  315. * @return array [ $result, $errors ] A map of blob addresses to successfully fetched blobs
  316. * or false if fetch failed, plus and array of errors
  317. */
  318. private function fetchBlobs( $blobAddresses, $queryFlags ) {
  319. $textIdToBlobAddress = [];
  320. $result = [];
  321. $errors = [];
  322. foreach ( $blobAddresses as $blobAddress ) {
  323. list( $schema, $id ) = self::splitBlobAddress( $blobAddress );
  324. //TODO: MCR: also support 'ex' schema with ExternalStore URLs, plus flags encoded in the URL!
  325. if ( $schema === 'tt' ) {
  326. $textId = intval( $id );
  327. $textIdToBlobAddress[$textId] = $blobAddress;
  328. } else {
  329. $errors[$blobAddress] = "Unknown blob address schema: $schema";
  330. $result[$blobAddress] = false;
  331. continue;
  332. }
  333. if ( !$textId || $id !== (string)$textId ) {
  334. $errors[$blobAddress] = "Bad blob address: $blobAddress";
  335. $result[$blobAddress] = false;
  336. }
  337. }
  338. $textIds = array_keys( $textIdToBlobAddress );
  339. if ( !$textIds ) {
  340. return [ $result, $errors ];
  341. }
  342. // Callers doing updates will pass in READ_LATEST as usual. Since the text/blob tables
  343. // do not normally get rows changed around, set READ_LATEST_IMMUTABLE in those cases.
  344. $queryFlags |= DBAccessObjectUtils::hasFlags( $queryFlags, self::READ_LATEST )
  345. ? self::READ_LATEST_IMMUTABLE
  346. : 0;
  347. list( $index, $options, $fallbackIndex, $fallbackOptions ) =
  348. DBAccessObjectUtils::getDBOptions( $queryFlags );
  349. // Text data is immutable; check replica DBs first.
  350. $dbConnection = $this->getDBConnection( $index );
  351. $rows = $dbConnection->select(
  352. 'text',
  353. [ 'old_id', 'old_text', 'old_flags' ],
  354. [ 'old_id' => $textIds ],
  355. __METHOD__,
  356. $options
  357. );
  358. // Fallback to DB_MASTER in some cases if not all the rows were found, using the appropriate
  359. // options, such as FOR UPDATE to avoid missing rows due to REPEATABLE-READ.
  360. if ( $dbConnection->numRows( $rows ) !== count( $textIds ) && $fallbackIndex !== null ) {
  361. $fetchedTextIds = [];
  362. foreach ( $rows as $row ) {
  363. $fetchedTextIds[] = $row->old_id;
  364. }
  365. $missingTextIds = array_diff( $textIds, $fetchedTextIds );
  366. $dbConnection = $this->getDBConnection( $fallbackIndex );
  367. $rowsFromFallback = $dbConnection->select(
  368. 'text',
  369. [ 'old_id', 'old_text', 'old_flags' ],
  370. [ 'old_id' => $missingTextIds ],
  371. __METHOD__,
  372. $fallbackOptions
  373. );
  374. $appendIterator = new AppendIterator();
  375. $appendIterator->append( $rows );
  376. $appendIterator->append( $rowsFromFallback );
  377. $rows = $appendIterator;
  378. }
  379. foreach ( $rows as $row ) {
  380. $blobAddress = $textIdToBlobAddress[$row->old_id];
  381. $blob = $this->expandBlob( $row->old_text, $row->old_flags, $blobAddress );
  382. if ( $blob === false ) {
  383. $errors[$blobAddress] = "Bad data in text row {$row->old_id}.";
  384. }
  385. $result[$blobAddress] = $blob;
  386. }
  387. // If we're still missing some of the rows, set errors for missing blobs.
  388. if ( count( $result ) !== count( $blobAddresses ) ) {
  389. foreach ( $blobAddresses as $blobAddress ) {
  390. if ( !isset( $result[$blobAddress ] ) ) {
  391. $errors[$blobAddress] = "Unable to fetch blob at $blobAddress";
  392. $result[$blobAddress] = false;
  393. }
  394. }
  395. }
  396. return [ $result, $errors ];
  397. }
  398. /**
  399. * Get a cache key for a given Blob address.
  400. *
  401. * The cache key is constructed in a way that allows cached blobs from the same database
  402. * to be re-used between wikis. For example, wiki A and wiki B will use the same cache keys
  403. * for blobs fetched from wiki C.
  404. *
  405. * @param string $blobAddress
  406. * @return string
  407. */
  408. private function getCacheKey( $blobAddress ) {
  409. return $this->cache->makeGlobalKey(
  410. 'SqlBlobStore-blob',
  411. $this->dbLoadBalancer->resolveDomainID( $this->dbDomain ),
  412. $blobAddress
  413. );
  414. }
  415. /**
  416. * Expand a raw data blob according to the flags given.
  417. *
  418. * MCR migration note: this replaces Revision::getRevisionText
  419. *
  420. * @note direct use is deprecated, use getBlob() or SlotRecord::getContent() instead.
  421. * @todo make this private, there should be no need to use this method outside this class.
  422. *
  423. * @param string $raw The raw blob data, to be processed according to $flags.
  424. * May be the blob itself, or the blob compressed, or just the address
  425. * of the actual blob, depending on $flags.
  426. * @param string|string[] $flags Blob flags, such as 'external' or 'gzip'.
  427. * Note that not including 'utf-8' in $flags will cause the data to be decoded
  428. * according to the legacy encoding specified via setLegacyEncoding.
  429. * @param string|null $cacheKey A blob address for use in the cache key. If not given,
  430. * caching is disabled.
  431. *
  432. * @return false|string The expanded blob or false on failure
  433. */
  434. public function expandBlob( $raw, $flags, $cacheKey = null ) {
  435. if ( is_string( $flags ) ) {
  436. $flags = explode( ',', $flags );
  437. }
  438. // Use external methods for external objects, text in table is URL-only then
  439. if ( in_array( 'external', $flags ) ) {
  440. $url = $raw;
  441. $parts = explode( '://', $url, 2 );
  442. if ( count( $parts ) == 1 || $parts[1] == '' ) {
  443. return false;
  444. }
  445. if ( $cacheKey ) {
  446. // The cached value should be decompressed, so handle that and return here.
  447. return $this->cache->getWithSetCallback(
  448. $this->getCacheKey( $cacheKey ),
  449. $this->getCacheTTL(),
  450. function () use ( $url, $flags ) {
  451. // Ignore $setOpts; blobs are immutable and negatives are not cached
  452. $blob = $this->extStoreAccess
  453. ->fetchFromURL( $url, [ 'domain' => $this->dbDomain ] );
  454. return $blob === false ? false : $this->decompressData( $blob, $flags );
  455. },
  456. [ 'pcGroup' => self::TEXT_CACHE_GROUP, 'pcTTL' => WANObjectCache::TTL_PROC_LONG ]
  457. );
  458. } else {
  459. $blob = $this->extStoreAccess->fetchFromURL( $url, [ 'domain' => $this->dbDomain ] );
  460. return $blob === false ? false : $this->decompressData( $blob, $flags );
  461. }
  462. } else {
  463. return $this->decompressData( $raw, $flags );
  464. }
  465. }
  466. /**
  467. * If $wgCompressRevisions is enabled, we will compress data.
  468. * The input string is modified in place.
  469. * Return value is the flags field: contains 'gzip' if the
  470. * data is compressed, and 'utf-8' if we're saving in UTF-8
  471. * mode.
  472. *
  473. * MCR migration note: this replaces Revision::compressRevisionText
  474. *
  475. * @note direct use is deprecated!
  476. * @todo make this private, there should be no need to use this method outside this class.
  477. *
  478. * @param mixed &$blob Reference to a text
  479. *
  480. * @return string
  481. */
  482. public function compressData( &$blob ) {
  483. $blobFlags = [];
  484. // Revisions not marked as UTF-8 will have legacy decoding applied by decompressData().
  485. // XXX: if $this->legacyEncoding is not set, we could skip this. That would however be
  486. // risky, since $this->legacyEncoding being set in the future would lead to data corruption.
  487. $blobFlags[] = 'utf-8';
  488. if ( $this->compressBlobs ) {
  489. if ( function_exists( 'gzdeflate' ) ) {
  490. $deflated = gzdeflate( $blob );
  491. if ( $deflated === false ) {
  492. wfLogWarning( __METHOD__ . ': gzdeflate() failed' );
  493. } else {
  494. $blob = $deflated;
  495. $blobFlags[] = 'gzip';
  496. }
  497. } else {
  498. wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" );
  499. }
  500. }
  501. return implode( ',', $blobFlags );
  502. }
  503. /**
  504. * Re-converts revision text according to its flags.
  505. *
  506. * MCR migration note: this replaces Revision::decompressRevisionText
  507. *
  508. * @note direct use is deprecated, use getBlob() or SlotRecord::getContent() instead.
  509. * @todo make this private, there should be no need to use this method outside this class.
  510. *
  511. * @param string $blob Blob in compressed/encoded form.
  512. * @param array $blobFlags Compression flags, such as 'gzip'.
  513. * Note that not including 'utf-8' in $blobFlags will cause the data to be decoded
  514. * according to the legacy encoding specified via setLegacyEncoding.
  515. *
  516. * @return string|bool Decompressed text, or false on failure
  517. */
  518. public function decompressData( $blob, array $blobFlags ) {
  519. // Revision::decompressRevisionText accepted false here, so defend against that
  520. Assert::parameterType( 'string', $blob, '$blob' );
  521. if ( in_array( 'error', $blobFlags ) ) {
  522. // Error row, return false
  523. return false;
  524. }
  525. if ( in_array( 'gzip', $blobFlags ) ) {
  526. # Deal with optional compression of archived pages.
  527. # This can be done periodically via maintenance/compressOld.php, and
  528. # as pages are saved if $wgCompressRevisions is set.
  529. $blob = gzinflate( $blob );
  530. if ( $blob === false ) {
  531. wfWarn( __METHOD__ . ': gzinflate() failed' );
  532. return false;
  533. }
  534. }
  535. if ( in_array( 'object', $blobFlags ) ) {
  536. # Generic compressed storage
  537. $obj = unserialize( $blob );
  538. if ( !is_object( $obj ) ) {
  539. // Invalid object
  540. return false;
  541. }
  542. $blob = $obj->getText();
  543. }
  544. // Needed to support old revisions left over from from the 1.4 / 1.5 migration.
  545. if ( $blob !== false && $this->legacyEncoding
  546. && !in_array( 'utf-8', $blobFlags ) && !in_array( 'utf8', $blobFlags )
  547. ) {
  548. # Old revisions kept around in a legacy encoding?
  549. # Upconvert on demand.
  550. # ("utf8" checked for compatibility with some broken
  551. # conversion scripts 2008-12-30)
  552. # Even with //IGNORE iconv can whine about illegal characters in
  553. # *input* string. We just ignore those too.
  554. # REF: https://bugs.php.net/bug.php?id=37166
  555. # REF: https://phabricator.wikimedia.org/T18885
  556. AtEase::suppressWarnings();
  557. $blob = iconv( $this->legacyEncoding, 'UTF-8//IGNORE', $blob );
  558. AtEase::restoreWarnings();
  559. }
  560. return $blob;
  561. }
  562. /**
  563. * Get the text cache TTL
  564. *
  565. * MCR migration note: this replaces Revision::getCacheTTL
  566. *
  567. * @return int
  568. */
  569. private function getCacheTTL() {
  570. if ( $this->cache->getQoS( WANObjectCache::ATTR_EMULATION )
  571. <= WANObjectCache::QOS_EMULATION_SQL
  572. ) {
  573. // Do not cache RDBMs blobs in...the RDBMs store
  574. $ttl = WANObjectCache::TTL_UNCACHEABLE;
  575. } else {
  576. $ttl = $this->cacheExpiry ?: WANObjectCache::TTL_UNCACHEABLE;
  577. }
  578. return $ttl;
  579. }
  580. /**
  581. * Returns an ID corresponding to the old_id field in the text table, corresponding
  582. * to the given $address.
  583. *
  584. * Currently, $address must start with 'tt:' followed by a decimal integer representing
  585. * the old_id; if $address does not start with 'tt:', null is returned. However,
  586. * the implementation may change to insert rows into the text table on the fly.
  587. * This implies that this method cannot be static.
  588. *
  589. * @note This method exists for use with the text table based storage schema.
  590. * It should not be assumed that is will function with all future kinds of content addresses.
  591. *
  592. * @deprecated since 1.31, so don't assume that all blob addresses refer to a row in the text
  593. * table. This method should become private once the relevant refactoring in WikiPage is
  594. * complete.
  595. *
  596. * @param string $address
  597. *
  598. * @return int|null
  599. */
  600. public function getTextIdFromAddress( $address ) {
  601. list( $schema, $id, ) = self::splitBlobAddress( $address );
  602. if ( $schema !== 'tt' ) {
  603. return null;
  604. }
  605. $textId = intval( $id );
  606. if ( !$textId || $id !== (string)$textId ) {
  607. throw new InvalidArgumentException( "Malformed text_id: $id" );
  608. }
  609. return $textId;
  610. }
  611. /**
  612. * Returns an address referring to content stored in the text table row with the given ID.
  613. * The address schema for blobs stored in the text table is "tt:" followed by an integer
  614. * that corresponds to a value of the old_id field.
  615. *
  616. * @deprecated since 1.31. This method should become private once the relevant refactoring
  617. * in WikiPage is complete.
  618. *
  619. * @param int $id
  620. *
  621. * @return string
  622. */
  623. public static function makeAddressFromTextId( $id ) {
  624. return 'tt:' . $id;
  625. }
  626. /**
  627. * Splits a blob address into three parts: the schema, the ID, and parameters/flags.
  628. *
  629. * @since 1.33
  630. *
  631. * @param string $address
  632. *
  633. * @throws InvalidArgumentException
  634. * @return array [ $schema, $id, $parameters ], with $parameters being an assoc array.
  635. */
  636. public static function splitBlobAddress( $address ) {
  637. if ( !preg_match( '/^(\w+):(\w+)(\?(.*))?$/', $address, $m ) ) {
  638. throw new InvalidArgumentException( "Bad blob address: $address" );
  639. }
  640. $schema = strtolower( $m[1] );
  641. $id = $m[2];
  642. $parameters = isset( $m[4] ) ? wfCgiToArray( $m[4] ) : [];
  643. return [ $schema, $id, $parameters ];
  644. }
  645. public function isReadOnly() {
  646. if ( $this->useExternalStore && $this->extStoreAccess->isReadOnly() ) {
  647. return true;
  648. }
  649. return ( $this->getDBLoadBalancer()->getReadOnlyReason() !== false );
  650. }
  651. }